y0ngb1n

Aben Blog

欢迎来到我的技术小黑屋ヾ(◍°∇°◍)ノ゙
github

转载 / Docker 鏡像瘦身 & 優化

為什麼在儲存如此便宜的今天我們仍然需要對 Docker 鏡像進行瘦身?

小鏡像的優點#

  1. 加速構建 / 部署
    雖然儲存資源較為廉價,但是網路 IO 是有限的,在頻寬有限的情況下,部署一個 1G 的鏡像和 10M 的鏡像帶來的時間差距可能就是分鐘級和秒級的差距。特別是在出現故障,服務被調度到其他節點時,這個時間尤為寶貴
  2. 提高安全性,減少攻擊面積
    越小的鏡像表示無用的程序越少,可以大大減少被攻擊的目標
  3. 減少儲存開銷

小鏡像的製作原則#

  1. 選用最小的基礎鏡像
  2. 減少層,去除非必要的文件
    在實際製作鏡像的過程中,一味的合併層不可取,需要學會充分利用 Docker 的快取機制,提取公共層,加速構建
    • 依賴文件和實際的代碼文件單獨分層
    • 團隊 / 公司採用公共的基礎鏡像等
  3. 使用多階段構建
    往往我們在構建階段和實際運行階段需要的依賴環境是不同的,例如 Golang 編寫的程序實際運行的時候僅僅需要一個二進制文件即可,對於 Node.js 來說,可能最後運行的只是一些打包之後的 js 文件而不需要包含 node_modules 裡成千上萬的依賴

基礎鏡像#

  • distroless

    "Distroless" images contain only your application and its runtime dependencies. They do not contain package managers, shells or any other programs you would expect to find in a standard Linux distribution.

    distroless 是 Google 推出的一個僅僅包含運行時環境,不包含包管理器,shell 等其他程序。如果你的程序沒有其他依賴的話,這是一個不錯的選擇

  • alpine

    Alpine Linux is a security-oriented, lightweight Linux distribution based on musl libc and busybox.

    alpine 是一個基於 muslbusybox 的安全的 Linux 發行版。麻雀雖小五臟俱全,雖然不到 10M,但包含了一個包管理器和 shell 環境,這在我們實際的使用調試當中將非常有用。但是請注意,由於 alpine 使用了更小的 muslc 替代 glibc,會導致某些應用無法使用,需要重新編譯

  • scratch
    scratch 是空白鏡像,一般用於基礎鏡像構建,例如 alpine 鏡像的 Dockerfile 便是從 scratch 開始的

    FROM scratch
    ADD alpine-minirootfs-20190228-x86_64.tar.gz /
    CMD ["/bin/sh"]
    
  • busybox

一般而言,distroless 相對會更加的安全,但是在實際使用的過程中可能會遇到添加依賴以及調試方面的問題,alpine 更小,自帶包管理器,更加貼合使用習慣,但是 muslc 可能會帶來兼容性問題,一般而言我會選擇 alpine 作為基礎鏡像使用。除此之外,在 Docker Hub 當中我們可以發現常用的 debian 的鏡像也會提供的只包含基礎功能的小鏡像

基礎鏡像對比#

此處直接拉取基礎鏡像,查看鏡像大小,通過觀察我們可以發現, alpine 只有 5M 左右為 debian 的 20 分之一

alpine      latest    5cb3aa00f899        3 weeks ago         5.53MB
debian      latest    0af60a5c6dd0        3 weeks ago         101MB
ubuntu      18.04     47b19964fb50        7 weeks ago         88.1MB
ubuntu      latest    47b19964fb50        7 weeks ago         88.1MB
alpine      3.8       3f53bb00af94        3 months ago        4.41MB

似乎從上面看,感覺差距不大,實踐中,不同語言的基礎鏡像都會提供一些採用不同基礎鏡像製作的 tag,下面我們以 ruby 的鏡像為例,查看不同基礎鏡像的差異。可以看到默認的 latest 鏡像 881MBalpine 僅僅只有不到 50MB 這個差距就十分的可觀了

ruby   latest   a5d26127d8d0        4 weeks ago         881MB
ruby   alpine   8d8f7d19d1fa        4 weeks ago         47.8MB
ruby   slim     58dd4d3c99da        4 weeks ago         125MB

