使用 Buildah 加快容器镜像构建速度

数月前,我曾撰文探讨如何在容器内部加速容器构建过程。该文重点关注容器镜像拉取速度,并介绍了通过主机卷挂载及Buildah的“附加存储”机制预填充镜像存储库的多种方案。Buildah 是一款命令行工具,可快速轻松地构建符合开放容器倡议(即兼容 Docker 和 Kubernetes)的镜像。它易于集成到脚本和构建管道中,最重要的是,构建镜像时无需运行容器守护进程。

数月前,我曾撰文探讨如何在容器内部加速容器构建过程。该文重点关注容器镜像拉取速度,并介绍了通过主机卷挂载及Buildah的“附加存储”机制预填充镜像存储库的多种方案。

Buildah 是一款命令行工具,可快速轻松地构建符合开放容器倡议(即兼容 Docker 和 Kubernetes)的镜像。它易于集成到脚本和构建管道中,最重要的是,构建镜像时无需运行容器守护进程。

本文将探讨在容器内使用 dnf/yum 命令时构建速度的第二个问题。请注意,本文将使用上游名称 dnf(而非某些下游版本使用的 yum)。这些说明同时适用于 dnf 和 yum。

下载速度

你是否注意到,有时当你时隔许久首次运行 dnf -y update或 dnf -y install 时,命令会在开始下载 RPM 包前长时间停滞?这是怎么回事?

dnf 首先会下载庞大的缓存文件。这些文件采用 XML 格式编写,包含远程仓库中的每个软件包,以及大量关于软件包的详细信息。甚至包含软件包内部的所有路径。这些数据对于执行类似 dnf -y install /usr/bin/httpd 的命令至关重要——dnf 正是通过这些信息来确定需要安装的软件包。许多软件包包含类似 (requires: /usr/bin/sendmail) 的指令,正是利用了此特性,使 dnf 能自动获取满足需求的相应软件包。

提取这些庞大的文件 —— 更重要的是处理这些文件 —— 可能需要超过一分钟的时间。这些数据由 libsolv 使用,因此必须转换为 solv 格式,而这个过程相当耗时。当您仅在主机上定期执行此操作时,速度问题并不突出,但在构建容器时,这便成了一个重大问题。

Dockerfile 语法

尽管 Buildah 允许您直接在终端中构建容器镜像,但大多数人仍使用 Dockerfile 和 Containerfile 来构建容器并定义可复现的镜像配方。Buildah 现已默认搜索 Containerfile 和 Dockerfile 文件。由于两者采用相同语法,本文后续将统一使用 Containerfile 进行说明。

在 Containerfile 中常见的操作是使用如下语法:

FROM ubi8  
RUN dnf -y update; dnf -y install nginx; dnf -y clean all  
…  
RUN dnf -y install jboss; dnf -y clean all

让我们来检查一下 dnf 行。第一行 dnf 是:

(dnf -y update; dnf -y install nginx; dnf -y clean all):

  1. 更新容器内的所有软件包。

  2. 安装所选的软件包 nginx 。

  3. 清理所有内容。

由于 ubi8 镜像很可能创建于较早时期,且其 /var/cache/dnf 目录在容器镜像内部可能不存在,dnf 必须先下载 XML 缓存文件并进行处理。随后,dnf 会安装实际软件包,最后通过 dnf -y clean all 命令清除所有冗余数据——这些数据(如日志和缓存文件)是由先前命令写入镜像的。

建议用户运行 clean all 命令以尽可能缩小镜像体积。每次 RUN 命令都会创建新层,即使后续 RUN 命令清空内容,初始层仍会保留全部内容。这意味着所有拉取该镜像的用户最终都会获取日志和缓存文件。若您的容器文件包含一个或多个dnf命令,这种代价将反复累积。不仅如此,每次重建镜像时都需重新付出代价。若在构建服务器上操作,每次构建的容器镜像都会反复下载这些XML文件,造成海量资源与时间的浪费。

带有覆盖挂载的 Buildah

我们注意到上述问题,认为可以采用更优方案处理。我们能否直接将XML数据拉取到主机,在主机上处理后,再通过卷挂载到容器中?或许可以设置一个cron任务或systemd定时器,为每个需要构建容器镜像的操作系统版本执行一次dnf makecache?您可以在主机上每天运行一次或多次该任务,然后让所有容器构建器将相应的缓存卷挂载到buildah容器中。

Buildah支持将主机上的卷挂载到容器目录中。这本应解决问题,实际也确实如此。但问题在于,容器在更新缓存时往往需要向该目录写入数据,因此缓存必须以读写模式挂载到容器中。这便造成了巨大的安全漏洞。试想这样一种情况:恶意容器构建者向缓存写入内容,随后被其他容器构建者读取。这可能诱使第二个容器安装被篡改的软件。我们需要一种方案:内容被挂载到容器中,容器无法写入,但从容器视角看仍可写入。这正是Overlay挂载点的核心原理。

覆盖文件系统将下层目录挂载后,再将上层目录附加到合并后的挂载点。当进程向合并目录中的新文件写入时,新文件会被写入上层目录。当进程修改下层目录中的现有文件时,内核会将文件从下层目录向上层目录复制,并允许进程在上层目录中修改该文件。

我们向 Buildah 引入了 Overlay 挂载的概念。现在您可以通过以下方式运行构建:

buildah bud -v /var/cache/dnf:/var/cache/dnf:O -f /tmp/Containerfile /tmp

容器内的Dnf仍会检查仓库是否有更新内容,若存在则会拉取新内容。但若主机上的内容已为最新版本,则会快速使用主机的缓存。建议您至少每天更新一次主机缓存。

我们为Buildah覆盖挂载新增的一项功能是在每次RUN指令执行后销毁上层目录。回顾示例中,我们使用了多个RUN命令,每个命令都执行了dnf -y clean all。该命令会导致上级目录显示下级目录的所有内容已被删除。若后续dnf命令复用先前创建的上级目录,将误判缓存为空,从而强制重新下载XML数据存储并进行处理。移除上级目录后,每个dnf命令将重新访问主机的下级目录,并继续共享主机缓存。

速度差异

我将创建一个简单的容器文件,其中包含两个 dnf run 命令。

FROM fedora:31  
RUN dnf -y install net-utils; dnf -y clean all  
RUN dnf -y install iputils; dnf -y clean all

在我的 Fedora 31 主机上本地运行这个:

# time -f "Elapsed Time: %E" buildah bud -f Containerfile .  
STEP 1: FROM fedora:31  
STEP 2: RUN dnf -y install procps-ng; dnf -y clean all  
Fedora Modular 31 - x86_64 2.0 MB/s | 5.2 MB 00:02  
Fedora Modular 31 - x86_64 - Updates 1.6 MB/s | 4.0 MB 00:02  
Fedora 31 - x86_64 - Updates 4.2 MB/s | 19 MB 00:04  
Fedora 31 - x86_64 1.8 MB/s | 71 MB 00:39  
Last metadata expiration check: 0:00:01 ago on Wed Feb 5 13:55:54 2020.  
Dependencies resolved.  
================================================================================  
Package Architecture Version Repository Size  
================================================================================  
Installing:  
procps-ng x86_64 3.3.15-6.fc31 fedora 326 k

Transaction Summary  
================================================================================  
Install 1 Package

Total download size: 326 k  
Installed size: 966 k  
Downloading Packages:  
procps-ng-3.3.15-6.fc31.x86_64.rpm 375 kB/s | 326 kB 00:00  
--------------------------------------------------------------------------------  
Total 218 kB/s | 326 kB 00:01  
Running transaction check  
Transaction check succeeded.  
Running transaction test  
Transaction test succeeded.  
Running transaction  
Preparing : 1/1  
Installing : procps-ng-3.3.15-6.fc31.x86_64 1/1  
Running scriptlet: procps-ng-3.3.15-6.fc31.x86_64 1/1  
Verifying : procps-ng-3.3.15-6.fc31.x86_64 1/1

Installed:  
procps-ng-3.3.15-6.fc31.x86_64

Complete!  
33 files removed  
STEP 3: RUN dnf -y install iputils; dnf -y clean all  
Fedora Modular 31 - x86_64 741 kB/s | 5.2 MB 00:07  
Fedora Modular 31 - x86_64 - Updates 928 kB/s | 4.0 MB 00:04  
Fedora 31 - x86_64 - Updates 3.8 MB/s | 19 MB 00:05  
Fedora 31 - x86_64 7.9 MB/s | 71 MB 00:08  
Last metadata expiration check: 0:00:01 ago on Wed Feb 5 13:57:13 2020.  
Dependencies resolved.  
================================================================================  
Package Architecture Version Repository Size  
================================================================================  
Installing:  
iputils x86_64 20190515-3.fc31 fedora 141 k

