Dockerfile構建鏡像最佳實踐


  在前文Dockefile及命令詳解中我們已經學習了如何通過Dockerfile構建鏡像以及命令的詳細說明,但是在生產環境或項目使用時如何構建出一個盡可能小的鏡像是一個必須要學會的要點,本文將帶領大家討論如何精簡鏡像以及精簡鏡像帶來的好處。在學習本文前建議大家看下Docker核心技術原理Docker容器和鏡像的區別文章中關於鏡像的分層等知識有基礎的了解。

一、為什么要精簡鏡像

  Docker鏡像由很多鏡像層(Layers)組成(最多127層),鏡像層依賴於一系列的底層技術,比如文件系統(filesystems)、寫時復制(copy-on-write)、聯合掛載(union mounts)等技術,總的來說,Dockerfile中的每條指令都會創建一個鏡像層,繼而會增加整體鏡像的尺寸。

  下面是精簡Docker鏡像尺寸的好處:

  1、減少構建時間

  2、減少磁盤使用量

  3、減少下載時間

  4、因為包含文件少,攻擊面減小,提高了安全性

  5、提高部署速度

二、如何精簡鏡像

2.1 優化基礎鏡像

  優化基礎鏡像的方法就是選用合適的更小的基礎鏡像,常用的 Linux 系統鏡像一般有 Ubuntu、CentOs、Alpine,其中Alpine更推薦使用。大小對比如下:

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個小竅門》

  查看上面的鏡像尺寸對比結果,你會發現最小的鏡像也有4.41M,那么有辦法構建更小的鏡像嗎?答案是肯定的,例如 gcr.io/google_containers/pause-amd64:3.1 鏡像僅有742KB。為什么這個鏡像能這么小?在為大家解密之前,再推薦兩個基礎鏡像:

1、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等提取所有動態庫,然后將二進制文件和依賴動態庫一起打包到鏡像中。

2、busybox鏡像

  scratch是個空鏡像,如果希望鏡像里可以包含一些常用的Linux工具,busybox鏡像是個不錯選擇,鏡像本身只有1.16M,非常便於構建小鏡像。

2.2 串聯 Dockerfile 指令 

  大家在定義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 時遵循最佳實踐編寫,不要試圖用這種辦法去壓縮鏡像。

2.3 多階段構建

使用多階段構建

  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階段則使用生產數據

使用外部鏡像作為stage

  使用多階段構建時,您不僅可以從Dockerfile中創建的鏡像中進行復制。您還可以使用COPY –from指令從單獨的image中復制,使用本地image名稱,本地或Docker注冊表中可用的標記或標記ID。如有必要,Docker會提取image並從那里開始復制。語法是:

COPY --from=nginx:latest /etc/nginx/nginx.conf /nginx.conf

 三、其他優化鏡像構建方法

3.1利用分層機制,減小鏡像傳輸大小

  Docker在build鏡像的時候,如果某個命令相關的內容沒有變化,會使用上一次緩存(cache)的文件層,在構建業務鏡像的時候可以注意下面兩點:

  • 不變或者變化很少的體積較大的依賴庫和經常修改的自有代碼分開;

  • 因為cache緩存在運行Docker build命令的本地機器上,建議固定使用某台機器來進行Docker build,以便利用cache。

  下面是構建Spring Boot應用鏡像的例子,用來說明如何分層。其他類型的應用,比如Java WAR包,Nodejs的npm 模塊等,可以采取類似的方式。

1、在Dockerfile所在目錄,解壓縮maven生成的jar包

$ unzip <path-to-app-jar>.jar -d app

2、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"]

這樣在構建鏡像時候可大大提高構建速度。

3.2RUN命令中執行apt、apk或者yum類工具技巧

  如果在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 命令清理。

3.3壓縮鏡像

  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 ,用起來更簡單方便,並且不會丟失原有鏡像的自帶信息,大家有興趣可以試試。

3.4明確指定鏡像版本,管理更方便

  為了讓版本管理起來更方便,應用部署速度更快,在創建鏡像的過程中,建議工程師們明確指定包含版本或者其他輔助信息的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 ---


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM