最小化 Java 映象的常用技巧
背景
隨著容器技術的普及,越來越多的應用被容器化。人們使用容器的頻率越來越高,但常常忽略一個基本但又非常重要的問題 - 容器映象的體積。本文將介紹精簡容器映象的必要性並以基於 spring boot 的 java 應用為例描述最小化容器映象的常用技巧。
精簡容器映象的必要性
精簡容器映象是非常必要的,下面分別從安全性和敏捷性兩個角度進行闡釋。
安全性
基於安全方面的考慮,將不必要的元件從映象中移除可以減少攻擊面、降低安全風險。雖然 docker 支援使用者通過 Seccomp 限制容器內可以執行操作或者使用 AppArmor 為容器配置安全策略,但它們的使用門檻較高,要求使用者具備安全領域的專業素養。
敏捷性
精簡的容器映象能提高容器的部署速度。假設某一時刻訪問流量激增,您需要通過增加容器副本數以應對突發壓力。如果某些宿主機不包含目標映象,需要先拉取映象,然後啟動容器,這時使用體積較小的映象能加速這一過程、縮短擴容時間。另外,映象體積越小,其構建速度也越快,同時還能減少儲存和傳輸的成本。
常用技巧
將一個 java 應用容器化所需的步驟可歸納如下:
- 編譯 java 原始碼並生成 jar 包。
- 將應用 jar 包和依賴的第三方 jar 包移動到合適的位置。
本章所用的樣例是一個基於 spring boot 的 java 應用 spring-boot-docker,所用的未經優化的
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 為依賴 java、python、nodejs、dotnet 等環境的應用提供了基礎映象。
使用 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。
其他技巧
除了可以通過上述技巧精簡映象外,還有以下方式:
- 將 dockerfile 中的多條指令合併成一條,通過減少映象層數的方式達到精簡映象體積的目的。
- 將穩定且體積較大的內容置於映象下層,將變動頻繁且體積較小的內容置於映象上層。雖然該方式無法直接精簡映象體積,但充分利用了映象的快取機制,同樣可以達到加快映象構建和容器部署的目的。
想了解更多優化 dockerfile 的小竅門可參考教程 Best practices for writing Dockerfiles。
總結
- 本文通過一系列的優化,將 java 應用的映象體積由最初的 719MB 縮小到 100MB 左右。如果您的應用依賴其他環境,也可以用類似的原則進行優化。
- 針對 java 映象,google 提供的另一款工具 jib 能為您遮蔽映象構建過程中的複雜細節,自動構建出精簡的 java 映象。使用它您無須編寫 dockerfile,甚至不需要安裝 docker。
- 對於類似 distroless 這樣無法 attach 或者不方便 attach 的容器,建議您將它們的日誌中心化儲存,以便問題的追蹤和排查。具體方法可參考文章面向容器日誌的技術實踐。
本文作者:吳波bruce_wu
本文為雲棲社群原創內容,未經允許不得轉載。