我可以減肥失敗,但我的 Docker 映象一定要瘦身成功!
作者|徐偉
簡介
容器映象類似於虛擬機器映象,封裝了程式的執行環境,保證了執行環境的一致性,使得我們可以一次建立任意場景部署執行。映象構建的方式有兩種,一種是透過 docker build 執行 Dockerfile 裡的指令來構建映象,另一種是透過 docker commit 將存在的容器打包成映象,通常我們都是使用第一種方式來構建容器映象。
在構建 docker 容器時,我們一般希望儘量減小映象,以便加快映象的分發;但是不恰當的映象構建方式,很容易導致映象過大,造成頻寬和磁碟資源浪費,尤其是遇到 daemonset 這種需要在每臺機器上拉取映象的服務,會造成大量資源浪費;而且映象過大還會影響服務的啟動速度,尤其是處理緊急線上映象變更時,直接影響變更的速度。如果不是刻意控制映象大小、注意映象瘦身,一般的業務系統中可能 90% 以上的大映象都存在映象空間浪費的現象(不信可以嘗試檢測看看)。因此我們非常有必要了解映象瘦身方法,減小容器映象。
如何判斷映象是否需要瘦身
通常,我們可能都是在容器映象過大,明顯影響到映象上傳/拉取速度時,才會考慮到分析映象,嘗試映象瘦身。此時採用的多是 docker image history 等 docker 自帶的映象分析命令,以檢視映象構建歷史、映象大小在各層的分佈等。然後根據經驗判斷是否存在空間浪費,但是這種判斷方式起點較高、沒有量化,不方便自動化判斷。當前,社群中也有很多映象分析工具,其中比較流行的 [dive](
https://
github。com/wagoodman/di
ve
) 分析工具,就可以量化給出_**容器映象有效率**_、_**映象空間浪費率**_等指標,如下圖:
採用 dive 對一個 mysql 映象進行效率分析,發現映象有效率只有 41%,映象空間浪費率高達 59%,顯然需要瘦身。
如何進行映象瘦身
當判斷一個映象需要瘦身後,我們就需要知道如何進行映象瘦身,下面將結合具體案例講解一些典型的映象瘦身方法。
多階段構建
所謂多階段構建,實際上是允許在一個 Dockerfile 中出現多個 FROM 指令。最後生成的映象,以最後一條 FROM 構建階段為準,之前的 FROM 構建階段會被拋棄。透過多階段構建,後一個階段的構建過程可以直接利用前一階段的構建快取,有效降低映象大小。一個典型的場景是將編譯環境和執行環境分離,以一個 go 專案映象構建過程為例:
# Go語言編譯環境基礎映象
FROM golang:1。16-alpine
# 複製原始碼到映象
COPY server。go /build/
# 指定工作目錄
WORKDIR /build
# 編譯映象時,執行 go build 編譯生成 server 程式
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOARM=6 go build -ldflags ‘-w -s’ -o server
# 指定容器執行時入口程式
ENTRYPOINT [“/build/server”]
這種傳統的構建方式有以下缺點:
- 基礎映象為支援編譯環境,包含大量go語言的工具/庫,而執行時並不需要
- COPY 原始碼,增加了映象分層,同時有原始碼洩漏風險
採用多階段構建方式,可以將上述傳統的構建方式修改如下:
## 1 編譯構建階段
# Go語言編譯環境基礎映象
FROM golang:1。16-alpine AS build
# 複製原始碼到映象
COPY server。go /build/
# 指定工作目錄
WORKDIR /build
# 編譯映象時,執行 go build 編譯生成 server 程式
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOARM=6 go build -ldflags ‘-w -s’ -o server
## 2 執行構建階段
# 採用更小的執行時基礎映象
FROM scratch
# 從編譯階段僅複製所需的編譯結果到當前映象中
COPY ——from=build /build/server /build/server
# 指定容器執行時入口程式
ENTRYPOINT [“/build/server”]
可以看到,使用多階段構建,可以獲取如下好處:
- 最終映象只關心執行時,採用了更小的基礎映象。
- 直接複製上一個編譯階段的編譯結果,減少了映象分層,還避免了原始碼洩漏。
減少映象分層
映象的層就像 Git 的提交(commit)一樣,用於儲存映象的當前版本與上一版本之間的差異,但是映象層會佔用空間,擁有的層越多,最終的映象就越大。在構建映象時,RUN, ADD, COPY 指令對應的層會增加映象大小,其他命令並不會增加最終的映象大小。下面以實際工作中的一個案例講解如何減少映象分層,以減小映象大小。
背景
測試專案 mysql 映象時,遇到了容器建立比較慢的情況,我們發現主要是因為容器映象較大,拉取映象時間較長,所以就打算看看 mysql 映象為什麼這麼大,是否可以減小容器映象。
映象大小分析
透過 docker image history 檢視映象構建歷史及各層大小。
映象大小:2。9GB
其相應 Dockerfile 如下:
##
## MySQL 5。7
FROM centos:7
。。。
RUN yum -y install crontabs
RUN groupadd -g ${MY_GID} -r ${MY_GROUP} && \
adduser ${MY_USER} -u ${MY_UID} -M -s /sbin/nologin -g ${MY_GROUP}
# RUN wget https://dev。mysql。com/get/Downloads/MySQL-5。7/mysql-5。7。29-1。el7。x86_64。rpm-bundle。tar
COPY mysql-5。7。29-1。el7。x86_64。rpm-bundle。tar /
RUN tar -vxf /mysql-5。7。29-1。el7。x86_64。rpm-bundle。tar
RUN rm /mysql-5。7。29-1。el7。x86_64。rpm-bundle。tar
RUN yum clean all
RUN yum -y install libaio
RUN yum -y install numactl
RUN yum -y install net-tools
RUN yum -y install perl
# RUN rpm -e ——nodeps mariadb-libs-1:5。5。52-1。el7。x86_64
RUN rpm -ivh mysql-community-common-5。7。29-1。el7。x86_64。rpm
RUN rpm -ivh mysql-community-libs-5。7。29-1。el7。x86_64。rpm
RUN rpm -ivh mysql-community-client-5。7。29-1。el7。x86_64。rpm
RUN rpm -ivh mysql-community-server-5。7。29-1。el7。x86_64。rpm
RUN rm -rf mysql-community-*
RUN yum clean all
## Entrypoint
ENTRYPOINT [“/bin/bash”,“/docker-entrypoint。sh”]
可以發現:Dockerfile 中存在過多分散的 RUN/COPY 指令,而且還是大檔案相關操作,導致了過多的映象分層,使得映象過大,可以嘗試合併相關指令,以減小映象分層。
合併 RUN 指令
該 Dockerfile 中 RUN 指令較多,可以將 RUN 指令合併到同一層:
RUN yum -y install crontabs && \
mv /tmp/dumb-init_1。2。5_x86_64 /usr/bin/dumb-init && \
chmod +x /usr/bin/dumb-init && \
groupadd -g ${MY_GID} -r ${MY_GROUP} && \
adduser ${MY_USER} -u ${MY_UID} -M -s /sbin/nologin -g ${MY_GROUP} && \
tar -vxf /tmp/mysql-5。7。29-1。el7。x86_64。rpm-bundle。tar && \
yum clean all && \
yum -y install libaio numactl net-tools perl && \
rpm -ivh mysql-community-common-5。7。29-1。el7。x86_64。rpm && \
rpm -ivh mysql-community-libs-5。7。29-1。el7。x86_64。rpm && \
rpm -ivh mysql-community-client-5。7。29-1。el7。x86_64。rpm && \
rpm -ivh mysql-community-server-5。7。29-1。el7。x86_64。rpm && \
rm -rf mysql-community-* && \
rm -rf /tmp/mysql-5。7。29-1。el7。x86_64。rpm-bundle。tar
編譯後鏡像大小顯著下降:
映象大小:1。92GB
COPY 指令轉換合併到 RUN 指令
從上圖中可以看到,一個較大的映象層是 COPY 指令導致的,複製的檔案較大,所以我們考慮將 COPY 指令轉換合併到 RUN 指令;具體做法是將檔案上傳到 oss,在 RUN 指令中下載。當然也可以發現之前還有一個 RUN 指令漏掉沒有合併,需要繼續合併到已有 RUN 指令中。
RUN curl -o /tmp/mysql-5。7。29-1。el7。x86_64。rpm-bundle。tar http://xxx。oss。aliyuncs。com/addon-pkgs/mysql-5。7。29-1。el7。x86_64。rpm-bundle。tar && \
tar -vxf /tmp/mysql-5。7。29-1。el7。x86_64。rpm-bundle。tar && \
。。。
編譯後鏡像大小顯著下降:
映象大小: 1。27GB
注意
:此處主要是因為 COPY 指令操作的相關檔案較大,對應層佔用空間較多,才會將 COPY 指令轉換合併到RUN 指令;如果其對應層佔用空間較小,則只需分別合併 COPY 指令、RUN 指令,會更加清晰,而沒必要將兩者轉換合併到一層。
減少容器中不必要的包
還是以上述 mysql 映象為例,我們發現下載的包 mysql-5。7。29-1。el7。x86_64。rpm-bundle。tar 包含如下 rpm 包:
而安裝所需的 rmp 包只有:
刪除不必要的包,用最新的最小 rpm 壓縮包替換 mysql-5。7。29-1。el7。x86_64。rpm-bundle。tar 後重新編譯映象:
映象大小: 1。19GB
映象分析工具
前面我們透過 docker 自帶的 docker image history 命令分析映象,本節主要講解映象分析工具 [dive](wagoodman/dive) 的使用,其主要特徵如下:
- 按層顯示 Docker 映象內容
- 指出每一層的變化
- 評估 “映象的效率”,浪費的空間
- 快速的構建/分析週期
- 和 CI 整合,方便自動化檢測映象效率是否合格
映象效率分析
之前是透過 docker image history 分析映象體積分佈,並進行映象瘦身,此處將採用 dive 分析映象有效率。
使用方法:dive
最佳化前
原始映象有效率
: 41%,大部分映象體積都是浪費的。
如下:
最佳化後
最佳化後鏡像有效率:97%
注意:
最佳化後,映象分層明顯減少,映象有效率顯著提高;但是此時的映象效率提升主要是依靠減少浪費空間獲取的,如果要繼續最佳化映象體積,需要結合映象體積瓶頸點評估下一步最佳化方向。一個通常的繼續最佳化點是:減小基礎映象體積和不必要的包。
如下所示:
番外篇:如何透過映象恢復 Dockerfile
前面主要透過映象分析工具分析映象體積分佈,發現浪費空間,最佳化映象大小。映象分析工具的另一個典型應用場景是:當只有容器映象時如何透過映象恢復 Dockerfile?
映象構建歷史檢視
一般,我們可以透過 docker image history 檢視映象構建歷史、映象層及對應的構建指令,從而還原出對應Dockerfile。
注意:
docker image history 檢視對應的構建命令可能顯示不全,需要帶上 ——no-trunc 選項。
這種方法有如下缺陷:
- 一些指令資訊提取不完整、不易讀,如 COPY/ADD 指令,對應的操作檔案用 id 表示,如下圖所示。
- 對於一些映象層,不是透過 Dockerfile 指令構建出來的,而是直接透過修改容器內容,然後 docker commit 生成,不方便檢視該層變更的檔案。
藉助 dive 分析工具還原
藉助 dive 分析工具還原 Dockerfile,主要是因為 dive 可以指出每一層的變化,如下:
- 可以根據 COPY 層變化內容(右側),直觀判斷複製的檔案。
- 因為可以檢視每一層的變化,所以對於 docker commit 也更容易分析相關操作對應的變動範圍。
思考
映象變胖的原因
映象變胖的原因很多,如:
- 無用檔案,比如編譯過程中的依賴檔案對編譯或執行無關的指令被引入到映象
- 系統映象冗餘檔案多
- 各種日誌檔案,快取檔案
- 重複編譯中間檔案
- 重複複製資原始檔
- 執行無依賴檔案
但是一般情況是,使用者可能對少量的映象空間浪費不那麼敏感;但是在操作大檔案時,一些不當的指令(RUN/COPY/ADD)使用方式卻很容易造成大量的空間浪費,此時尤其要注意映象分析與映象瘦身。
映象瘦身難嗎
對於基礎映象的減小、系統包的減小,將映象體積從 200M 減小到 190M 等可能相對難些,此時需要對程式映象非常熟悉,並結合專門的分析工具具體分析。但是一般場景下,映象的浪費很可能僅僅是因為映象構建命令的使用姿勢不佳。此時結合本文的映象瘦身方法,和 Dockerfile 最佳實踐,一般都能實現映象瘦身。
如何評價瘦身效果(映象效率)
如果可以評價映象的空間使用效率,一方面可以比較直觀的判斷哪些映象空降浪費嚴重,需要瘦身;另一方面也可以對瘦身的效果進行評價。上文介紹的,映象分析工具 [dive](wagoodman/dive) 即可滿足要求。
CI 整合
如果需要對大量映象的體積使用效率進行把關,就必須將效率檢測作為自動化流程的一環,而 [dive](wagoodman/dive) 就比較容易整合到 CI 中,只需執行如下指令:
CI=true dive
最佳化前 mysql 映象執行結果:由上文可知,最佳化前實際效率值為 41%,由於預設效率閾值為 90%,所以執行失敗。
最佳化後鏡像執行結果:效率值為 97%,由於預設效率閾值為 90%,所以執行透過。
同時專案也可以根據其對映象大小的敏感度,將映象大小最為一個檢測條件,如只有映象大小超過 1G 時,才進行映象效率檢測,這就可以避免大量小映象的檢測,加快 CI 流程。
如何自動化的檢測 Docerfile 並給出最佳化建議呢
結合上文,ADD/COPY/RUN 指令對應層會增加最終映象大小,而一般映象的構建過程包含:檔案準備、檔案操作等。檔案準備階段在 ADD/COPY/RUN 指令中都有可能出現;檔案操作階段主要由 RUN 指令實現,如果指令過於分散,檔案操作階段會根據 **寫時複製** 原則,複製一份到當前映象層,造成空間浪費,尤其是在涉及大檔案操作時。更嚴重的情況是,假如對檔案的操作分散在不同的 RUN 指令中,不就造成了多次檔案複製浪費了。試想一下,如果複製和操作在同一層進行,不就可以避免這些檔案跨層複製了嗎。
所以有以下一些通用的最佳化檢測方法和建議:
- 檢測 RUN 指令是否過於分散,建議合併。
- 檢測 COPY/ADD 指令是否有複製大檔案,且在 RUN 指令中有對檔案進行操作,則建議將 COPY/ADD 指令轉換合併到 RUN 指令中。當然此種檢測方法,僅僅只有 Dockerfile 還是不夠的,還需要有上下文,才能檢測相關檔案的大小。
當然還有很多其他的檢測方向和最佳化建議,有待進一步完善,歡迎**新增小助手微信(Erda202106)
進入交流群討論!
參考
- [dive](wagoodman/dive)
- [Best practices for writing Dockerfiles](Best practices for writing Dockerfiles)
歡迎參與開源
Erda 作為開源的一站式雲原生 PaaS 平臺,具備 DevOps、微服務觀測治理、多雲管理以及快資料治理等平臺級能力。
點選下方連結即可參與開源
,和眾多開發者一起探討、交流,共建開源社群。
歡迎大家關注、貢獻程式碼和 Star!
-
Erda Github 地址
:[_
https://
github。com/erda-project
/erda_
](erda-project/erda)
-
Erda Cloud 官網
:[_
https://www。
erda。cloud/_
](Erda Cloud - One-stop enterprise digital platform)