在前文Dockefile及命令詳解中咱們已經學習瞭如何經過Dockerfile構建鏡像以及命令的詳細說明,可是在生產環境或項目使用時如何構建出一個儘量小的鏡像是一個必需要學會的要點,本文將帶領你們討論如何精簡鏡像以及精簡鏡像帶來的好處。在學習本文前建議你們看下Docker核心技術原理和Docker容器和鏡像的區別文章中關於鏡像的分層等知識有基礎的瞭解。php
Docker鏡像由不少鏡像層(Layers)組成(最多127層),鏡像層依賴於一系列的底層技術,好比文件系統(filesystems)、寫時複製(copy-on-write)、聯合掛載(union mounts)等技術,總的來講,Dockerfile中的每條指令都會建立一個鏡像層,繼而會增長總體鏡像的尺寸。html
下面是精簡Docker鏡像尺寸的好處:java
一、減小構建時間linux
二、減小磁盤使用量nginx
三、減小下載時間c++
四、由於包含文件少,攻擊面減少,提升了安全性git
五、提升部署速度golang
優化基礎鏡像的方法就是選用合適的更小的基礎鏡像,經常使用的 Linux 系統鏡像通常有 Ubuntu、CentOs、Alpine,其中Alpine更推薦使用。大小對好比下:redis
lynzabo@ubuntu ~/s> docker images REPOSITORY TAG IMAGE ID CREATED SIZE ubuntu latest 74f8760a2a8b 8 days ago 82.4MB alpine latest 11cd0b38bc3c 2 weeks ago 4.41MB centos 7 49f7960eb7e4 7 weeks ago 200MB debian latest 3bbb526d2608 8 days ago 101MB lynzabo@ubuntu ~/s>
Alpine是一個高度精簡又包含了基本工具的輕量級Linux發行版,基礎鏡像只有4.41M,各開發語言和框架都有基於Alpine製做的基礎鏡像,強烈推薦使用它。Alpine鏡像各個語言和框架支持狀況,能夠參考《優化Docker鏡像、加速應用部署,教你6個小竅門》。spring
查看上面的鏡像尺寸對比結果,你會發現最小的鏡像也有4.41M,那麼有辦法構建更小的鏡像嗎?答案是確定的,例如 gcr.io/google_containers/pause-amd64:3.1
鏡像僅有742KB。爲何這個鏡像能這麼小?在爲你們解密以前,再推薦兩個基礎鏡像:
一、scratch鏡像
scratch是一個空鏡像,只能用於構建其餘鏡像,好比你要運行一個包含全部依賴的二進制文件,如Golang程序,能夠直接使用scratch做爲基礎鏡像。如今給你們展現一下上文提到的Google pause鏡像Dockerfile:
FROM scratch ARG ARCH ADD bin/pause-${ARCH} /pause ENTRYPOINT ["/pause"]
Google pause鏡像使用了scratch做爲基礎鏡像,這個鏡像自己是不佔空間的,使用它構建的鏡像大小几乎和二進制文件自己同樣大,因此鏡像很是小。固然在咱們的Golang程序中也會使用。對於一些Golang/C程序,可能會依賴一些動態庫,你可使用自動提取動態庫工具,好比ldd、linuxdeployqt等提取全部動態庫,而後將二進制文件和依賴動態庫一塊兒打包到鏡像中。
二、busybox鏡像
scratch是個空鏡像,若是但願鏡像裏能夠包含一些經常使用的Linux工具,busybox鏡像是個不錯選擇,鏡像自己只有1.16M,很是便於構建小鏡像。
你們在定義Dockerfile時,若是太多的使用RUN指令,常常會致使鏡像有特別多的層,鏡像很臃腫,並且甚至會碰到超出最大層數(127層)限制的問題,遵循 Dockerfile 最佳實踐,咱們應該把多個命令串聯合併爲一個 RUN(經過運算符&&
和/
來實現),每個 RUN 要精心設計,確保安裝構建最後進行清理,這樣才能夠下降鏡像體積,以及最大化的利用構建緩存。
下面是一個優化前Dockerfile:
FROM ubuntu ENV VER 3.0.0 ENV TARBALL http://download.redis.io/releases/redis-$VER.tar.gz # ==> Install curl and helper tools... RUN apt-get update RUN apt-get install -y curl make gcc # ==> Download, compile, and install... RUN curl -L $TARBALL | tar zxv WORKDIR redis-$VER RUN make RUN make install #... # ==> Clean up... WORKDIR / RUN apt-get remove -y --auto-remove curl make gcc RUN apt-get clean RUN rm -rf /var/lib/apt/lists/* /redis-$VER #... CMD ["redis-server"]
構建鏡像,名稱叫 test/test:0.1
。
咱們對Dockerfile作優化,優化後Dockerfile:
FROM ubuntu ENV VER 3.0.0 ENV TARBALL http://download.redis.io/releases/redis-$VER.tar.gz RUN echo "==> Install curl and helper tools..." && \ apt-get update && \ apt-get install -y curl make gcc && \ echo "==> Download, compile, and install..." && \ curl -L $TARBALL | tar zxv && \ cd redis-$VER && \ make && \ make install && \ echo "==> Clean up..." && \ apt-get remove -y --auto-remove curl make gcc && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* /redis-$VER #... CMD ["redis-server"]
構建鏡像,名稱叫 test/test:0.2
。
對比兩個鏡像大小:
root@k8s-master:/tmp/iops# docker images REPOSITORY TAG IMAGE ID CREATED SIZE test/test 0.2 58468c0222ed 2 minutes ago 98.1MB test/test 0.1 e496cf7243f2 6 minutes ago 307MB root@k8s-master:/tmp/iops#
能夠看到,將多條RUN命令串聯起來構建的鏡像大小是每條命令分別RUN的三分之一。
提示:爲了應對鏡像中存在太多鏡像層,Docker 1.13版本之後,提供了一個壓扁鏡像功能,即將 Dockerfile 中全部的操做壓縮爲一層。這個特性還處於實驗階段,Docker默認沒有開啓,若是要開啓,須要在啓動Docker時添加-experimental 選項,並在Docker build 構建鏡像時候添加 --squash 。咱們不推薦使用這個辦法,請在撰寫 Dockerfile 時遵循最佳實踐編寫,不要試圖用這種辦法去壓縮鏡像。
Dockerfile中每條指令都會爲鏡像增長一個鏡像層,而且你須要在移動到下一個鏡像層以前清理不須要的組件。實際上,有一個Dockerfile用於開發(其中包含構建應用程序所需的全部內容)以及一個用於生產的瘦客戶端,它只包含你的應用程序以及運行它所需的內容。這被稱爲「建造者模式」。Docker 17.05.0-ce版本之後支持多階段構建。使用多階段構建,你能夠在Dockerfile中使用多個FROM
語句,每條FROM
指令可使用不一樣的基礎鏡像,這樣您能夠選擇性地將服務組件從一個階段COPY
到另外一個階段,在最終鏡像中只保留須要的內容。
下面是一個使用COPY --from
和 FROM ... AS ...
的Dockerfile:
# Compile FROM golang:1.9.0 WORKDIR /go/src/v9.git...com/.../k8s-monitor COPY . . WORKDIR /go/src/v9.git...com/.../k8s-monitor RUN make build RUN mv k8s-monitor /root # Package # Use scratch image FROM scratch WORKDIR /root/ COPY --from=0 /root . EXPOSE 8080 CMD ["/root/k8s-monitor"]
構建鏡像,你會發現生成的鏡像只有上面COPY
指令指定的內容,鏡像大小隻有2M。這樣在之前使用兩個Dockerfile(一個Dockerfile用於開發和一個用於生產的瘦客戶端),如今使用多階段構建就能夠搞定。
它是如何工做的?第二個FROM指令以alpine:latest image爲基礎開始一個新的構建階段。COPY –from = 0行僅將前一階段的構建文件複製到此新階段。Go SDK和任何中間層都被遺忘,而不是保存在最終image中。
默認狀況下,階段未命名,您能夠經過整數來引用它們,從第0個FROM指令開始。 可是,您能夠經過向FROM指令添加as NAME來命名您的階段。此示例經過命名階段並使用COPY指令中的名稱來改進前一個示例。這意味着即便稍後從新排序Dockerfile中的指令,COPY也不會中斷。
# Compile FROM golang:1.9.0 AS builder WORKDIR /go/src/v9.git...com/.../k8s-monitor COPY . . WORKDIR /go/src/v9.git...com/.../k8s-monitor RUN make build RUN mv k8s-monitor /root # Package # Use scratch image FROM scratch WORKDIR /root/ COPY --from=builder /root . EXPOSE 8080 CMD ["/root/k8s-monitor"]
構建映像時,不必定須要構建整個Dockerfile每一個階段。您能夠指定目標構建階段。如下命令假定您使用的是之前的Dockerfile,但在名爲builder的階段中止:
$ docker build --target builder -t alexellis2/href-counter:latest .
使用此功能可能的一些很是適合的場景是:
調試特定的構建階段
在debug階段,啓用全部調試或工具,而在production階段儘可能精簡
在testing階段,您的應用程序將填充測試數據,但在production階段則使用生產數據
使用多階段構建時,您不只能夠從Dockerfile中建立的鏡像中進行復制。您還可使用COPY –from指令從單獨的image中複製,使用本地image名稱,本地或Docker註冊表中可用的標記或標記ID。若有必要,Docker會提取image並從那裏開始複製。語法是:
COPY --from=nginx:latest /etc/nginx/nginx.conf /nginx.conf
Docker在build鏡像的時候,若是某個命令相關的內容沒有變化,會使用上一次緩存(cache)的文件層,在構建業務鏡像的時候能夠注意下面兩點:
不變或者變化不多的體積較大的依賴庫和常常修改的自有代碼分開;
由於cache緩存在運行Docker build命令的本地機器上,建議固定使用某臺機器來進行Docker build,以便利用cache。
下面是構建Spring Boot應用鏡像的例子,用來講明如何分層。其餘類型的應用,好比Java WAR包,Nodejs的npm 模塊等,能夠採起相似的方式。
一、在Dockerfile所在目錄,解壓縮maven生成的jar包
$ unzip <path-to-app-jar>.jar -d app
二、Dockerfile 咱們把應用的內容分紅4個部分COPY到鏡像裏面:其中前面3個基本不變,第4個是常常變化的自有代碼。最後一行是解壓縮後,啓動spring boot應用的方式。
FROM openjdk:8-jre-alpine LABEL maintainer "opl-xws@xiaomi.com" COPY app/BOOT-INF/lib/ /app/BOOT-INF/lib/ COPY app/org /app/org COPY app/META-INF /app/META-INF COPY app/BOOT-INF/classes /app/BOOT-INF/classes EXPOSE 8080 CMD ["/usr/bin/java", "-cp", "/app", "org.springframework.boot.loader.JarLauncher"]
這樣在構建鏡像時候可大大提升構建速度。
若是在RUN命令中執行apt、apk或者yum類工具,能夠藉助這些工具提供的一些小技巧來減小鏡像層數量及鏡像大小。舉幾個例子:
(1)在執行apt-get install -y
時增長選項— no-install-recommends
,能夠不用安裝建議性(非必須)的依賴,也能夠在執行apk add
時添加選項--no-cache
達到一樣效果;
(2)執行yum install -y
時候, 能夠同時安裝多個工具,好比yum install -y gcc gcc-c++ make ...
。將全部yum install
任務放在一條RUN命令上執行,從而減小鏡像層的數量;
(3)組件的安裝和清理要串聯在一條指令裏面,如 apk --update add php7 && rm -rf /var/cache/apk/*
,由於Dockerfile的每條指令都會產生一個文件層,若是將apk add ...
和 rm -rf ...
命令分開,清理沒法減少apk命令產生的文件層的大小。 Ubuntu或Debian可使用 rm -rf /**var**/lib/apt/lists/*
清理鏡像中緩存文件;CentOS等系統使用yum clean all
命令清理。
Docker 自帶的一些命令還能協助壓縮鏡像,好比 export
和 import
$ docker run -d test/test:0.2 $ docker export 747dc0e72d13 | docker import - test/test:0.3
使用這種方式須要先將容器運行起來,並且這個過程當中會丟失鏡像原有的一些信息,好比:導出端口,環境變量,默認指令。查看這兩個鏡像history信息,以下,能夠看到test/test:0.3
丟失了全部的鏡像層信息:
root@k8s-master:/tmp/iops# docker history test/test:0.3 IMAGE CREATED CREATED BY SIZE COMMENT 6fb3f00b7a72 15 seconds ago 84.7MB Imported from - root@k8s-master:/tmp/iops# docker history test/test:0.2 IMAGE CREATED CREATED BY SIZE COMMENT 58468c0222ed 2 hours ago /bin/sh -c #(nop) CMD ["redis-server"] 0B 1af7ffe3d163 2 hours ago /bin/sh -c echo "==> Install curl and helper… 15.7MB 8bac6e733d54 2 hours ago /bin/sh -c #(nop) ENV TARBALL=http://downlo… 0B 793282f3ef7a 2 hours ago /bin/sh -c #(nop) ENV VER=3.0.0 0B 74f8760a2a8b 8 days ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0B <missing> 8 days ago /bin/sh -c mkdir -p /run/systemd && echo 'do… 7B <missing> 8 days ago /bin/sh -c sed -i 's/^#\s*\(deb.*universe\)$… 2.76kB <missing> 8 days ago /bin/sh -c rm -rf /var/lib/apt/lists/* 0B <missing> 8 days ago /bin/sh -c set -xe && echo '#!/bin/sh' > /… 745B <missing> 8 days ago /bin/sh -c #(nop) ADD file:5fabb77ea8d61e02d… 82.4MB root@k8s-master:/tmp/iops#
社區裏還有不少壓縮工具,好比Docker-squash ,用起來更簡單方便,而且不會丟失原有鏡像的自帶信息,你們有興趣能夠試試。
爲了讓版本管理起來更方便,應用部署速度更快,在建立鏡像的過程當中,建議工程師們明確指定包含版本或者其餘輔助信息的tag。若是不指定鏡像tag,默認會使用latest。每次啓動應用實例時,都須要去鏡像倉庫檢查鏡像是否更新。這種方式不利於版本管理,對應用啓動速度也有必定影響。
使用小容器鏡像的性能和安全優點不言而喻,使用小的基礎鏡像和builder pattern能夠更容易地構建小鏡像,而且還有許多其餘技術可用於單個技術棧和編程語言,以最小化容器體積。 不管你作什麼,你均可以確信你保持容器鏡像最小化的努力是值得的!Docker鏡像的精簡手段和精簡效果值得深刻探討和實踐,但願本文能爲你們帶來幫助。
參考文獻:
1.https://mp.weixin.qq.com/s/T1Rp8x-WWzG9iXqXFp3ADw
2.https://blog.csdn.net/a1010256340/article/details/80092038
3.https://www.docker.com
4.https://wilhelmguo.tk/blog/post/william/Docker構建之多階段構建
5.https://v.qq.com/x/page/t0752jh1emh.html
--- END ---