Docker 是一個用於開發,交付和執行應用程式的開發平臺。 它能夠將應用程式和基礎架構分開,保證開發,測試, 部署的環境完全一致,從而達到快速交付的目的。 但是在實際專案中,會對專案中的模組或者服務進行細分, 導致部署的映象過多(50+ 個),過大(打包壓縮後的映象達 50G+),這給部署帶來了不小的隱患,特別是私有化部署(透過移動介質複製映象進行部署)。本文從多篇映象瘦身的文章入手,並進行實踐驗證,結合官方的Dockerfile最佳實踐 總結了映象壓縮的4種方法和日常實踐的多個技巧。
映象構建構建方式映象構建的方式有兩種,一種是透過 docker build 執行 Dockerfile 裡的指令來構建映象,另一種是透過 docker commit 將存在的容器打包成映象。 通常我們都是使用第一種方式來構建容器,二者的區別就像批處理和單步執行一樣。
體積分析Docker映象是由很多映象層(Layers)組成的(最多127層), Dockerfile 中的每條指定都會建立映象層,不過只有 RUN, COPY, ADD 會使映象的體積增加。這個可以透過命令 docker history image_id 來檢視每一層的大小。 這裡我們以官方的 alpine:3.12 為例看看它的映象層情況。
FROM scratchADD alpine-minirootfs-3.12.0-x86_64.tar.gz /CMD ["/bin/sh"]
對比 Dockerfile 和映象歷史層數發現 ADD 命令層佔據了 5.57M 大小,而 CMD 命令層並不佔空間。
映象的層就像 Git 的每一次提交 Commit, 用於儲存映象的上一個版本和當前版本之間的差異。所以當我們使用 docker pull 命令從公有或私有的 Hub 上拉取映象時,它只會下載我們尚未擁有的層。 這是一種非常高效的共享映象的方式,但是有時會被錯誤使用,比如反覆提交。
從上圖看出,基礎映象 alpine:3.12 佔據了 5.57M 大小,idps_sm.tar.gz 檔案佔據了 4.52M。 但是命令 RUN rm -f ./idps_sm.tar.gz 並沒有降低映象大小, 映象大小由一個基礎映象和兩次 ADD 檔案構成。
瘦身方法瞭解了映象構建中體積增大的原因,那麼就可以對症下藥:精簡層數或精簡每一層大小。
精簡層數的方法有如下幾種:RUN指令合併多階段構建精簡每一層的方法有如下幾種:使用合適的基礎映象(首選alpine)刪除RUN的快取檔案映象瘦身關於映象瘦身這塊的實際操作以打包 redis 映象為例,在打包之前我們先拉取官方 redis 的映象, 發現標籤為6的映象大小為 104M, 標籤為 6-alpine 的映象大小為 31.5M。打包的流程如下:
選擇基礎映象,更新軟體源,安裝打包工具下載原始碼並進行打包安裝清理不需要的安裝檔案按照上述的流程,我們編寫如下的Dockerfile,該映象使用命令 docker build --no-cache -t optimize/redis:multiline -f redis_multiline . 打包後鏡像大小為 441M。
FROM ubuntu:focalENV REDIS_VERSION=6.0.5ENV REDIS_URL=http://download.redis.io/releases/redis-$REDIS_VERSION.tar.gz# update source and install toolsRUN sed -i "s/archive.ubuntu.com/mirrors.aliyun.com/g; s/security.ubuntu.com/mirrors.aliyun.com/g" /etc/apt/sources.list RUN apt update RUN apt install -y curl make gcc# download source code and install redisRUN curl -L $REDIS_URL | tar xzvWORKDIR redis-$REDIS_VERSIONRUN makeRUN make install # clean upRUN rm -rf /var/lib/apt/lists/* CMD ["redis-server"]
RUN指令合併指令合併是最簡單也是最方便的降低映象層數的方式。該操作節省空間的原理是在同一層中清理“快取”和工具軟體。 還是打包 redis 的需要,指令合併的Dockerfile如下,打包後的映象大小為 292M。
FROM ubuntu:focalENV REDIS_VERSION=6.0.5ENV REDIS_URL=http://download.redis.io/releases/redis-$REDIS_VERSION.tar.gz# update source and install toolsRUN sed -i "s/archive.ubuntu.com/mirrors.aliyun.com/g; s/security.ubuntu.com/mirrors.aliyun.com/g" /etc/apt/sources.list &&\ apt update &&\ apt install -y curl make gcc &&\# download source code and install redis curl -L $REDIS_URL | tar xzv &&\ cd redis-$REDIS_VERSION &&\ make &&\ make install &&\# clean up apt remove -y --auto-remove curl make gcc &&\ apt clean &&\ rm -rf /var/lib/apt/lists/* CMD ["redis-server"]
使用 docker history 分析 optimize/redis:multiline 和 optimize/redis:singleline 映象,得到如下情況:
分析上圖發現,映象 optimize/redis:multiline 中清理資料的幾層並沒有降低映象的大小,這就是上面說的共享映象層帶來的問題。所以指令合併的方法是透過在同一層中將快取和不用的工具軟體清理掉,以達到減小映象體積的目的。
多階段構建多階段構建方法是官方打包映象的最佳實踐,它是將精簡層數做到極致的方法。通俗點講它是將打包映象分成兩個階段,一個階段用於開發,打包,該階段包含構建應用程式所需的所有內容;一個用於生產執行,該階段只包含你的應用程式以及執行它所需的內容。這被稱為“建造者模式”。兩個階段的關係有點像JDK和JRE的關係。 使用多階段構建肯定會降低映象大小,但是瘦身的粒度和程式語言有關係,對編譯型語言效果比較好,因為它去掉了編譯環境中多餘的依賴,直接使用編譯後的二進位制檔案或jar包。而對於解釋型語言效果就不那麼明顯了。
依然還是上面打包 redis 映象的需求,使用多階段構建的 Dockerfile,打包後的進行大小為135M。
FROM ubuntu:focal AS buildENV REDIS_VERSION=6.0.5ENV REDIS_URL=http://download.redis.io/releases/redis-$REDIS_VERSION.tar.gz# update source and install toolsRUN sed -i "s/archive.ubuntu.com/mirrors.aliyun.com/g; s/security.ubuntu.com/mirrors.aliyun.com/g" /etc/apt/sources.list &&\ apt update &&\ apt install -y curl make gcc &&\# download source code and install redis curl -L $REDIS_URL | tar xzv &&\ cd redis-$REDIS_VERSION &&\ make &&\ make installFROM ubuntu:focal# copyENV REDIS_VERSION=6.0.5COPY --from=build /usr/local/bin/redis* /usr/local/bin/CMD ["redis-server"]
相比 optimize/redis:singleline 改動有以下三點:
第一行多了As build, 為後面的COPY做準備第一階段中沒有了清理操作,因為第一階段構建的映象只有編譯的目標檔案(二進位制檔案或jar包)有用,其它的都無用第二階段直接從第一階段複製目標檔案同樣的,使用 docker history 檢視映象體積情況:
比較我們使用多階段構建的映象和官方提供 redis:6(無法和 redis:6-alpine 相比,因為 redis:6 和 ubuntu:focal 都是基於 debain 的映象),發現二者有 30M 的空間。研究 redis:6 的 Dockerfile 發現如下"騷操作":
serverMd5="$(md5sum /usr/local/bin/redis-server | cut -d' ' -f1)"; export serverMd5; \find /usr/local/bin/redis* -maxdepth 0 \ -type f -not -name redis-server \ -exec sh -eux -c ' \ md5="$(md5sum "$1" | cut -d" " -f1)"; \ test "$md5" = "$serverMd5"; \ ' -- '{}' ';' \ -exec ln -svfT 'redis-server' '{}' ';' \
編譯 redis 的原始碼發現二進位制檔案 redis-server 和 redis-check-aof(aof持久化), redis-check-rdb(rdb持久化), redis-sentinel(redis哨兵)是相同的檔案,大小為 11M。官方映象透過上面的指令碼將後三個透過 ln 來生成。
使用合適的基礎映象基礎映象,推薦使用 Alpine。Alpine 是一個高度精簡又包含了基本工具的輕量級 Linux 發行版,基礎映象只有 4.41M,各開發語言和框架都有基於 Alpine 製作的基礎映象,強烈推薦使用它。進階可以嘗試使用scratch和busybox映象進行基礎映象的構建。 從官方映象 redis:6(104M) 和 redis:6-alpine(31.5M) 就可以看出 alpine 的映象只有基於debian映象的 1/3。
使用 Alpine映象有個注意點,就是它是基於 muslc的(glibc的替代標準庫),這兩個庫實現了相同的核心介面。 其中 glibc 更常見,速度更快,而 muslic 使用較少的空間,側重於安全性。 在編譯應用程式時,大部分都是針對特定的 libc 進行編譯的。如果我們要將它們與另一個 libc 一起使用,則必須重新編譯它們。換句話說,基於 Alpine 基礎映象構建容器可能會導致非預期的行為,因為標準 C 庫是不一樣的。 不過,這種情況比較難碰到,即使碰到也有解決方法。