惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
WordPress大学
WordPress大学
博客园 - 【当耐特】
Engineering at Meta
Engineering at Meta
IT之家
IT之家
Apple Machine Learning Research
Apple Machine Learning Research
小众软件
小众软件
美团技术团队
S
SegmentFault 最新的问题
The GitHub Blog
The GitHub Blog
Martin Fowler
Martin Fowler
Recorded Future
Recorded Future
H
Help Net Security
aimingoo的专栏
aimingoo的专栏
Y
Y Combinator Blog
博客园_首页
A
About on SuperTechFans
MongoDB | Blog
MongoDB | Blog
阮一峰的网络日志
阮一峰的网络日志
V
Visual Studio Blog
D
DataBreaches.Net
人人都是产品经理
人人都是产品经理
大猫的无限游戏
大猫的无限游戏
B
Blog
The Register - Security
The Register - Security
I
InfoQ
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
雷峰网
雷峰网
Last Week in AI
Last Week in AI
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
月光博客
月光博客
酷 壳 – CoolShell
酷 壳 – CoolShell
Blog — PlanetScale
Blog — PlanetScale
N
Netflix TechBlog - Medium
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
有赞技术团队
有赞技术团队
Stack Overflow Blog
Stack Overflow Blog
Jina AI
Jina AI
Security Archives - TechRepublic
Security Archives - TechRepublic
Hacker News - Newest:
Hacker News - Newest: "LLM"
U
Unit 42
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
W
WeLiveSecurity
Latest news
Latest news
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
博客园 - 叶小钗
L
Lohrmann on Cybersecurity
博客园 - Franky
Recent Commits to openclaw:main
Recent Commits to openclaw:main
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO

陈少文的网站

巨变与机遇的未来十年 Kubernetes 平台管理软件压力测试方案 使用镜像部署 Hexo 静态页面 终于等到你 - GitHub 镜像仓库服务(ghcr.io) 一起来学 Go --(6)Interface 一起来学 Go --(5)Goroutine 和 Channel 什么是函数式编程 如何在 Kubernetes 集群集成 Kata 柯里化与偏函数 使用 PyGithub 自动创建 Label 软件产品是团队能力的输出 Helm 2 、Helm 3 比较 IoT 变现 Kubernetes 中的 DNS 服务 国内的 Helm 镜像源 Harbor 使用自签证书支持 Https 访问 DevOps 工具链之 Prow 如何使用 kfctl 安装 Kubeflow VS Code 无法下载 Go 插件的工具包 工程师更应具有服务精神 你不知道的 Docker 使用技巧 使用 Docker 运行 Tensorflow 论中国 什么是左移 如何清空 Git 仓库全部历史记录 一禅小和尚 有风吹过厨房 时间的玫瑰 如何在 CentOS 安装 GPU 驱动 开发 Tips(19) 使用 Velero 备份 Kubernetes 集群 Kubernetes Cheat Sheet 开发 Tips(18) 如何构建一个 Java 工程 开发 Tips(17) KubeSpray 安装 Kubernetes 报错 ip in ansible_all_ipv4_addresses 基于 Kubernetes 和 Jenkins 搭建自动化测试系统 在 Kubernetes 上动态创建 Jenkins Slave 使用 Jenkins 进行服务拨测 开发 Tips(16) Kubernetes 签发 Ingress 证书及日常故障运维 Kubernetes 中 Deployment 的基本操作 Kubernetes 中的证书 如何使用 KubeBuilder 开发一个 Operator Kubernetes 1.6.0 安装问题汇总 镜像管理工具 -- Harbor 开发 Tips(15) Docker 如何拉取镜像 开发 Tips(14) 使用 Helm 安装 harbor 开发 Tips(13) 使用 S2I 构建云原生应用 在 Kubernetes 中使用 emptyDir、hostPath、localVolume 开发 Tips(12) 开发 Tips(11) 代码质量分析工具 SonarQube 使用 Kubeadm 安装 Kubernetes 集群 一起来学 Go --(4)常用函数 Kubernetes 中的 Ceph Kubernetes 之 Volumes Kubernetes 之 Labels、Selectors 开发 Tips(10) 开源正在重构商业模式 Kubernetes 之网络 Kubernetes 之 API 使用 Helm 和 Operator 快速部署 Prometheus Kubernetes 复杂有状态应用管理框架 -- Operator Kubernetes 的包管理器 -- Helm 一起来学 Go --(3)Go Modules 如何一步一步地优化博客方案 kubectl 实用指南 Kubernetes 中的基本概念 搭建远程 Kubernetes 开发环境 大公司和小公司的 ToB 思路 开发 Tips(9) Go 入门指南 一起来学 Go --(2)数据与逻辑结构 如何预防 Web 富文本中的 XSS 攻击 django-xss-cleaner 云工作时代 一起来学 Go --(1)背景与特点 SaaS 开发团队的不同阶段 你不知道的 Git 使用技巧 输出既服务 微服务设计 继续奔跑 开发 Tips(8) 从账户安全到二次验证 Django 性能之数据库查询优化 Django 性能之分库分表 敏捷开发之研发流程 打造一致性的团队 开发 Tips(7) Pytest 进阶学习之 Mock PaaS 部署之 buildpack Go 开发配置 领域输出才是 PaaS 的核心竞争力 Pytest 入门学习 开发 Tips(6) 如何使用 Jenkins、Docker、GitLab 搭建 Django 自动化部署流程
排查构建镜像时 IO 慢问题
微信公众号 · 2023-02-02 · via 陈少文的网站

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 缓存
  • 清理全部 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 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
  • 测试 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. 参考