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/