Transaction Summary  
================================================================================  
Install 1 Package

Total download size: 141 k  
Installed size: 387 k  
Downloading Packages:  
iputils-20190515-3.fc31.x86_64.rpm 252 kB/s | 141 kB 00:00  
--------------------------------------------------------------------------------  
Total 141 kB/s | 141 kB 00:01  
Running transaction check  
Transaction check succeeded.  
Running transaction test  
Transaction test succeeded.  
Running transaction  
Preparing : 1/1  
Installing : iputils-20190515-3.fc31.x86_64 1/1  
Running scriptlet: iputils-20190515-3.fc31.x86_64 1/1  
Verifying : iputils-20190515-3.fc31.x86_64 1/1

Installed:  
iputils-20190515-3.fc31.x86_64

Complete!  
33 files removed  
STEP 4: COMMIT  
Getting image source signatures  
Copying blob ac0b803c5612 skipped: already exists  
Copying blob 922380d685bc done  
Copying config 566e2afbb4 done  
Writing manifest to image destination  
Storing signatures  
566e2afbb417f0119109578a87950250b566a3b4908868627975a4c7428accfb  
566e2afbb417f0119109578a87950250b566a3b4908868627975a4c7428accfb

Elapsed Time: 2:15.00

本次运行耗时2分15秒,成功创建了包含两个新软件包的新容器镜像。

现在让我们尝试使用来自主机的覆盖挂载进行操作。

# dnf -y makecache  
# time -f "Elapsed Time: %E" buildah bud -v /var/cache/dnf:/var/cache/dnf:O -f Containerfile .  
STEP 1: FROM fedora:31  
STEP 2: RUN dnf -y install procps-ng; dnf -y clean all  
Last metadata expiration check: 0:02:34 ago on Wed Feb 5 13:51:54 2020.  
Dependencies resolved.  
================================================================================  
Package Architecture Version Repository Size  
================================================================================  
Installing:  
procps-ng x86_64 3.3.15-6.fc31 fedora 326 k

Transaction Summary  
================================================================================  
Install 1 Package

Total download size: 326 k  
Installed size: 966 k  
Downloading Packages:  
procps-ng-3.3.15-6.fc31.x86_64.rpm 496 kB/s | 326 kB 00:00  
--------------------------------------------------------------------------------  
Total 245 kB/s | 326 kB 00:01  
Running transaction check  
Transaction check succeeded.  
Running transaction test  
Transaction test succeeded.  
Running transaction  
Preparing : 1/1  
Installing : procps-ng-3.3.15-6.fc31.x86_64 1/1  
Running scriptlet: procps-ng-3.3.15-6.fc31.x86_64 1/1  
Verifying : procps-ng-3.3.15-6.fc31.x86_64 1/1

Installed:  
procps-ng-3.3.15-6.fc31.x86_64

Complete!  
285 files removed  
STEP 3: RUN dnf -y install iputils; dnf -y clean all  
Last metadata expiration check: 0:02:41 ago on Wed Feb 5 13:51:54 2020.  
Dependencies resolved.  
================================================================================  
Package Architecture Version Repository Size  
================================================================================  
Installing:  
iputils x86_64 20190515-3.fc31 fedora 141 k

Transaction Summary  
================================================================================  
Install 1 Package

Total download size: 141 k  
Installed size: 387 k  
Downloading Packages:  
iputils-20190515-3.fc31.x86_64.rpm 556 kB/s | 141 kB 00:00  
--------------------------------------------------------------------------------  
Total 222 kB/s | 141 kB 00:00  
Running transaction check  
Transaction check succeeded.  
Running transaction test  
Transaction test succeeded.  
Running transaction  
Preparing : 1/1  
Installing : iputils-20190515-3.fc31.x86_64 1/1  
Running scriptlet: iputils-20190515-3.fc31.x86_64 1/1  
Verifying : iputils-20190515-3.fc31.x86_64 1/1

Installed:  
iputils-20190515-3.fc31.x86_64

Complete!  
285 files removed  
STEP 4: COMMIT  
Getting image source signatures  
Copying blob ac0b803c5612 skipped: already exists  
Copying blob 524bb3b83d61 done  
Copying config 0f82aa6064 done  
Writing manifest to image destination  
Storing signatures  
0f82aa6064814ff3dcb603c34c75e516e00817811681b83b8632f3e9b694e518  
0f82aa6064814ff3dcb603c34c75e516e00817811681b83b8632f3e9b694e518  
Elapsed Time: 0.17.44

借助Overlay挂载技术,我们仅用17秒就完成了包含两个新增软件包的新镜像构建,而传统方式需耗时2分15秒。这意味着构建相同容器镜像的速度提升了近8倍。

这表明,若在预先缓存了dnf元数据的主机操作系统上构建镜像,可大幅提升安装速度。但若您的构建系统需为不同操作系统版本构建镜像呢?例如您既要构建Fedora 30镜像,又要构建Fedora 31镜像。注:此方法同样适用于RHEL8系统,当您需要同时构建RHEL7甚至RHEL6镜像时。

Dnf具备一项实用功能:通过--releasever选项可指定不同版本的内容拉取。此外,您还能使用--setopt=cachedir参数指定缓存目录的替代路径。

在下面的示例中,我将先在主机上卸载两个缓存,然后在命令行模式下使用 Buildah。

# dnf -y makecache --releasever=31 --setopt=cachedir=/var/cache/dnf/31  
# dnf -y makecache --releasever=30 --setopt=cachedir=/var/cache/dnf/30  
# ctr31=$(buildah from fedora:31)  
# time -f 'Elapsed Time: %E' buildah run -v /var/cache/dnf/31:/var/cache/dnf:O ${ctr31} dnf -y install iputils  
Last metadata expiration check: 0:00:15 ago on Wed Feb 5 14:17:41 2020.  
Dependencies resolved.  
================================================================================  
Package Architecture Version Repository Size  
================================================================================  
Installing:  
iputils x86_64 20190515-3.fc31 fedora 141 k

Transaction Summary  
================================================================================  
Install 1 Package

Total download size: 141 k  
Installed size: 387 k  
Downloading Packages:  
iputils-20190515-3.fc31.x86_64.rpm 192 kB/s | 141 kB 00:00  
--------------------------------------------------------------------------------  
Total 107 kB/s | 141 kB 00:01  
Running transaction check  
Transaction check succeeded.  
Running transaction test  
Transaction test succeeded.  
Running transaction  
Preparing : 1/1  
Installing : iputils-20190515-3.fc31.x86_64 1/1  
Running scriptlet: iputils-20190515-3.fc31.x86_64 1/1  
Verifying : iputils-20190515-3.fc31.x86_64 1/1

Installed:  
iputils-20190515-3.fc31.x86_64

Complete!  
Elapsed Time: 0:06.85

# ctr30=$(buildah from fedora:30)  
# time -f 'Elapsed Time: %E' buildah run -v /var/cache/dnf/30:/var/cache/dnf:O ${ctr30} dnf -y install iputils  
Last metadata expiration check: 0:00:15 ago on Wed Feb 5 14:17:47 2020.  
Dependencies resolved.  
================================================================================  
Package Architecture Version Repository Size  
================================================================================  
Installing:  
iputils x86_64 20180629-4.fc30 fedora 123 k

Transaction Summary  
================================================================================  
Install 1 Package

Total download size: 123 k  
Installed size: 351 k  
Downloading Packages:  
iputils-20180629-4.fc30.x86_64.rpm 370 kB/s | 123 kB 00:00  
--------------------------------------------------------------------------------  
Total 138 kB/s | 123 kB 00:00  
Running transaction check  
Transaction check succeeded.  
Running transaction test  
Transaction test succeeded.  
Running transaction  
Preparing : 1/1  
Installing : iputils-20180629-4.fc30.x86_64 1/1  
Running scriptlet: iputils-20180629-4.fc30.x86_64 1/1  
Verifying : iputils-20180629-4.fc30.x86_64 1/1

Installed:  
iputils-20180629-4.fc30.x86_64

Complete!  
Elapsed Time: 0:08.88

如您所见,我们成功在同一构建主机上使用两个不同版本的Fedora的dnf缓存运行了Buildah容器,其中Fedora 31的容器耗时6秒以上,而Fedora 30的构建耗时8秒以上。

注:我选择将缓存文件存放在/var/cache/dnf的子目录中,以确保SELinux标签正确。仅执行 dnf clean all 无法清理 /var/cache/dnf/31 目录。需执行 dnf clean all --setopt=cachedir=/var/cache/dnf/31 才能彻底清除仓库缓存文件,但部分残留物(如 gpg 密钥、空目录)仍会存在。

现在只需测试在未挂载Overlay的情况下,Fedora 31系统构建过程所需耗时。

# ctr31=$(buildah from fedora:31)  
# time -f 'Elapsed Time: %E' buildah run ${ctr31} dnf -y install iputils  
Fedora Modular 31 - x86_64 1.2 MB/s | 5.2 MB 00:04  
Fedora Modular 31 - x86_64 - Updates 875 kB/s | 4.0 MB 00:04  
Fedora 31 - x86_64 - Updates 2.4 MB/s | 19 MB 00:07  
Fedora 31 - x86_64 1.7 MB/s | 71 MB 00:41  
Dependencies resolved.  
================================================================================  
Package Architecture Version Repository Size  
================================================================================  
Installing:  
iputils x86_64 20190515-3.fc31 fedora 141 k

Transaction Summary  
================================================================================  
Install 1 Package

Total download size: 141 k  
Installed size: 387 k  
Downloading Packages:  
iputils-20190515-3.fc31.x86_64.rpm 279 kB/s | 141 kB 00:00  
--------------------------------------------------------------------------------  
Total 129 kB/s | 141 kB 00:01  
Running transaction check  
Transaction check succeeded.  
Running transaction test  
Transaction test succeeded.  
Running transaction  
Preparing : 1/1  
Installing : iputils-20190515-3.fc31.x86_64 1/1  
Running scriptlet: iputils-20190515-3.fc31.x86_64 1/1  
Verifying : iputils-20190515-3.fc31.x86_64 1/1

Installed:  
iputils-20190515-3.fc31.x86_64

Complete!  
Elapsed Time: 1:29.85

在此情况下,运行相同容器耗时近1.5分钟。而使用Overlay挂载的Buildah运行速度提升了14倍。

无root权限容器

此前所有示例均使用Containerfiles以root身份运行构建。但您同样可通过无根容器实现此功能。在接下来的几个示例中,我将让Buildah使用buildah run语法运行容器,以演示缓存的使用。

执行:

$ buildah run -v /var/cache/dnf/30:/var/cache/dnf:O ${ctr30} dnf -y install iputils

运行正常。只要用户能读取/var/cache/dnf/30目录,下级目录即可读取。但必须依赖主机端定期更新缓存。

用户若需要,甚至可使用dnf在个人主目录中创建缓存。

$ dnf -y makecache --releasever=30 --setopt=cachedir=$HOME/dnfcache  
$ chcon --reference /var/cache/dnf -R $HOME/dnfcache  
$ ctr30=$(buildah from fedora:30)  
$ buildah run -v $HOME/dnfcache:/var/cache/dnf:O ${ctr30} dnf -y install iputils

请注意,我不得不更改$HOME/dnfcache目录的SELinux标签,以便SELinux允许容器读取用于Overlay挂载的下层目录。

结论

要加快容器构建速度,需要理解安装软件包时的底层机制。在主机端预缓存dnf数据,并通过Buildah使用Overlay挂载将缓存挂载到容器中,可显著提升构建速度并减少构建集群所需资源。

Buildah以简洁著称,同时具备Overlay挂载和附加存储等强大功能,能有效加速容器镜像构建。

[ 容器新手?下载《容器入门指南》学习Linux容器基础知识。 ]

原文地址:https://www.redhat.com/en/blog/speeding-container-buildah

  

我们一定要给自己提出这样的任务:第一,学习,第二是学习,第三还是学习。 —— 列宁
0 不喜欢
说说我的看法 -
全部评论(
没有评论
关于
本网站专注于 Java、数据库(MySQL、Oracle)、Linux、软件架构及大数据等多领域技术知识分享。涵盖丰富的原创与精选技术文章,助力技术传播与交流。无论是技术新手渴望入门,还是资深开发者寻求进阶,这里都能为您提供深度见解与实用经验,让复杂编码变得轻松易懂,携手共赴技术提升新高度。如有侵权,请来信告知:hxstrive@outlook.com
其他应用
公众号