?1. 遇到的問題
項目介紹:
Dockerfile
FROM golang:1.13
COPY ./ /go/src/code
構建命令及輸入如下:
time DOCKER_BUILDKIT=1 docker build --no-cache -t test:v3 -f Dockerfile . --progress=plain
#1 [internal] load build definition from Dockerfile
#1 sha256:2a154d4ad813d1ef3355d055345ad0e7c5e14923755cea703d980ecc1c576ce7
#1 transferring dockerfile: 37B done
#1 DONE 0.1s
#2 [internal] load .dockerignore
#2 sha256:9598c0ddacf682f2cac2be6caedf6786888ec68f009c197523f8b1c2b5257b34
#2 transferring context: 2B done
#2 DONE 0.2s
#3 [internal] load metadata for golang:1.13
#3 sha256:0c7952f0b4e5d57d371191fa036da65d51f4c4195e1f4e1b080eb561c3930497
#3 DONE 0.0s
#4 [1/2] FROM golang:1.13
#4 sha256:692ef5b58e708635d7cbe3bf133ba934336d80cde9e2fdf24f6d1af56d5469ed
#4 CACHED
#5 [internal] load build context
#5 sha256:f87f36fa1dc9c0557ebc53645f7ffe404ed3cfa3332535260e5a4a1d7285be3c
#5 transferring context: 18.73MB 4.8s
#5 transferring context: 38.21MB 9.8s done
#5 DONE 10.5s
#6 [2/2] COPY ./ /go/src/code
#6 sha256:2c63806741b84767def3d7cebea3872b91d7ef00bd3d524f48976077cce3849a
#6 DONE 26.8s
#7 exporting to image
#7 sha256:e8c613e07b0b7ff33893b694f7759a10d42e180f2b4dc349fb57dc6b71dcab00
#7 exporting layers
#7 exporting layers 67.5s done
#7 writing image sha256:03b278543ab0f920f5af0540d93c5e5340f5e1f0de2d389ec21a2dc82af96754 done
#7 naming to docker.io/library/test:v3 done
#7 DONE 67.6s
real 1m45.411s
user 0m18.374s
sys 0m7.344s
其中比較花時間的是:
- 10s,load build context
- 26s,執行 COPY 操作
- 67s,導出鏡像,鏡像大小 5.79GB
以下也是按照這個思路進行逐一排查,測試驗證,尋找構建時的 IO 瓶頸。
2. 自制 go client 直接提交給 Dockerd 構建效果不佳
工程 https://github.com/shaowenchen/demo/tree/master/buidl-cli 實現的功能就是將本地的 Dockerfile 及上下文提交給 Dockerd 進行構建,從而測試 Docker CLI 是否有提交文件上的瓶頸。
2.1 編譯生成二進制文件
GOOS=linux GOARCH=amd64 go build -o build main.go
2.2 自制二進制提交構建任務
time ./build ./ test:v3
real 5m12.758s
user 0m2.182s
sys 0m14.169s
使用 Go 寫的 cli 工具,將構建上下文提交給 Dockerd 進行構建,時長急劇增加;與此同時,構建機的負載飆升。
也可能還有其他優化點,需要慢慢調試。而 Docker CLI 其實也有相關的參數可以用于減少 IO 占用時間。
3. 構建參數 compress、stream 參數優化效果不佳
compress 會將上下文壓縮為 gzip 格式進行傳輸,而 stream 會以流的形式傳輸上下文。
3.1 使用 compress 優化
time DOCKER_BUILDKIT=1 docker build --no-cache -t test:v3 -f Dockerfile . --compress
real 1m46.117s
user 0m18.551s
sys 0m7.803s
3.2 使用 stream 優化
time DOCKER_BUILDKIT=1 docker build --no-cache -t test:v3 -f Dockerfile . --stream
real 1m51.825s
user 0m19.399s
sys 0m7.657s
這兩個參數對縮短構建時間,并沒有什么效果。但需要注意的是測試項目的文件大而且數量多,如果測試用例發生變化,可能產生不同的效果。接著,我們一起看看文件數量、文件大小對 Dockerd 構建鏡像的影響。
4. 文件數量對 COPY 影響遠不及文件大小
4.1 準備測試文件
du -h --max-depth=1
119M ./data
119M .
在 data 目錄下放置了一個 119MB 的文件,通過復制該文件不斷增加 build context 的大小。
4.2 測試 Dockerfile
FROM golang:1.13
COPY ./ /go/src/code
4.3 構建命令
DOCKER_BUILDKIT=1 docker build --no-cache -t test:v3 -f Dockerfile .
4.4 測試文件大小對 COPY 影響明顯
文件大小
| 構建時長
| 文件個數
|
119M
| 0.3s
| 1個
|
237M
| 0.4s
| 2個
|
355M
| 0.5s
| 3個
|
473M
| 0.6s
| 4個
|
1.3G
| 3.7s
| 11個
|
2.6G
| 9.0s
| 22個
|
文件大小對 COPY 影響明顯,接近線性增長。
4.5 測試文件數量對 COPY 影響甚微
文件大小
| 構建時長
| 文件個數
|
2.9G
| 13.8s
| 264724個
|
5.6G
| 37.1s
| 529341個
|
文件數量對 COPY 影響不大。這是由于在 Docker CLI 將 build context 發送給 Dockerd 時,會對 context 進行 tar 打包,并不是一個一個文件傳輸。
4.6 構建并發數的瓶頸在磁盤IO
5.6G 529341個
通過 iotop 可以實時觀測到磁盤寫速度,最快能達到 200MB/s,與文件系統 4K 隨機寫速度最接近。
Rand_Write_Testing: (groupid=0, jobs=1): err= 0: pid=30436
write: IOPS=37.9k, BW=148MiB/s (155MB/s)(3072MiB/20752msec); 0 zone resets
由于公用一個 Dockerd,并發時 Dockerd 吞吐會有瓶頸,系統磁盤 IO 也會成為瓶頸。
5. 不清理 Buildkit 緩存對新的構建影響甚微
如果提示找不到 docker build?,則需要開啟EXPERIMENTAL? 或者沒有 buildx,需要下載 docker-buildx? 到 /usr/libexec/docker/cli-plugins/ 目錄。
DOCKER_BUILDKIT=1 docker builder prune -f
僅當開啟 BuildKit 時,才會產生 Build cache。生產環境的緩存大小達到 1.408TB,但比較清理前后,對于新項目的構建并沒有發現明顯構建速度變化;對于老項目,如果沒有變動,命中緩存后速度很快。可能的原因是緩存雖大但條目不多,查詢是否有緩存的時間開銷很小。
但定期定理緩存,有利于預防磁盤被占滿的風險。
清理掉 72h 之前的緩存
DOCKER_CLI_EXPERIMENTAL=enabled docker buildx prune --filter "until=72h" -f
6. 構建不會限制 CPU 但 IO 速度很慢
6.1 測試 CPU 限制
Dockerfile 文件
FROM ubuntu
RUN apt-get update -y
RUN apt-get install -y stress
RUN stress -c 40
DOCKER_BUILDKIT=1 docker build --no-cache -t test:v3 -f Dockerfile .
構建機有 40C,構建時機器 CPU 負載能達到 95%,說明構建時,Dockerd 默認不會對 CPU 消耗進行限制。在生產環境下,出現過 npm run build 占用 十幾個 GB 內存的場景,因此我判斷 Dockerd 默認也不會對內存消耗進行限制。
6.2 在 Dockerfile 中測試 IO
Dockerfile 文件
FROM ubuntu
RUN apt-get update -y
RUN apt-get install -y fio
RUN fio -direct=1 -iodepth=128 -rw=randwrite -ioengine=libaio -bs=4k -size=3G -numjobs=1 -runtime=1000 -group_reporting -filename=/tmp/test.file --allow_mounted_write=1 -name=Rand_Write_Testing
DOCKER_BUILDKIT=1 docker build --no-cache -t test:v3 -f Dockerfile .
Rand_Write_Testing: (groupid=0, jobs=1): err= 0
write: IOPS=17.4k, BW=67.9MiB/s (71.2MB/s)(3072MiB/45241msec); 0 zone resets
6.3 在容器中測試 IO
docker run -it shaowenchen/demo-fio bash
Rand_Write_Testing: (groupid=0, jobs=1): err= 0
write: IOPS=17.4k, BW=68.1MiB/s (71.4MB/s)(3072MiB/45091msec); 0 zone resets
6.4 在容器的存儲卷中測試 IO
docker run -v /tmp:/tmp -it shaowenchen/demo-fio bash
Rand_Write_Testing: (groupid=0, jobs=1): err= 0
write: IOPS=39.0k, BW=152MiB/s (160MB/s)(3072MiB/20162msec); 0 zone resets
6.5 在主機上試 IO
Rand_Write_Testing: (groupid=0, jobs=1): err= 0
write: IOPS=38.6k, BW=151MiB/s (158MB/s)(3072MiB/20366msec); 0 zone resets
Dockerd 在構建 Dockerfile 時,遇到 Run 命令會啟動一個容器運行,然后提交鏡像。從測試結果,可以看到 Dockerfile 中的 IO 速度遠達不到主機的,與容器中的 IO 速度一致;主機存儲卷的 IO 速度與主機的 IO 速度一致。
7. 直接使用 buildkitd 構建效果不佳
雖然可以通過 DOCKER_BUILDKIT=1 開啟 Buildkit 構建,但如果直接使用 buildkitd 效果不錯,用于替換 Dockerd 構建也是一個不錯的選擇。
7.1 安裝 buildkit
wget https://github.com/moby/buildkit/releases/download/v0.11.2/buildkit-v0.11.2.linux-amd64.tar.gz
tar xvf buildkit-v0.11.2.linux-amd64.tar.gz
mv bin/* /usr/local/bin/
7.2 部署 buildkitd
cat > /usr/lib/systemd/system/buildkitd.service <<EOF
[Unit]
Description=/usr/local/bin/buildkitd
ConditionPathExists=/usr/local/bin/buildkitd
After=containerd.service
[Service]
Type=simple
ExecStart=/usr/local/bin/buildkitd
User=root
Restart=on-failure
RestartSec=1500ms
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl restart buildkitd
systemctl enable buildkitd
systemctl status buildkitd
查看到 buildkitd 正常運行即可。
7.3 測試 buildctl 提交構建
buildctl build --frontend=dockerfile.v0 --local context=. --local dockerfile=. --no-cache --output type=docker,name=test:v4 | docker load
[+] Building 240.8s (7/7) FINISHED
使用 buildctl 提交給 buildkitd 進行構建,需要的時間更多,達到 4min,較之前增加一倍。
8. 當前存儲驅動下讀寫鏡像有瓶頸
8.1 查看 Dockerd 處理邏輯
在代碼 https://github.com/moby/moby/blob/8d193d81af9cbbe800475d4bb8c529d67a6d8f14/builder/dockerfile/dispatchers.go 可以找到處理 Dockerfile 的邏輯。
1,Add 和 Copy 都是調用 performCopy 函數 2,performCopy 中調用 NewRWLayer() 新建層,調用 exportImage 寫入數據
因此,懷疑的是 Dockerd 寫鏡像層速度慢。
8.2 測試鏡像層寫入速度
準備一個鏡像,大小 16GB,一共 18 層。
time docker load < /tmp/16GB.tar
real 2m43.288s
time docker save 0d08de176b9f > /tmp/16GB.tar
real 2m48.497s
docker load? 和 docker save 速度差不多,對鏡像層的處理速度大約為 100 MB/s。這個速度比磁盤 4K 隨機寫速度少了近 30%。在我看來,如果是個人使用勉強接受;如果用于對外提供構建服務的平臺產品,這塊磁盤顯然是不合適的。
8.3 存儲驅動怎么選
下面是從 https://docs.docker.com/storage/storagedriver/select-storage-driver/ 整理得出的一個比較表格:
存儲驅動
| 文件系統要求
| 高頻寫入性能
| 穩定性
| 其他
|
overlay2
| xfs、ext4
| 差
| 好
| 當前首選
|
fuse-overlayfs
| 無限制
| -
| -
| 適用 rootless 場景
|
btrfs
| btrfs
| 好
| -
| -
|
zfs
| zfs
| 好
| -
| -
|
vfs
| 無限制
| -
| -
| 不建議生產
|
aufs
| xfs、ext4
| -
| 好
| Docker 18.06 及之前版本首選,不維護
|
devicemapper
| direct-lvm
| 好
| 好
| 不維護
|
overlay
| xfs、ext4
| 差,但好于 overlay2
| -
| 不維護
|
排除不維護和非生產適用的,可選項其實沒幾個。正好有一臺機器,前段時間初始化時,將磁盤格式化成 Btrfs 文件格式,可以用于測試。zfs 存儲驅動推薦用于高密度 PaaS 系統。
8.4 測試 Btrfs 存儲驅動
Rand_Write_Testing: (groupid=0, jobs=1): err= 0
write: IOPS=40.0k, BW=160MiB/s (168MB/s)(3072MiB/19191msec); 0 zone resets
運行容器
docker run -it shaowenchen/demo-fio bash
執行測試
fio -direct=1 -iodepth=128 -rw=randwrite -ioengine=libaio -bs=4k -size=3G -numjobs=1 -runtime=1000 -group_reporting -filename=/data/test.file --allow_mounted_write=1 -name=Rand_Write_Testing
docker info
Server Version: 20.10.12
Storage Driver: overlay2
Backing Filesystem: btrfs
Rand_Write_Testing: (groupid=0, jobs=1): err= 0: pid=78: Thu Feb 2 02:41:48 2023
write: IOPS=21.5k, BW=84.1MiB/s (88.2MB/s)(3072MiB/36512msec); 0 zone resets
docker info
Server Version: 20.10.12
Storage Driver: btrfs
Build Version: Btrfs v5.4.1
Rand_Write_Testing: (groupid=0, jobs=1): err= 0
write: IOPS=39.8k, BW=156MiB/s (163MB/s)(3072MiB/19750msec); 0 zone resets
可以明顯看到 btrfs 存儲驅動在速度上優于 overlay2。
9. 總結
本篇主要是記錄在生產環境下碰到的 Dockerfile 構建 IO 慢問題排查過程。
通過設計各種測試案例排查問題,對各個要素進行一一驗證,需要極大耐心,也特別容易走錯方向,得出錯誤結論。
本篇主要觀點如下:
- compress、stream 參數對構建速度不一定有效
- 減少構建上下文大小,有利于緩解構建 IO 壓力
- Buildkit 的緩存可以不用頻繁清理
- 構建 Dockerfile 執行命令時,CPU、Mem 不會受到限制,但 IO 速度慢
- 使用 buildkitd 構建速度不如 Dockerd 開啟 DOCKER_BUILDKIT
- 使用 Btrfs 存儲有利于獲得更好的 IO 速度
但最簡單的還是使用 4K 隨機讀寫快的磁盤,在拿到新的環境用于生產之前,務必先進行測試,僅當滿足需求時,再執行后續計劃。
10. 參考
- https://docs.docker.com/engine/reference/commandline/build/
- https://docs.docker.com/build/install-buildx/
- https://flyer103.com/2022/08/20220806-buildkitd-usage/
- https://pepa.holla.cz/2019/11/18/how-build-own-docker-image-in-golang/