不同于 CentOS、Ubuntu,我们感受到 mv 比 cp 快;在使用 Dockerfile 构建镜像时,使用 Run cp 会比 Run mv 更快。本篇将给出相关的一些测试、验证的数据结果。
1. 测试准备
Ubuntu 20.04.1 LTS
32C
125Gi
由于是生产机器,上面会有些负载,因此测试会有偏差。我会多次测试,等结果稳定时取样。
1
2
3
| ls
main lib i100 cc
|
1
2
3
4
5
6
7
| du -h --max-depth=1
2.7M ./i100
47M ./main
808K ./cc
424M ./lib
474M .
|
1
2
3
| ls -lR| wc -l
42978
|
2. 使用 Run mv 命令构建
Dockerfile 内容
1
2
3
4
5
6
7
| FROM golang:1.13
COPY ./ /go/src/code
RUN mkdir /a && mv /go/src/code/cc/* /a/ \
&& mkdir /b && mv /go/src/code/lib/* /b/ \
&& mkdir /c && mv /go/src/code/i100/resource/* /c/ \
&& mkdir /d && mv /go/src/code/main/* /d/
|
构建镜像
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| DOCKER_BUILDKIT=1 docker build --no-cache -t test:v1 -f ./Dockerfile1 .
[+] Building 78.0s (8/8) FINISHED
=> [internal] load build definition from Dockerfile1 0.0s
=> => transferring dockerfile: 334B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for golang:1.13 0.0s
=> CACHED [1/3] FROM golang:1.13 0.0s
=> [internal] load build context 2.0s
=> => transferring context: 3.05MB 1.9s
=> [2/3] COPY ./ /go/src/code 4.7s
=> [3/3] RUN mkdir /a && mv /go/src/code/cc/* /a/ && mkdir /b && mv /go/src/code/lib/* /b/ && mkdir /c && mv /go/src/code/i100/resource/* /c/ && mkdir /d && mv /go/src/code/main/* /d/ 57.6s
=> exporting to image 13.6s
=> => exporting layers 13.6s
=> => writing image sha256:973b97d407a6403132d279f2c8ac713268ada69fe067e355700efa650ff65d8b 0.0s
=> => naming to docker.io/library/test:v1 0.0s
|
Run mv 使用了 57.6s。
3. 使用 Run cp 命令构建
Dockerfile 内容
1
2
3
4
5
6
7
| FROM golang:1.13
COPY ./ /go/src/code
RUN mkdir /a && cp -R /go/src/code/cc/* /a/ \
&& mkdir /b && cp -R /go/src/code/lib/* /b/ \
&& mkdir /c && cp -R /go/src/code/i100/resource/* /c/ \
&& mkdir /d && cp -R /go/src/code/main/* /d/
|
构建镜像
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| DOCKER_BUILDKIT=1 docker build --no-cache -t test:v1 -f ./Dockerfile2 .
[+] Building 26.2s (8/8) FINISHED
=> [internal] load build definition from Dockerfile2 0.0s
=> => transferring dockerfile: 282B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for golang:1.13 0.0s
=> [internal] load build context 2.0s
=> => transferring context: 3.05MB 2.0s
=> CACHED [1/3] FROM golang:1.13 0.0s
=> [2/3] COPY ./ /go/src/code 5.4s
=> [3/3] RUN cp -R /go/src/code/cc / && cp -R /go/src/code/lib / && cp -R /go/src/code/i100/resource / && cp -R /go/src/code/main / 5.1s
=> exporting to image 13.5s
=> => exporting layers 13.4s
=> => writing image sha256:bd3c53ac40006a79ec009b6112fdcfec85e0adef6d0fcf6aa65d3ee02b2e202a 0.0s
=> => naming to docker.io/library/test:v1 0.0s
|
Run cp 使用了 5.1s。
4. 使用 strace 追踪 mv 和 cp 命令
上面没有给出大量测试之后的统计值,但多次执行能稳定复现,在相同工程下 RUN cp 命令比 RUN mv 命令镜像构建效率高很多。
- Ubuntu 上,如果源文件和目标文件在同一个文件系统上
mv 命令会先尝试调用 rename 快速移动,在失败之后,才会采用 cp 模式的复制。下面是使用 strace 跟踪到的系统部分系统调用。
1
2
3
4
5
6
7
8
| strace cp -R main main1
read(3, "# -*- coding: utf-8 -*-\n\n\"\"\"\n@ve"..., 131072) = 8425
write(4, "# -*- coding: utf-8 -*-\n\n\"\"\"\n@ve"..., 8425) = 8425
mkdir("main1/scripts/update_oauth", 0755) = 0
lstat("main1/scripts/update_oauth", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
openat(AT_FDCWD, "main/scripts/update_oauth", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 3
fstat(3, {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
|
1
2
3
4
5
| strace mv main1 main2
ioctl(0, TCGETS, {B9600 opost isig icanon echo ...}) = 0
renameat2(AT_FDCWD, "main1", AT_FDCWD, "main2", RENAME_NOREPLACE) = 0
lseek(0, 0, SEEK_CUR) = -1 ESPIPE (Illegal seek)
|
可以看到 mv 直接调用了 renameat2,跳过了复制文件的操作。
- Ubuntu 上,如果源文件和目标文件不在同一个文件系统上
此时,mv 命令不仅需要复制文件,还需要 unlink 删除文件,比 cp 多一个步骤。如果文件很多,那么 unlink 将非常耗时。
/data 挂载了另外一个硬盘
1
2
3
4
5
| strace cp -R main /data/main1
lstat("main/upgrade/1.0.1/README.MD", {st_mode=S_IFREG|0644, st_size=1599, ...}) = 0
openat(AT_FDCWD, "main/upgrade/1.0.1/README.MD", O_RDONLY|O_NOFOLLOW) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=1599, ...}) = 0
|
1
2
3
4
5
6
7
| strace mv main /data/main2
renameat2(AT_FDCWD, "main/upgrade/1.0.7/README.MD", AT_FDCWD, "/data/main2/upgrade/1.0.7/README.MD", RENAME_NOREPLACE) = -1 EXDEV (Invalid cross-device link)
lstat("main/upgrade/1.0.7/README.MD", {st_mode=S_IFREG|0644, st_size=1280, ...}) = 0
newfstatat(AT_FDCWD, "/data/main2/upgrade/1.0.7/README.MD", 0x7fff09ac3810, AT_SYMLINK_NOFOLLOW) = -1 ENOENT (No such file or directory)
unlink("/data/main2/upgrade/1.0.7/README.MD") = -1 ENOENT (No such file or directory)
|
mv 尝试 rename,但是失败,只能进入 cp 模式。
- 在 Dockerfile 中的 Run mv 命令
由于上面的基础镜像没有 strace 命令,这里在 test:v1
镜像的基础上,安装 strace 重新提交,具体步骤略过。
Dockerfile 内容
1
2
3
4
5
6
| FROM test:v1
RUN strace mv /a /a1 \
&& strace mv /b /b1 \
&& strace mv /c /c1 \
&& strace mv /d /d1
|
构建镜像
1
2
3
4
5
6
| DOCKER_BUILDKIT=1 docker build --no-cache -t test:v2 -f ./Dockerfile . --progress=plain
#5 0.435 renameat2(AT_FDCWD, "/a", AT_FDCWD, "/a1", RENAME_NOREPLACE) = -1 EXDEV (Invalid cross-device link)
#5 0.436 newfstatat(AT_FDCWD, "/a/CHANGES.txt", {st_mode=S_IFREG|0644, st_size=435, ...}, AT_SYMLINK_NOFOLLOW) = 0
#5 0.436 newfstatat(AT_FDCWD, "/a1/CHANGES.txt", 0x7ffeb191e6d0, AT_SYMLINK_NOFOLLOW) = -1 ENOENT (No such file or directory)
#5 0.436 unlink("/a1/CHANGES.txt") = -1 ENOENT (No such file or directory)
|
Invalid cross-device link
说明,Dockerfile 中的 Run mv 调用 rename 并不能成功。也就是说 Dockerfile 中的 Run mv = Run cp + Run unlink
5. 总结
由于在生产的 CICD 系统中,有些流水线构建时执行 Run mv 很慢,找不到原因。本篇主要是分析这一问题,并给出解,可以通过 Run cp
替代 Run mv
。对于文件数比较大的构建项目,会有显著加速效果。如果文件数较少,可以忽略这一优化。具体结论如下:
- 在同一个文件系统下,mv 比 cp 快
- 在不同文件系统下,cp 比 mv 快
- 在 Dockerfile 中,Run cp 比 mv 快,上面的例子从 57s 降到了 5s
那么问题来了,Docker Daemon 是怎么处理 Dockerfile 的?其他工具,例如 Kaniko 等会不会有类似问题?为什么 Dockerfile 中的 unlink 比主机上的 unlink 操作更费时?
在这个案例下,第二种优化是,使用 Copy、Add 命令替代 Run mv,避免在 Dockerfile 中执行 Run 进行文件的操作;第三种优化是,一个项目很难达到 4w 文件数,可以通过 .dockerignore 忽略用不上的文件传入 context,例如 .git、node_modules、vendor、.m2 等,以减少 unlink 时间。
6. 参考