優(yōu)化docker鏡像體積的方法詳解
關注鏡像體積有關的關鍵點如下:
RUN、COPY和ADD指令會在已有鏡像層的基礎上創(chuàng)建一個新的鏡像層,執(zhí)行指令產(chǎn)生的所有文件系統(tǒng)變更會在指令結束后作為一個鏡像層整體提交。- 鏡像層具有
copy-on-write的特性,如果去更新其他鏡像層中已存在的文件,會先將其復制到新的鏡像層中再修改,造成雙倍的文件空間占用。 - 如果去刪除其他鏡像層的一個文件,只會在當前鏡像層生成一個該文件的刪除標記,并不會減少整個鏡像的實際體積。
上述理論可以通過如下 Dockerfile 來驗證:
FROM alpine:latest COPY resource.tar / RUN touch /resource.tar RUN rm -f /resource.tar ENTRYPOINT ["/bin/ash"]
我們在 Dockerfile 中簡單地添加、修改和刪除某個資源文件,然后構建鏡像查看其鏡像層信息:
$ docker build -t test-image -f Dockerfile . $ docker history test-image:latest IMAGE CREATED CREATED BY SIZE COMMENT 95f1695b2904 About a minute ago /bin/sh -c #(nop) ENTRYPOINT ["/bin/ash"] 0B 1780448c656f About a minute ago /bin/sh -c rm -f /resource.tar 0B a85d29bf7738 About a minute ago /bin/sh -c touch /resource.tar 135MB 6dac335fa653 4 minutes ago /bin/sh -c #(nop) COPY file:66065d6e23e0bc52… 135MB e66264b98777 7 weeks ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B <missing> 7 weeks ago /bin/sh -c #(nop) ADD file:8e81116368669ed3d… 5.53MB
在 docker history 的輸出結果中可以看到:
RUN touch /resource.tar指令只是修改了文件的元信息,但依然將整個文件拷貝到了新的鏡像層中。RUN rm -f /resource.tar指令雖然刪除了文件,并且該文件在運行容器時不可見,但依然在前兩個鏡像層中以及最終的鏡像中存在。
分析工具
給代碼做性能調(diào)優(yōu)時,首先要借助 Profiling 工具找到代碼的性能瓶頸,對于優(yōu)化鏡像體積也是如此。下面介紹兩個可以分析鏡像體積的工具:
docker history
docker 自帶的 docker history 命令,該命令可以展示所有鏡像層的創(chuàng)建時間、指令以及體積等較為基礎的信息,但對于復雜的鏡像則有些乏力。使用方式見上方的示例。
dive
第三方的 dive 工具,該工具可以分析鏡像層組成,并列出每個鏡像層所包含的文件列表,可以很方便地定位到影響鏡像體積的構建指令以及具體文件。
以 golang:1.16 鏡像為例,首先安裝 dive,然后執(zhí)行 dive golang:1.16,輸出如下:

