Please enable Javascript to view the contents

排查构建镜像时 IO 慢问题

 ·  ☕ 6 分钟

1. 遇到的问题

项目介绍:

  • 文件大小 5.6 GB
  • 文件数量 529352

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 影响明显

文件大小构建时长文件个数
119M0.3s1个
237M0.4s2个
355M0.5s3个
473M0.6s4个
1.3G3.7s11个
2.6G9.0s22个

文件大小对 COPY 影响明显,接近线性增长。

4.5 测试文件数量对 COPY 影响甚微

文件大小构建时长文件个数
2.9G13.8s264724个
5.6G37.1s529341个

文件数量对 COPY 影响不大。这是由于在 Docker CLI 将 build context 发送给 Dockerd 时,会对 context 进行 tar 打包,并不是一个一个文件传输。

4.6 构建并发数的瓶颈在磁盘 ​IO

5.6G 529341个

并发量构建时长
137.1s
246s
381s

通过 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/ 目录。

  • 查看 build 缓存
1
docker system df  -v
  • 清理全部 build 缓存
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 loaddocker save 速度差不多,对镜像层的处理速度大约为 100 MB/s。这个速度比磁盘 4K 随机写速度少了近 30%。在我看来,如果是个人使用勉强接受;如果用于对外提供构建服务的平台产品,这块磁盘显然是不合适的。

8.3 存储驱动怎么选

下面是从 https://docs.docker.com/storage/storagedriver/select-storage-driver/ 整理得出的一个比较表格:

存储驱动文件系统要求高频写入性能稳定性其他
overlay2xfs、ext4当前首选
fuse-overlayfs无限制--适用 rootless 场景
btrfsbtrfs--
zfszfs--
vfs无限制--不建议生产
aufsxfs、ext4-Docker 18.06 及之前版本首选,不维护
devicemapperdirect-lvm不维护
overlayxfs、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 hubimage/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
  • 测试 overlay2 存储驱动
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
  • 测试 btrfs 存储驱动
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. 参考


微信公众号
作者
微信公众号