1. 程式人生 > >最小化 Java 映象的常用技巧

最小化 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

閱讀原文


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