原文首發於我的部落格: lailin.xyz/post/51252.…
為什麼在儲存如此便宜的今天我們仍然需要對 Docker 映象進行瘦身?
小映象的優點
-
加速構建/部署
雖然儲存資源較為廉價,但是網路 IO 是有限的,在頻寬有限的情況下,部署一個 1G 的映象和 10M 的映象帶來的時間差距可能就是分鐘級和秒級的差距。特別是在出現故障,服務被排程到其他節點時,這個時間尤為寶貴。
-
提高安全性,減少攻擊面積
越小的映象表示無用的程式越少,可以大大的減少被攻擊的目標
-
減少儲存開銷
小映象的製作原則
-
選用最小的基礎映象
-
減少層,去除非必要的檔案
在實際製作映象的過程中,一味的合併層不可取,需要學會充分的利用 Docker 的快取機制,提取公共層,加速構建。
- 依賴檔案和實際的程式碼檔案單獨分層
- 團隊/公司採用公共的基礎映象等
-
使用多階段構建
往往我們在構建階段和實際執行階段需要的依賴環境是不同的,例如
golang
編寫的程式實際執行的時候僅僅需要一個二進位制檔案即可,對於Node
來說,可能最後執行的只是一些打包之後的js
檔案而不需要包含node_modules
裡成千上萬的依賴
基礎映象
-
"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 Linux is a security-oriented, lightweight Linux distribution based on musl libc and busybox.
alpine 是一個基於
musl
,busybox
的安全的linux
發行版。麻雀雖小五臟俱全,雖然不到 10M, 但是包含了一個包管理器和shell
環境,這在我們實際的使用除錯當中將非常有用。但是請注意,由於
alpine
使用了更小的muslc
替代glibc
,會導致某些應用無法使用,需要重新編譯 -
scratch 是空白映象,一般用於基礎映象構建,例如
alpine
映象的dockerfile
便是從scratch
開始的FROM scratch ADD alpine-minirootfs-20190228-x86_64.tar.gz / CMD ["/bin/sh"] 複製程式碼
一般而言,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 映象881MB
而alpine
僅僅只有不到50MB
這個差距就十分的可觀了
ruby latest a5d26127d8d0 4 weeks ago 881MB
ruby alpine 8d8f7d19d1fa 4 weeks ago 47.8MB
ruby slim 58dd4d3c99da 4 weeks ago 125MB
複製程式碼
減少層,去除非必要的檔案
- 刪除檔案不要跨行
# 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 隱藏檔案來實現的,所以在當前層去刪除上一層的檔案,只是會把這個檔案隱藏掉罷了
- 使用單行命令
除了刪除語句需要放在一行以外,由於層的機制,我們安裝依賴的一些公共的語句最好也使用條RUN
命令生成,減少最終的層數
-
分離依賴包,以及原始碼程式,充分利用層的快取
這是一個最佳實踐,在實際的開發過程中,我們的依賴包往往是變動不大的,但是我們正在開發的原始碼的變動是較為頻繁,如果我們實際的程式碼只有
10M
,但是依賴項有1G
, 如果在COPY
的時候直接COPY . .
會導致每次修改程式碼都會時這一層的快取失效,導致浪費複製以及推送到映象倉庫的時間,將 COPY 語句分開,每次 push 就可以只變更我們頻繁修改的程式碼層,而不是連著依賴一起 -
使用
.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"]
複製程式碼
由於本文篇幅較長,這裡不對多階段構建展開講解,詳情可以參考多階段構建
奇淫技巧
-
使用dive檢視 docker 映象的層,可以幫助你分析減少映象體積
-
使用docker-slim 可以自動幫助你減少映象體積,對於 Web 應用較為有用
-
安裝軟體時去除依賴
# ubuntu
apt-get install -y — no-install-recommends
#alpine
apk add --no-cache && apk del build-dependencies
# centos
yum install -y ... && yum clean all
複製程式碼
-
使用
--flatten
引數,減少層(不推薦) -
使用docker-squash壓縮層
不同語言的示例
新增中......
Ruby(Rails)
-
只安裝生產所需的依賴
-
刪除不需要的依賴檔案
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
複製程式碼
- 刪除前端的
node_modules
以及快取檔案
rm -rf node_modules tmp/cache app/assets vendor/assets spec
複製程式碼
上述內容可以結合多階段構建實現
Golang
Golang 在使用多階段構建之後,只剩下了一個二進位制檔案,這時候再要優化,就只有使用upx
之類的工具壓縮二進位制檔案的體積了
參考資料
- Docker 容器映象瘦身的三個小竅門
- 基礎映象 | 再談 Docker 瘦身
- Docker —— 從入門到實踐這是一本很不錯的 Docker 開源書
- Docker 基本原理簡析
- Ruby on Rails — Smaller docker images
License
- 本文作者: mohuishou 1@lailin.xyz
- 本文連結: lailin.xyz/post/51252.…
- 版權宣告: 本部落格所有文章除特別宣告外,均採用 BY-NC-SA 許可協議。轉載請註明出處!