如上圖所示,在左側(cè)選中鏡像層后,在右側(cè)的文件樹視圖中可以清晰地看到該層的具體文件,并能夠篩選相比上一層新增、更新或刪除的文件。在選中的鏡像層中,由于執(zhí)行了 apt-get 安裝編譯依賴,因此在 /usr/lib 目錄下新增了 150MB 依賴庫文件。
優(yōu)化技巧
下面介紹一些優(yōu)化效果比較顯著的優(yōu)化技巧。
分階段構建與從零構建
分階段構建(multi-stage builds)和從零構建(build from scratch)是優(yōu)化鏡像體積的基本手段和必備技巧。該技巧將鏡像構建過程區(qū)分為構建和運行環(huán)境,在構建環(huán)境安裝編譯器等依賴并編譯所需的二進制包,然后將其復制到僅包含必要運行依賴的運行環(huán)境中。
對 golang 這類能夠編譯靜態(tài)二進制文件的語言來說分階段構建的效果尤為明顯,我們可以將編譯產(chǎn)生的二進制文件放到 scratch 鏡像中運行(scratch 是一個特殊的空鏡像):
FROM golang COPY hell0.go . ENV CGO_ENABLED=0 RUN go build hello.go FROM scratch COPY --from=0 /go/hello . CMD ["./hello"]
如果直接使用 golang 鏡像作為運行環(huán)境,其鏡像體積通常接近 1 個 G,其中大部分文件都不是在運行容器時所必要的。 將編譯結果拷貝到運行環(huán)境后,體積只有幾十 kb~mb 不等,如果需要在運行容器中保留基本的系統(tǒng)工具,可以考慮使用 alpine 鏡像作為運行環(huán)境。
關于分階段構建和從零構建的更多細節(jié)可參考 Docker 官方文檔中的 Use multi-stage builds 和 Create a simple parent image using scratch。
避免產(chǎn)生無用的文檔或緩存
docker 鏡像不應該包含文檔、緩存等對運行容器沒有作用的內(nèi)容。
docker 鏡像不應該包含文檔、緩存等對運行容器沒有作用的內(nèi)容。
避免在本地保留安裝緩存。
大部分包管理器會在安裝時緩存下載的資源以備之后使用,以 pip 為例,會將下載的響應和構建的中間文件保存在
~/.cache/pip目錄,應使用--no-cache-dir選項禁用默認的緩存行為。避免安裝文檔。
部分包管理器提供了選項可以不安裝附帶的文檔,如 dnf 可使用
--nodocs選項。避免緩存包索引。
部分包管理器在執(zhí)行安裝之前,會嘗試查詢所有已啟用倉庫的包列表、版本等元信息緩存在本地作為索引。個別倉庫的索引緩存可達到 150 M 以上。 我們應該僅在安裝包時查詢索引,并在安裝完成后清理,不應該在單獨的指令中執(zhí)行
yum makecache這類緩存索引的命令。
及時清理不需要的文件
運行容器時不需要的文件,一定要在創(chuàng)建的同一層清理,否則依然會保留在最終的鏡像中。
通過包管理安裝包,通常會產(chǎn)生大量的緩存文件,一定要在同一 RUN 指令的結尾處立刻清理。在安裝依賴數(shù)量較多時,可以節(jié)省大量的緩存空間。
以 dnf 為例:
RUN dnf install -y --nodocs <PACKAGES> \ && dnf clean all \ && rm -rf /var/cache/dnf
以 apt 為例:
RUN apt-get update \ && apt-get install -y <PACKAGES> \ && rm -rf /var/lib/apt/lists/* # 官方的 ubuntu/debian 鏡像 apt-get 會在安裝后自動執(zhí)行 clean 命令
合并多個鏡像層
上文解釋過,應該避免在不同鏡像層中更新文件而造成額外的體積占用。當構建的層數(shù)很多且執(zhí)行指令較復雜時,很難避免在不同的鏡像層中更新文件,可通過以下手段精簡這部分額外體積:
在最終生成鏡像時將所有鏡像層合并成一層,在 docker build 命令中使用 —squash 即可實現(xiàn)(需要開啟 docker daemon 的實驗性功能)。以本文開頭的 Dockerfile 為例:
$ docker build -t squash-image --squash -f Dockerfile . $ docker history squash-image IMAGE CREATED CREATED BY SIZE COMMENT 55ded8881d63 9 hours ago 0B merge sha256:95f1695b29044522250de1b0c1904aaf8670b991ec1064d086c0c15865051d5d to sha256:e66264b98777e12192600bf9b4d663655c98a090072e1bab49e233d7531d1294 <missing> 11 hours ago /bin/sh -c #(nop) ENTRYPOINT ["/bin/ash"] 0B <missing> 11 hours ago /bin/sh -c rm -f /resource.tar 0B <missing> 11 hours ago /bin/sh -c touch /resource.tar 0B <missing> 11 hours ago /bin/sh -c #(nop) COPY file:66065d6e23e0bc52… 0B <missing> 7 weeks ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B <missing> 7 weeks ago /bin/sh -c #(nop) ADD file:8e81116368669ed3d… 5.53MB
最終生成的鏡像只有一個鏡像層,包含最后實際存在的文件系統(tǒng),在合并所有鏡像層的過程中,相當于禁用了
copy-on-write特性。這種做法的壞處在于,鏡像在保存和分發(fā)時是可以復用鏡像層的,推送鏡像時會跳過鏡像倉庫已存在的鏡像層,拉取鏡像時會跳過本地已拉取過的鏡像層,而合并成一層后則失去了這種優(yōu)勢。
對于可能和其他共用鏡像層的場景,可以采取下面一種方式。
分階段構建,將部分中間鏡像層壓縮成一層作為基礎鏡像。 在開發(fā)團隊內(nèi)部,我們往往會在官方鏡像的基礎上添加或更新部分依賴,然后作為團隊內(nèi)部統(tǒng)一使用的基礎鏡像,這種復用方式可以大大減少實際占用的鏡像體積。 更進一步,我們可以將這類基礎鏡像壓縮成一層。下面以 golang 官方鏡像為例:
FROM golang:1.16 as base FROM scratch COPY --from=base / / ENTRYPOINT ["/bin/bash"]
壓縮成一層后,
golang:1.16的鏡像體積從 919MB 變成 913MB,官方鏡像已經(jīng)做了很多優(yōu)化所以節(jié)省空間十分有限,但對于開發(fā)團隊內(nèi)部制作的基礎鏡像,這種優(yōu)化往往會帶來意外驚喜。
復制文件的同時修改元信息
先將文件添加到鏡像內(nèi),然后再修改文件的執(zhí)行權限和所屬用戶,這類 COPY-RUN 指令在 Dockerfile 中十分常見:
COPY output/hello /usr/bin/hello RUN chmod +x /usr/bin/hello && chown normal:normal /usr/bin/hello
但修改文件元信息也會將文件復制到新的鏡像層,以上指令會產(chǎn)生兩份相同的文件。在文件體積較大時,會顯著增加整個鏡像的體積。 事實上,我們可以在復制文件的同時完成對文件元信息的修改,COPY 和 ADD 指令都提供了修改元信息的 --chmod 和 --chown 選項:
COPY --chmod=755 --chown=normal:normal output/hello /usr/bin/hello
--chmod 特性目前還未添加到官方文檔,使用前需要開啟 docker 的 buildkit 特性(在 docker build 命令前添加 DOCKER_BUILDKIT=1 即可),目前只支持 --chmod=755 和 --chmod=0755 這種設置方法,不支持 --chmod=+x。
注:經(jīng)測試,當使用 ADD 指令且源文件為下載鏈接時 --chmod 選項不起作用,不清楚這是 docker 的 bug 還是 feature。解決方案是直接使用 RUN 指令 wget + chmod 來替代 ADD。
以上就是優(yōu)化docker鏡像體積的方法詳解的詳細內(nèi)容,更多關于docker鏡像體積優(yōu)化的資料請關注腳本之家其它相關文章!
相關文章
手把手教你docker部署(使用docker-compose)教程
使用 Docker Compose 可以輕松、高效的管理容器,下面這篇文章主要給大家介紹了關于手把手教你docker部署(使用docker-compose)的相關資料,文中通過實例代碼介紹的非常詳細,需要的朋友可以參考下2023-01-01
Docker部署Rabbitmq容器實現(xiàn)過程解析
這篇文章主要介紹了Docker部署Rabbitmq容器實現(xiàn)過程解析,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2020-11-11
docker 在容器外執(zhí)行某個容器內(nèi)的某個命令操作
這篇文章主要介紹了docker 在容器外執(zhí)行某個容器內(nèi)的某個命令操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-11-11
docker可視化管理工具portainer忘記密碼重置教程的實現(xiàn)
本文主要介紹了docker可視化管理工具portainer忘記密碼重置教程的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2023-08-08

