Docker 是一個用於開發,交付和運行應用程序的開發平臺。 它可以將應用程序和基礎架構分開,保證開發,測試,
部署的環境徹底一致,從而達到快速交付的目的。 可是在實際項目中,會對項目中的模塊或者服務進行細分,
致使部署的鏡像過多(50+ 個),過大(打包壓縮後的鏡像達 50G+),這給部署帶來了不小的隱患,特別是私有化部署(經過移動介質拷貝鏡像進行部署)。本文從多篇鏡像瘦身的文章入手,並進行實踐驗證,結合官方的Dockerfile最佳實踐 總結了鏡像壓縮的4種方法和平常實踐的多個技巧。linux
鏡像構建的方式有兩種,一種是經過 docker build
執行 Dockerfile 裏的指令來構建鏡像,另外一種是經過 docker commit
將存在的容器打包成鏡像。 一般咱們都是使用第一種方式來構建容器,兩者的區別就像批處理和單步執行同樣。git
Docker鏡像是由不少鏡像層(Layers)組成的(最多127層), Dockerfile 中的每條指定都會建立鏡像層,不過只有 RUN
, COPY
, ADD
會使鏡像的體積增長。這個能夠經過命令 docker history image_id
來查看每一層的大小。
這裏咱們以官方的 alpine:3.12 爲例看看它的鏡像層狀況。github
FROM scratch ADD alpine-minirootfs-3.12.0-x86_64.tar.gz / CMD ["/bin/sh"]
對比 Dockerfile 和鏡像歷史層數發現 ADD
命令層佔據了 5.57M 大小,而 CMD
命令層並不佔空間。golang
鏡像的層就像 Git
的每一次提交 Commit
, 用於保存鏡像的上一個版本和當前版本之間的差別。因此當咱們使用docker pull
命令從公有或私有的 Hub 上拉取鏡像時,它只會下載咱們還沒有擁有的層。
這是一種很是高效的共享鏡像的方式,可是有時會被錯誤使用,好比反覆提交。
從上圖看出,基礎鏡像 alpine:3.12 佔據了 5.57M 大小,idps_sm.tar.gz 文件佔據了 4.52M。 可是命令 RUN rm -f ./idps_sm.tar.gz
並無下降鏡像大小, 鏡像大小由一個基礎鏡像和兩次 ADD
文件構成。redis
瞭解了鏡像構建中體積增大的緣由,那麼就能夠對症下藥:精簡層數或精簡每一層大小。docker
精簡層數的方法有以下幾種:編程
精簡每一層的方法有以下幾種:ubuntu
關於鏡像瘦身這塊的實際操做以打包 redis 鏡像爲例,在打包以前咱們先拉取官方 redis 的鏡像,
發現標籤爲6的鏡像大小爲 104M, 標籤爲 6-alpine 的鏡像大小爲 31.5M。打包的流程以下:centos
按照上述的流程,咱們編寫以下的Dockerfile,該鏡像使用命令 docker build --no-cache -t optimize/redis:multiline -f redis_multiline .
打包後鏡像大小爲 441M。緩存
FROM ubuntu:focal ENV REDIS_VERSION=6.0.5 ENV REDIS_URL=http://download.redis.io/releases/redis-$REDIS_VERSION.tar.gz # update source and install tools RUN 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 redis RUN curl -L $REDIS_URL | tar xzv WORKDIR redis-$REDIS_VERSION RUN make RUN make install # clean up RUN rm -rf /var/lib/apt/lists/* CMD ["redis-server"]
指令合併是最簡單也是最方便的下降鏡像層數的方式。該操做節省空間的原理是在同一層中清理「緩存」和工具軟件。
仍是打包 redis 的須要,指令合併的Dockerfile以下,打包後的鏡像大小爲 292M。
FROM ubuntu:focal ENV REDIS_VERSION=6.0.5 ENV REDIS_URL=http://download.redis.io/releases/redis-$REDIS_VERSION.tar.gz # update source and install tools RUN 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 build ENV REDIS_VERSION=6.0.5 ENV REDIS_URL=http://download.redis.io/releases/redis-$REDIS_VERSION.tar.gz # update source and install tools RUN 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 FROM ubuntu:focal # copy ENV REDIS_VERSION=6.0.5 COPY --from=build /usr/local/bin/redis* /usr/local/bin/ CMD ["redis-server"]
相比 optimize/redis:singleline 改動有如下三點:
一樣的,使用 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 庫是不同的。
不過,這種狀況比較難碰到,即便碰到也有解決方法。
linux中大部分包管理軟件都須要更新源,該操做會帶來一些緩存文件,這裏記錄了經常使用的清理方法。
基於debian的鏡像
# 換國內源,並更新 sed -i 「s/deb.debian.org/mirrors.aliyun.com/g」 /etc/apt/sources.list && apt update # --no-install-recommends 頗有用 apt install -y --no-install-recommends a b c && rm -rf /var/lib/apt/lists/*
alpine鏡像
# 換國內源,並更新 sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories # --no-cache 表示不緩存 apk add --no-cache a b c && rm -rf /var/cache/apk/*
centos鏡像
# 換國內源並更新 curl -o /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo && yum makecache yum install -y a b c && yum clean al
FROM golang:1.11-alpine AS build # 安裝項目所需工具 # Run `docker build --no-cache .` to update dependencies RUN apk add --no-cache git RUN go get github.com/golang/dep/cmd/dep # 安裝項目的依賴庫(GO使用 Gopkg.toml and Gopkg.lock) # These layers are only re-built when Gopkg files are updated COPY Gopkg.lock Gopkg.toml /go/src/project/ WORKDIR /go/src/project/ # Install library dependencies RUN dep ensure -vendor-only # 拷貝項目並進行構建 # This layer is rebuilt when a file changes in the project directory COPY . /go/src/project/ RUN go build -o /bin/project # 精簡的生成環境 FROM scratch COPY --from=build /bin/project /bin/project ENTRYPOINT ["/bin/project"] CMD ["--help"]
解決 glibc 問題
ENV ALPINE_GLIBC_VERSION="2.31-r0" ENV LANG=C.UTF-8 RUN set -x \ && sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories \ && apk add --no-cache wget \ && wget -q -O /etc/apk/keys/sgerrand.rsa.pub https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub \ && wget -O https://github.com/sgerrand/alpine-pkg-glibc/releases/download/$ALPINE_GLIBC_VERSION/glibc-$ALPINE_GLIBC_VERSION.apk \ && wget -O https://github.com/sgerrand/alpine-pkg-glibc/releases/download/$ALPINE_GLIBC_VERSION/glibc-$ALPINE_GLIBC_VERSION.apk \ && wget -O https://github.com/sgerrand/alpine-pkg-glibc/releases/download/$ALPINE_GLIBC_VERSION/glibc-bin-$ALPINE_GLIBC_VERSION.apk \ && wget -O https://github.com/sgerrand/alpine-pkg-glibc/releases/download/$ALPINE_GLIBC_VERSION/glibc-i18n-$ALPINE_GLIBC_VERSION.apk \ && apk add --no-cache glibc-$ALPINE_GLIBC_VERSION.apk \ glibc-bin-$ALPINE_GLIBC_VERSION.apk \ glibc-i18n-$ALPINE_GLIBC_VERSION.apk \ && /usr/glibc-compat/bin/localedef --force --inputfile POSIX --charmap UTF-8 "$LANG" || true \ && echo "export LANG=$LANG" > /etc/profile.d/locale.sh \ && apk del glibc-i18n \ && rm glibc-$ALPINE_GLIBC_VERSION.apk glibc-bin-$ALPINE_GLIBC_VERSION.apk glibc-i18n-$ALPINE_GLIBC_VERSION.apk
若是該文章對您產生了幫助,或者您對技術文章感興趣,能夠關注微信公衆號: 技術茶話會, 可以第一時間收到相關的技術文章,謝謝!
本篇文章由一文多發平臺ArtiPub自動發佈