1. 遇到的问题
项目介绍:
Dockerfile
1
2
3
|
FROM golang:1.13
COPY ./ /go/src/code
|
构建命令及输入如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
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 编译生成二进制文件
1
|
GOOS=linux GOARCH=amd64 go build -o build main.go
|
2.2 自制二进制提交构建任务
1
2
3
4
5
|
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 优化
1
2
3
4
5
|
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 优化
1
2
3
4
5
|
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 准备测试文件
1
2
3
4
|
du -h --max-depth=1
119M ./data
119M .
|
在 data 目录下放置了一个 119MB 的文件,通过复制该文件不断增加 build context 的大小。
4.2 测试 Dockerfile
1
2
3
|
FROM golang:1.13
COPY ./ /go/src/code
|
4.3 构建命令
1
|
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个
并发量
构建时长
1 |
37.1s |
2 |
46s |
3 |
81s |
通过 iotop
可以实时观测到磁盘写速度,最快能达到 200MB/s,与文件系统 4K 随机写速度最接近。
1
2
|
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/
目录。
1
|
DOCKER_BUILDKIT=1 docker builder prune -f
|
仅当开启 BuildKit 时,才会产生 Build cache。生产环境的缓存大小达到 1.408TB,但比较清理前后,对于新项目的构建并没有发现明显构建速度变化;对于老项目,如果没有变动,命中缓存后速度很快。可能的原因是缓存虽大但条目不多,查询是否有缓存的时间开销很小。
但定期定理缓存,有利于预防磁盘被占满的风险。
清理掉 72h 之前的缓存
1
|
DOCKER_CLI_EXPERIMENTAL=enabled docker buildx prune --filter "until=72h" -f
|
6. 构建不会限制 CPU 但 IO 速度很慢
6.1 测试 CPU 限制
Dockerfile 文件
1
2
3
4
|
FROM ubuntu
RUN apt-get update -y
RUN apt-get install -y stress
RUN stress -c 40
|
1
|
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 文件
1
2
3
4
|
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
|
1
2
3
4
|
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
1
|
docker run -it shaowenchen/demo-fio bash
|
1
2
|
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
1
|
docker run -v /tmp:/tmp -it shaowenchen/demo-fio bash
|
1
2
|
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
1
2
|
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
1
2
3
|
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
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
|
1
2
3
4
|
systemctl daemon-reload
systemctl restart buildkitd
systemctl enable buildkitd
systemctl status buildkitd
|
查看到 buildkitd 正常运行即可。
7.3 测试 buildctl 提交构建
1
2
3
|
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 层。
1
2
3
|
time docker load < /tmp/16GB.tar
real 2m43.288s
|
1
2
3
|
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 存储驱动
1
2
|
Rand_Write_Testing: (groupid=0, jobs=1): err= 0
write: IOPS=40.0k, BW=160MiB/s (168MB/s)(3072MiB/19191msec); 0 zone resets
|
运行容器
1
|
docker run -it shaowenchen/demo-fio bash
|
执行测试
1
|
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
|
1
2
3
4
5
|
docker info
Server Version: 20.10.12
Storage Driver: overlay2
Backing Filesystem: btrfs
|
1
2
|
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
|
1
2
3
4
5
|
docker info
Server Version: 20.10.12
Storage Driver: btrfs
Build Version: Btrfs v5.4.1
|
1
2
|
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/