Rechunk
Rechunk 是一个用于在分发前重新处理 OSTree OCI 镜像的库
(例如,在刚构建完成后)。
它扁平化文件系统树,从而移除在后续 OCI 层中被替换的文件,然后通过分组算法
将镜像重新划分为一组 N 个大小相等的层。
这些层随后被输入到 ostree-rs-ext,该工具会生成一个新的等效
OSTree 提交,可用于部署或作为基础镜像。
它解决了以下四个关键问题:
- 丢弃未使用的文件并降低镜像大小
- 例如,如果你扩展 Kinoite 并替换内核,则无需分发 作为原始 OSTree 提交一部分的旧内核。
- 避免层变更
- 像 Plasma 这样一起更新的包组拥有自己的层, 并通过时间戳钳制具有相同的哈希值。 如果 KDE 没有更新,用户无需重新下载它。
- 提高下载速度
- 用户下载的是普通的 OSTree 层,而不是“OSTree 自定义层”,后者不需要通过哈希重新提交到 OSTree。
- 更好的更新恢复
- 包分散在 N 个小层中,而不是少数几个大层中。当 恢复下载时,只会丢弃 N 个小层中的一个。
当然,当向注册表上传新镜像时,由于部分层 已经存在,上传速度会更快。
变更日志与标签
作为图像分析的一部分,rechunk 还会记录图像的包版本和
元数据,并允许将它们嵌入到输出标签中。
此外,它提供了一种简单的标记语言,用于自动生成美观的变更日志
(在此查看示例)和标签。
使此操作成为为分发最终确定图像的一站式解决方案。
[!IMPORTANT] 本仓库中的操作使用了高级 podman 功能(例如,mount),并且 需要 root 访问权限(非 rootless 执行)以及 Ubuntu 24.04 runner(截至本文撰写时,而非 latest)。
建议使用 root 通过 buildah 或 podman 构建您的图像(例如,
sudo podman build),以便此操作可以直接挂载它 然后将其删除以节省空间。
结果
实验表明,在 Bazzite 上,由于对 Kinoite 的大量更改,
rechunk 将总图像大小降低了 1GB。
然后,如果用户每周更新一次,下载大小将减少约 40%(从 5GB 到 3GB)。
如果他们每 1-2 天更新一次,下载大小将减少 60-80%(从 5GB 到 1-1.5GB)。
对于连续构建的图像,下载大小降低超过 90%,降至 100MB-300MB,
这是 RPM 数据库(约 110MB)及变更的大小。
时间成本
Rechunk 会为生成 OCI 镜像增加大约 6-10 分钟的处理时间。
其中大约 2 分钟用于准备提交镜像,3 分钟用于 ostree 创建提交,
4 分钟(可通过跳过 gzip 压缩来降低)用于 ostree-rs-ext 生成最终的 gziped oci 目录。
rechunk 本身大约需要 20 秒来
从
ostree 加载信息,以及 10 秒来生成分析。
随后,通过跳过上传大约一半的层(这些层由于 rechunking 已存在于注册表中) 以及测试者需要下载一个 小得多的镜像,大部分时间得以收回。
与 zstd:chunked 的比较
rechunk 在不使用 zstd:chunked 的情况下实现了这一收益,而 zstd:chunked 是一项旨在
仅通过下载更改的文件来实现类似目标的功能。
这是好事,因为 zstd:chunked 尚未得到广泛支持,并且诸如
rpm-ostree 之类的工具可能永远不会支持其节省带宽的方面。
Rechunk 还会通过降低每次更新时必须重新处理的失效层数量, 从而加快 zstd:chunked 镜像的下载速度。 此外,当 zstd-chunked 变得广泛可用时,我们可以在其基础上进行进一步的 优化,例如将新文件重新定位到每个层的末尾, 以便使用更少的 HTTP 范围请求进行下载。
因此,rechunk 和 zstd:chunked 是互补的技术。
算法
1: 预处理
rechunk 通过首先使用 podman 挂载 OCI 镜像来工作。
然后,移除镜像中存在的 OSTree 仓库,并对镜像
进行清理(在权限方面以及通过文件调整)以使其类似于
由 rpm-ostree compose 生成的版本(参见 1_prune.sh;
遇到问题?PR 更改)。
2: 提交
然后,将 podman 挂载作为新的提交提交到 OSTree,由 OSTree 执行 SELinux 重新标记。 之后,修改 OSTree 仓库文件以获取静态时间戳 并避免层哈希变化。 最后,移除 podman 挂载,并且如果需要节省空间, 原始镜像也会被移除。
3: 重新分块准备
重新分块工具基于 OSTree 工作,OSTree 成为关于镜像中哪些文件存在及其大小的唯一事实来源 (这很好,因为我们可以丢弃挂载)。
首先,它构建一个文件映射,将每个文件映射到其 OSTree 哈希和大小。
然后,它从 OSTree 拉取 rpm 数据库并使用 rpm 读取它以获取
现有包及其文件映射。
4: 重新分块分析
利用完整的文件信息,rechunk 计算基础包大小。
然后,rechunk 使用“元”包的概念来将一起更新的包
分组在一起。
这非常简单,因为按报告的版本对包进行分组是微不足道的,
并且可以手动注意到模式(您可以在提供的
meta.yml 中看到它们)。
此外,rechunk 还支持将任意文件作为元包包含在 OCI 镜像中。
这非常有用,因为在 Bazzite 中,我们通常会包含大型压缩文件(例如 Steam 引导程序),而这些文件不属于任何软件包。
在原始的 OSTree 实现中,这些文件会被放置在一个未打包的层中且不会被缓存,这是不理想的。
最后,对于我们没有元包(meta package)的软件包,我们使用其变更日志(changelogs)执行一种启发式算法。 扫描该软件包变更日志的最后一年,并形成一个包含 53 个布尔值的数组,其中每个布尔值代表该软件包在特定一周内是否被更新。 然后,在一个四步过程中,我们执行以下操作:
- 将小型软件包(小于 1 MB)打包到它们自己的层中
- 将中型软件包(小于 5 MB)打包到它们自己的层中
- 对于每个未专门用于元包的层:
- 获取尚未打包的最大软件包以形成层种子
- 直到该层达到预定义的大小(例如,总镜像大小的 40%/N):
- 添加使该层在过去一年的总带宽成本增加最少的软件包
- 复杂度:N^2,其中 N 是软件包数量
- 会剩下一些软件包。对于剩余的软件包:
- 遍历每个层和每个软件包,找到使年度带宽成本增加最少的层与软件包组合
- 重复直到所有软件包都被放置
- 复杂度:M*N^2(M 是层数,N 是软件包数量;非常昂贵,但 N 已大幅减少)
此过程大约需要 30 秒,并生成一个 OSTree 哈希到层的映射。
上述流程仅在首次运行时执行。 在后续运行中,rechunk 从之前的镜像计划(该计划已打包到 之前的镜像清单中)开始,并且仅执行最后一步。 复用之前的计划可以最大限度地减少层偏移,从而降低层失效 以及随后的下载大小。
5: Rechunking
最后,这些信息被放入一个 JSON 文件,并提供给
ostree-rs-ext 的一个分支,该分支已被修改
以遵循以 JSON 文件为输入的自定义 rechunking 计划。
ostree-rs-ext 也被修改以允许自定义标签,这些标签
同时适用于 Docker 和 OCI 约定,允许它们从
github 以及例如 skopeo、rpm-ostree 中读取。
6: Uploading
最终结果是一个 OCI 目录,可以通过
skopeo(单线程)直接推送到注册表,或导入到 podman(占用空间和时间)然后
上传(多线程)。
该镜像也可以进行 zstd:chunked 处理,在这种情况下,此仓库中的操作
包含一个用于跳过 ostree-rs-ext 的 Gzip 压缩的环境变量,
这将处理时间减少约 3 分钟。
镜像之间的差异
此操作接收一个 OCI OSTree 镜像(带有自定义层)并生成一个 修改后的版本。两个镜像之间可能存在漂移,其中一个 镜像可能存在问题,而另一个没有。 即,不应将它们视为完全相同。
重新分块镜像中的问题
OCI 标准在 SELinux 和权限方面表现不佳。为了应对这一点,
rpm-ostree 在部署镜像时复用原始提交中的目录权限,而这些权限在压缩过程中会丢失。
这一点很重要,因为在容器中被触及后,某些目录可能会放宽其
权限或更改所有者。
例如,某些 systemd 目录可能变得可访问,这可能导致
systemd 无法启动,或者 polkit 目录可能不再由 polkitd 拥有
(由于它缺失于 /etc/group),导致 polkits 无法正常工作。
文件 ./1_prune.sh 试图手动缓解权限问题。 如果没有此文件,镜像将无法启动。 肯定还有其他怪癖需要添加到该文件中, 特别是针对未经测试的 DE,例如 Cosmic。
原始镜像中的问题
然而,这并不意味着原始镜像是完美的。
在 OCI 容器中安装的程序会向 /etc/passwd 和
/etc/group 添加条目,而这些条目不会被
rpm-ostree 移动到 /usr/lib/passwd 和 /usr/lib/group。
这对于干净安装有效,但如果用户 rebase 到派生镜像,他们
会遇到用户和组问题。
./1_prune.sh 将这些文件合并到 /usr/lib/passwd
和 /usr/lib/group,这意味着由该操作生成的镜像不会
共享这些问题。
还有其他一些小问题,例如 /var/lib、额外的锁文件,
以及修改过的 /var、/boot 目录,这些可能导致最终镜像中的滞后现象,
并且也由 ./1_prune.sh 处理。
此外,rpm-ostree 在部署镜像时使用原始提交的策略,
导致通过 Docker 安装的新程序(例如 Waydroid)被错误标记,
并且需要变通方法。
作为此操作的一部分,OSTree 使用完整策略执行重新标记, 因此输出镜像不会存在原始镜像中的 SELinux 问题。
TLDR
对于本地快速测试和迭代,使用原始镜像即可。 然而,在进入生产环境之前,请测试、验证并针对 重新分块镜像中的问题创建变通方法。