減少層,去除非必要的文件#

  1. 刪除文件不要跨行

    # dockerfile 1
    FROM alpine
    RUN wget https://github.com/mohuishou/scuplus-wechat/archive/1.0.0.zip
    
    # dockerfile 2
    FROM alpine
    RUN wget https://github.com/mohuishou/scuplus-wechat/archive/1.0.0.zip
    RUN rm 1.0.0.zip
    
    # dockerfile 3
    FROM alpine
    RUN wget https://github.com/mohuishou/scuplus-wechat/archive/1.0.0.zip && rm 1.0.0.zip
    
    test   3  351a80e99c22        5 seconds ago        5.53MB
    test   2  ad27e625b8e5        49 seconds ago       6.1MB
    test   1  165e2e0df1d3        About a minute ago   6.1MB
    

    可以發現 1,2 兩個大小一樣,但是 3 小了 0.5MB,這是因為 docker 幾乎每一行命令都會生成一個層,刪除文件的時候:因為底下各層都是只讀的,當需要刪除這些層中的文件時,AUFS 使用 whiteout 機制,它的實現是通過在上層的可寫的目錄下建立對應的 whiteout 隱藏文件來實現的,所以在當前層去刪除上一層的文件,只是會把這個文件隱藏掉罷了

  2. 使用單行命令
    除了刪除語句需要放在一行以外,由於層的機制,我們安裝依賴的一些公共的語句最好也使用條 RUN 命令生成,減少最終的層數

  3. 分離依賴包,以及源代碼程序,充分利用層的快取
    這是一個最佳實踐,在實際的開發過程中,我們的依賴包往往是變動不大的,但是我們正在開發的源碼的變動是較為頻繁,如果我們實際的代碼只有 10M,但是依賴項有 1G,如果在 COPY 的時候直接 COPY ... 會導致每次修改代碼都會使這一層的快取失效,導致浪費複製以及推送到鏡像倉庫的時間,將 COPY 語句分開,每次 push 就可以只變更我們頻繁修改的代碼層,而不是連著依賴一起

  4. 使用 .dockerignore
    在使用 Git 時,我們可以通過 .gitignore 忽略文件,在 docker build 的時候也可以使用 .dockerignore 在 Docker 上下文中忽略文件,這樣不僅可以減少一些非必要文件的導入,也可以提高安全性,避免將一些配置文件打包到鏡像中

多階段構建#

多階段構建其實也是減少層的一種,通過多階段構建,最終鏡像可以僅包含最後生成的可執行文件,和必須的運行時依賴,大大減少鏡像體積

GO 語言為例,實際運行的過程中只需要最後編譯生成的二進制文件即可,而 GO 語言本身以及擴展包,代碼文件都是不必要的,但是我們在編譯的時候這些依賴又是必須的,這時候就可以使用多階段構建的方式,減少最終生成的鏡像體積

# 使用 golang 鏡像作為 builder 鏡像
FROM golang:1.12 as builder
WORKDIR /go/src/github.com/go/helloworld/
COPY app.go .
RUN go build -o app .

# 編譯完成之後使用 alpine 鏡像作為最終的基礎鏡像
FROM alpine:latest as prod
RUN apk --no-cache add ca-certificates
WORKDIR /root/

# 從 builder 中複製編譯好的二進制文件
COPY --from=builder /go/src/github.com/go/helloworld/app .
CMD ["./app"]

由於本文篇幅較長,這裡不對多階段構建展開講解,詳情可以參考:多階段構建

奇淫技巧#

  1. 使用 dive 查看 Docker 鏡像的層,可以幫助你分析減少鏡像體積

  2. 使用 docker-slim 可以自動幫助你減少鏡像體積,對於 Web 應用較為有用

  3. 安裝軟體時去除依賴

    # ubuntu
    apt-get install -y — no-install-recommends
    
    # alpine
    apk add --no-cache &&  apk del build-dependencies
    
    # centos
    yum install -y ... && yum clean all
    
  4. 使用 --flatten 參數,減少層(不推薦)

  5. 使用 docker-squash 壓縮層

不同語言的示例#

Ruby(Rails)#

  1. 只安裝生產所需的依賴

  2. 刪除不需要的依賴文件

    bundle install --without development:test:assets -j4 --retry 3 --path=vendor/bundle \
        # Remove unneeded files (cached *.gem, *.o, *.c)
        && rm -rf vendor/bundle/ruby/2.5.0/cache/*.gem \
        && find vendor/bundle/ruby/2.5.0/gems/ -name "*.c" -delete \
        && find vendor/bundle/ruby/2.5.0/gems/ -name "*.o" -delete
    
  3. 刪除前端的 node_modules 以及快取文件

    rm -rf node_modules tmp/cache app/assets vendor/assets spec
    

上述內容可以結合多階段構建實現

Golang#

Golang 在使用多階段構建之後,只剩下了一個二進制文件,這時候再要優化,就只有使用 upx 之類的工具壓縮二進制文件的體積了

參考資料#


本文作者:mohuishou
原文鏈接:https://lailin.xyz/post/51252.html
版權聲明:版權歸作者所有,转载请注明出处!

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。