最小化 Java 鏡像的常用技巧


背景

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

精簡容器鏡像的必要性

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

安全性

基於安全方面的考慮,將不必要的組件從鏡像中移除可以減少攻擊面、降低安全風險。雖然 docker 支持用戶通過 Seccomp 限制容器內可以執行操作或者使用 AppArmor 為容器配置安全策略,但它們的使用門檻較高,要求用戶具備安全領域的專業素養。

敏捷性

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

常用技巧

將一個 java 應用容器化所需的步驟可歸納如下:

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

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


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

閱讀原文

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM