Docker 实用小技巧:如何在 Dockerfile 快速复制文件,实测 mv 和 cp 性能差异

公众号关注 「奇妙的 Linux 世界」

设为「星标」,每天带你玩转 Linux !

93284b7457f8e437f2cdf657a6b3d773.png

不同于 CentOS、Ubuntu,我们感受到 mv 比 cp 快;在使用 Dockerfile 构建镜像时,使用 Run cp 会比 Run mv 更快。本篇将给出相关的一些测试、验证的数据结果。

1. 测试准备

  • 机器环境

Ubuntu 20.04.1 LTS 32C 125Gi

由于是生产机器,上面会有些负载,因此测试会有偏差。我会多次测试,等结果稳定时取样。

  • 文件结构

ls 

main  lib  i100  cc
  • 文件大小

du -h --max-depth=1

2.7M    ./i100
47M     ./main
808K    ./cc
424M    ./lib
474M    .
  • 文件及目录总数

ls -lR| wc -l

42978

2. 使用 Run mv 命令构建

Dockerfile 内容

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/

构建镜像

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 内容

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/

构建镜像

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 跟踪到的系统部分系统调用。

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
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 挂载了另外一个硬盘

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
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 内容

FROM test:v1

RUN strace mv /a /a1 \
    && strace mv /b /b1 \
    && strace mv /c /c1 \
    && strace mv /d /d1

构建镜像

DOCKER_BUILDKIT=1 docker build --no-cache -t test:v2 -f ./Dockerfile3 . --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 的,rename 为什么会失效?其他工具,例如 Podman、Kaniko 等会不会有类似问题?为什么 Dockerfile 中的 unlink 比主机上的 unlink 操作更费时?

在这个案例下,第二种优化是,使用 Copy、Add 命令替代 Run mv,避免在 Dockerfile 中执行 Run 进行文件的操作;第三种优化是,一个项目很难达到 4w 文件数,可以通过 .dockerignore 忽略用不上的文件传入 context,例如 .git、node_modules、vendor、.m2 等,以减少 unlink 时间。

6. 参考

  • https://unix.stackexchange.com/questions/277412/cp-vs-mv-which-operation-is-more-efficient

本文转载自:「陈少文」,原文:https://url.hi-linux.com/dJTfO,版权归原作者所有。欢迎投稿,投稿邮箱: editor@hi-linux.com。

999f2e80f0e281f0066098e60cc05aa3.gif

最近,我们建立了一个技术交流微信群。目前群里已加入了不少行业内的大神,有兴趣的同学可以加入和我们一起交流技术,在 「奇妙的 Linux 世界」 公众号直接回复 「加群」 邀请你入群。

beca93bec8a9fd422b02a22f4b0f209e.png

你可能还喜欢

点击下方图片即可阅读

1c4b06948b5d8ecd6e5a52ef10f8adfc.png

Podman Desktop: 一款超高颜值和功能强大的 Podman 桌面管理工具

0d541a8f079723832d4aaf246b0957c0.png
点击上方图片,『美团|饿了么』外卖红包天天免费领

64ec89583874fde9ea5e83069f6d9453.png

更多有趣的互联网新鲜事,关注「奇妙的互联网」视频号全了解!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值