最小化 Java 鏡像的經常使用技巧

背景

隨着容器技術的普及,愈來愈多的應用被容器化。人們使用容器的頻率愈來愈高,但經常忽略一個基本但又很是重要的問題 - 容器鏡像的體積。本文將介紹精簡容器鏡像的必要性並以基於 spring boot 的 java 應用爲例描述最小化容器鏡像的經常使用技巧。java

精簡容器鏡像的必要性

精簡容器鏡像是很是必要的,下面分別從安全性和敏捷性兩個角度進行闡釋。node

安全性python

基於安全方面的考慮,將沒必要要的組件從鏡像中移除能夠減小攻擊面、下降安全風險。雖然 docker 支持用戶經過 Seccomp 限制容器內能夠執行操做或者使用 AppArmor 爲容器配置安全策略,但它們的使用門檻較高,要求用戶具有安全領域的專業素養。git

敏捷性github

精簡的容器鏡像能提升容器的部署速度。假設某一時刻訪問流量激增,您須要經過增長容器副本數以應對突發壓力。若是某些宿主機不包含目標鏡像,須要先拉取鏡像,而後啓動容器,這時使用體積較小的鏡像能加速這一過程、縮短擴容時間。另外,鏡像體積越小,其構建速度也越快,同時還能減小存儲和傳輸的成本。spring

經常使用技巧

將一個 java 應用容器化所需的步驟可概括以下:docker

  1. 編譯 java 源碼並生成 jar 包。
  2. 將應用 jar 包和依賴的第三方 jar 包移動到合適的位置。

本章所用的樣例是一個基於 spring boot 的 java 應用 spring-boot-docker,所用的未經優化的 dockerfile 以下:shell

FROM maven:3.5-jdk-8
COPY src /usr/src/app/src
COPY pom.xml /usr/src/app
RUN mvn -f /usr/src/app/pom.xml clean package
ENTRYPOINT ["java","-jar","/usr/src/app/target/spring-boot-docker-1.0.0.jar"]

因爲應用使用 maven 構建,dockerfile 中指定maven:3.5-jdk-8做爲基礎鏡像,該鏡像的大小爲 635MB。經過這種方式最終構建出的鏡像很是大,達到了 719MB,這是由於一方面基礎鏡像自己就很大,另外一方面 maven 在構建過程當中會下載許多用於執行構建任務的 jar 包。緩存

多階段構建安全

Java 程序的運行只依賴 JRE,並不須要 maven 或者 JDK 中衆多用於編譯、調試、運行的工具,所以一個明顯的優化方法是將用於編譯構建 java 源碼的鏡像和用於運行 java 應用的鏡像分開。爲了達到這一目的,在 docker 17.05 版本以前須要用戶維護 2 個 dockerfile 文件,這無疑增長了構建的複雜性。好在自 17.05 開始,docker 引入了多階段構建的概念,它容許用戶在一個 dockerfile 中使用多個 From 語句。每一個 From 語句能夠指定不一樣的基礎鏡像並將開啓一個全新的構建流程。您能夠選擇性地將前一階段的構建產物複製到另外一個階段,從而只將必要的內容保留在最終的鏡像裏。優化後的 dockerfile 以下:

FROM maven:3.5-jdk-8 AS build
COPY src /usr/src/app/src
COPY pom.xml /usr/src/app
RUN mvn -f /usr/src/app/pom.xml clean package

FROM openjdk:8-jre
ARG DEPENDENCY=/usr/src/app/target/dependency
COPY --from=build ${DEPENDENCY}/BOOT-INF/lib /app/lib
COPY --from=build ${DEPENDENCY}/META-INF /app/META-INF
COPY --from=build ${DEPENDENCY}/BOOT-INF/classes /app
ENTRYPOINT ["java","-cp","app:app/lib/*","hello.Application"]

該 dockerfile 選用maven:3.5-jdk-8做爲第一階段的構建鏡像,選用openjdk:8-jre做爲運行 java 應用的基礎鏡像而且只拷貝了第一階段編譯好的.claass文件和依賴的第三方 jar 包到最終的鏡像裏。經過這種方式優化後的鏡像大小爲 459MB。

使用 distroless 做爲基礎鏡像

雖然經過多階段構建能減少最終生成的鏡像的大小,但 459MB 的體積仍相對過大。經調查發現,這是由於使用的基礎鏡像openjdk:8-jre體積過大,到達了 443MB,所以下一步的優化方向是減少基礎鏡像的體積。

Google 開源的項目 distroless 正是爲了解決基礎鏡像體積過大這一問題。Distroless 鏡像只包含應用程序及其運行時依賴項,不包含包管理器、shell 以及在標準 Linux 發行版中能夠找到的任何其餘程序。目前,distroless 爲依賴 javapythonnodejsdotnet 等環境的應用提供了基礎鏡像。

