背景
隨着容器技術的普及,越來越多的應用被容器化。人們使用容器的頻率越來越高,但常常忽略一個基本但又非常重要的問題 - 容器鏡像的體積。本文將介紹精簡容器鏡像的必要性並以基於 spring boot 的 java 應用為例描述最小化容器鏡像的常用技巧。
精簡容器鏡像的必要性
精簡容器鏡像是非常必要的,下面分別從安全性和敏捷性兩個角度進行闡釋。
安全性
基於安全方面的考慮,將不必要的組件從鏡像中移除可以減少攻擊面、降低安全風險。雖然 docker 支持用戶通過 Seccomp 限制容器內可以執行操作或者使用 AppArmor 為容器配置安全策略,但它們的使用門檻較高,要求用戶具備安全領域的專業素養。
敏捷性
精簡的容器鏡像能提高容器的部署速度。假設某一時刻訪問流量激增,您需要通過增加容器副本數以應對突發壓力。如果某些宿主機不包含目標鏡像,需要先拉取鏡像,然后啟動容器,這時使用體積較小的鏡像能加速這一過程、縮短擴容時間。另外,鏡像體積越小,其構建速度也越快,同時還能減少存儲和傳輸的成本。
常用技巧
將一個 java 應用容器化所需的步驟可歸納如下:
- 編譯 java 源碼並生成 jar 包。
- 將應用 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 為依賴 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
本文為雲棲社區原創內容,未經允許不得轉載。