1. 程式人生 > 其它 >我可以減肥失敗,但我的 Docker 映象一定要瘦身成功!

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


作者|徐偉
來源|爾達 Erda 公眾號

簡介

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

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

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

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

採用 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 的使用,其主要特徵如下:

  • 按層顯示 Docker 映象內容
  • 指出每一層的變化
  • 評估 “映象的效率”,浪費的空間
  • 快速的構建/分析週期
  • 和 CI 整合,方便自動化檢測映象效率是否合格

映象效率分析

之前是通過 docker image history 分析映象體積分佈,並進行映象瘦身,此處將採用 dive 分析映象有效率。

使用方法:dive <image_name>

優化前

原始映象有效率: 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 即可滿足要求。

CI 整合

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

CI=true dive <image-name>

優化前 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)進入交流群討論!

參考

歡迎參與開源

Erda 作為開源的一站式雲原生 PaaS 平臺,具備 DevOps、微服務觀測治理、多雲管理以及快資料治理等平臺級能力。點選下方連結即可參與開源,和眾多開發者一起探討、交流,共建開源社群。歡迎大家關注、貢獻程式碼和 Star!