使用 distroless 的 dockerfile 以下:

FROM maven:3.5-jdk-8 AS build
COPY src /usr/src/app/src
COPY pom.xml /usr/src/app
RUN mvn -f /usr/src/app/pom.xml clean package

FROM gcr.io/distroless/java
ARG DEPENDENCY=/usr/src/app/target/dependency
COPY --from=build ${DEPENDENCY}/BOOT-INF/lib /app/lib
COPY --from=build ${DEPENDENCY}/META-INF /app/META-INF
COPY --from=build ${DEPENDENCY}/BOOT-INF/classes /app
ENTRYPOINT ["java","-cp","app:app/lib/*","hello.Application"]

該 dockerfile 和上一版的惟一區別在於將運行階段依賴的基礎鏡像由openjdk:8-jre(443 MB)替換成了gcr.io/distroless/java(119 MB)。通過這一優化,最終鏡像的大小爲 135MB。

使用 distroless 的惟一不即是您沒法 attach 到一個正在運行的容器上排查問題,由於鏡像中不包含 shell。雖然 distroless 的 debug 鏡像提供 busybox shell,但須要用戶從新打包鏡像、部署容器,對於那些已經基於非 debug 鏡像部署的容器無濟於事。 但從安全角度來看,沒法 attach 容器並不徹底是壞事,由於攻擊者沒法經過 shell 進行攻擊。

使用 alpine 做爲基礎鏡像

若是您確實有 attach 容器的需求,又但願最小化鏡像的大小,能夠選用 alpine 做爲基礎鏡像。Alpine 鏡像的特色是體積很是下,基礎款鏡像的體積僅 4 MB 左右。

使用 alpine 後的 dockerfile 以下:

FROM maven:3.5-jdk-8 AS build
COPY src /usr/src/app/src
COPY pom.xml /usr/src/app
RUN mvn -f /usr/src/app/pom.xml clean package

FROM openjdk:8-jre-alpine
ARG DEPENDENCY=/usr/src/app/target/dependency
COPY --from=build ${DEPENDENCY}/BOOT-INF/lib /app/lib
COPY --from=build ${DEPENDENCY}/META-INF /app/META-INF
COPY --from=build ${DEPENDENCY}/BOOT-INF/classes /app
ENTRYPOINT ["java","-cp","app:app/lib/*","hello.Application"]

這裏並未直接繼承基礎款 alpine,而是選用從 alpine 構建出的包含 java 運行時的openjdk:8-jre-alpine(83MB)做爲基礎鏡像。使用該 dockerfile 構建出的鏡像體積爲 99.2MB,比基於 distroless 的還要小。

執行命令docker exec -ti <container_id> sh能夠成功 attach 到運行的容器中。

distroless vs alpine

既然 distroless 和 alpine 都能提供很是小的基礎鏡像,那麼在生產環境中到底應該選擇哪種呢?若是安全性是您的首要考慮因素,建議選用 distroless,由於它惟一可運行的二進制文件就是您打包的應用;若是您更關注鏡像的體積,能夠選用 alpine。

其餘技巧

除了能夠經過上述技巧精簡鏡像外,還有如下方式:

  1. 將 dockerfile 中的多條指令合併成一條,經過減小鏡像層數的方式達到精簡鏡像體積的目的。
  2. 將穩定且體積較大的內容置於鏡像下層,將變更頻繁且體積較小的內容置於鏡像上層。雖然該方式沒法直接精簡鏡像體積,但充分利用了鏡像的緩存機制,一樣能夠達到加快鏡像構建和容器部署的目的。

想了解更多優化 dockerfile 的小竅門可參考教程 Best practices for writing Dockerfiles

總結

  1. 本文經過一系列的優化,將 java 應用的鏡像體積由最初的 719MB 縮小到 100MB 左右。若是您的應用依賴其餘環境,也能夠用相似的原則進行優化。
  2. 針對 java 鏡像,google 提供的另外一款工具 jib 能爲您屏蔽鏡像構建過程當中的複雜細節,自動構建出精簡的 java 鏡像。使用它您無須編寫 dockerfile,甚至不須要安裝 docker。
  3. 對於相似 distroless 這樣沒法 attach 或者不方便 attach 的容器,建議您將它們的日誌中心化存儲,以便問題的追蹤和排查。具體方法可參考文章面向容器日誌的技術實踐



本文做者:吳波bruce_wu

閱讀原文

本文爲雲棲社區原創內容,未經容許不得轉載。

相關文章
相關標籤/搜索