我可以減肥失敗,但我的 Docker 映象一定要瘦身成功!

作者|徐偉

簡介

容器映象類似於虛擬機器映象,封裝了程式的執行環境,保證了執行環境的一致性,使得我們可以一次建立任意場景部署執行。映象構建的方式有兩種,一種是透過 docker build 執行 Dockerfile 裡的指令來構建映象,另一種是透過 docker commit 將存在的容器打包成映象,通常我們都是使用第一種方式來構建容器映象。

在構建 docker 容器時,我們一般希望儘量減小映象,以便加快映象的分發;但是不恰當的映象構建方式,很容易導致映象過大,造成頻寬和磁碟資源浪費,尤其是遇到 daemonset 這種需要在每臺機器上拉取映象的服務,會造成大量資源浪費;而且映象過大還會影響服務的啟動速度,尤其是處理緊急線上映象變更時,直接影響變更的速度。如果不是刻意控制映象大小、注意映象瘦身,一般的業務系統中可能 90% 以上的大映象都存在映象空間浪費的現象(不信可以嘗試檢測看看)。因此我們非常有必要了解映象瘦身方法,減小容器映象。

如何判斷映象是否需要瘦身

通常,我們可能都是在容器映象過大,明顯影響到映象上傳/拉取速度時,才會考慮到分析映象,嘗試映象瘦身。此時採用的多是 docker image history 等 docker 自帶的映象分析命令,以檢視映象構建歷史、映象大小在各層的分佈等。然後根據經驗判斷是否存在空間浪費,但是這種判斷方式起點較高、沒有量化,不方便自動化判斷。當前,社群中也有很多映象分析工具,其中比較流行的 [dive](

https://

github。com/wagoodman/di

ve

) 分析工具,就可以量化給出_**容器映象有效率**_、_**映象空間浪費率**_等指標,如下圖:

我可以減肥失敗,但我的 Docker 映象一定要瘦身成功!

採用 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

我可以減肥失敗,但我的 Docker 映象一定要瘦身成功!

其相應 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

我可以減肥失敗,但我的 Docker 映象一定要瘦身成功!

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 包:

我可以減肥失敗,但我的 Docker 映象一定要瘦身成功!

而安裝所需的 rmp 包只有:

我可以減肥失敗,但我的 Docker 映象一定要瘦身成功!

刪除不必要的包,用最新的最小 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%,大部分映象體積都是浪費的。

如下:

我可以減肥失敗,但我的 Docker 映象一定要瘦身成功!

最佳化後

最佳化後鏡像有效率:97%

注意:

最佳化後,映象分層明顯減少,映象有效率顯著提高;但是此時的映象效率提升主要是依靠減少浪費空間獲取的,如果要繼續最佳化映象體積,需要結合映象體積瓶頸點評估下一步最佳化方向。一個通常的繼續最佳化點是:減小基礎映象體積和不必要的包。

如下所示:

我可以減肥失敗,但我的 Docker 映象一定要瘦身成功!

番外篇:如何透過映象恢復 Dockerfile

前面主要透過映象分析工具分析映象體積分佈,發現浪費空間,最佳化映象大小。映象分析工具的另一個典型應用場景是:當只有容器映象時如何透過映象恢復 Dockerfile?

映象構建歷史檢視

一般,我們可以透過 docker image history 檢視映象構建歷史、映象層及對應的構建指令,從而還原出對應Dockerfile。

注意:

docker image history 檢視對應的構建命令可能顯示不全,需要帶上 ——no-trunc 選項。

這種方法有如下缺陷:

- 一些指令資訊提取不完整、不易讀,如 COPY/ADD 指令,對應的操作檔案用 id 表示,如下圖所示。

我可以減肥失敗,但我的 Docker 映象一定要瘦身成功!

- 對於一些映象層,不是透過 Dockerfile 指令構建出來的,而是直接透過修改容器內容,然後 docker commit 生成,不方便檢視該層變更的檔案。

藉助 dive 分析工具還原

藉助 dive 分析工具還原 Dockerfile,主要是因為 dive 可以指出每一層的變化,如下:

- 可以根據 COPY 層變化內容(右側),直觀判斷複製的檔案。

- 因為可以檢視每一層的變化,所以對於 docker commit 也更容易分析相關操作對應的變動範圍。

我可以減肥失敗,但我的 Docker 映象一定要瘦身成功!

思考

映象變胖的原因

映象變胖的原因很多,如:

- 無用檔案,比如編譯過程中的依賴檔案對編譯或執行無關的指令被引入到映象

- 系統映象冗餘檔案多

- 各種日誌檔案,快取檔案

- 重複編譯中間檔案

- 重複複製資原始檔

- 執行無依賴檔案

但是一般情況是,使用者可能對少量的映象空間浪費不那麼敏感;但是在操作大檔案時,一些不當的指令(RUN/COPY/ADD)使用方式卻很容易造成大量的空間浪費,此時尤其要注意映象分析與映象瘦身。

映象瘦身難嗎

對於基礎映象的減小、系統包的減小,將映象體積從 200M 減小到 190M 等可能相對難些,此時需要對程式映象非常熟悉,並結合專門的分析工具具體分析。但是一般場景下,映象的浪費很可能僅僅是因為映象構建命令的使用姿勢不佳。此時結合本文的映象瘦身方法,和 Dockerfile 最佳實踐,一般都能實現映象瘦身。

如何評價瘦身效果(映象效率)

如果可以評價映象的空間使用效率,一方面可以比較直觀的判斷哪些映象空降浪費嚴重,需要瘦身;另一方面也可以對瘦身的效果進行評價。上文介紹的,映象分析工具 [dive](wagoodman/dive) 即可滿足要求。

CI 整合

如果需要對大量映象的體積使用效率進行把關,就必須將效率檢測作為自動化流程的一環,而 [dive](wagoodman/dive) 就比較容易整合到 CI 中,只需執行如下指令:

​CI=true dive

最佳化前 mysql 映象執行結果:由上文可知,最佳化前實際效率值為 41%,由於預設效率閾值為 90%,所以執行失敗。

我可以減肥失敗,但我的 Docker 映象一定要瘦身成功!

最佳化後鏡像執行結果:效率值為 97%,由於預設效率閾值為 90%,所以執行透過。

我可以減肥失敗,但我的 Docker 映象一定要瘦身成功!

同時專案也可以根據其對映象大小的敏感度,將映象大小最為一個檢測條件,如只有映象大小超過 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)