For the complete documentation index, see llms.txt. This page is also available as Markdown.

反思 FreeBSD.org 软件包

我上次撰写 FreeBSD.org 如何构建和提供软件包是在本刊 2014 年 3/4 月号。自那以后,我们面临许多挑战,做了许多更改,汲取了一些教训,并设想了一种以不同方式做事的未来。我们经验教训的总结是:自动化是关键,更多的 CPU 和 RAM 有助于更快地构建软件包。

构建频率

我们面临的最大挑战之一是构建频率。当有安全更新时,提供软件包的频率至关重要。在过去一年所有的 OpenSSL 漏洞、bash 漏洞、其他一些高知名度的问题中,我们无法及时交付软件包。我们往往很快更新了 Ports,一天内就发布了 freebsd-update 安全公告,但软件包最长需要一周才能跟上。当时,我们只用两台服务器构建 12 套官方 x86 软件包——一台处理 pkg_install 软件包,另一台处理实验性软件包。两台服务器上的 x86 构建通常每台耗时四到五天,这意味着我们只能每周交付一次软件包。Ports 的完整构建可能需要 18 到 31 小时。增量构建(仅重建有变更的部分)可能只需一小时。时间问题的症结在于我们如何做增量构建、如何处理基本系统更新、一些长尾软件包。与增量构建相关的是软件包失败的一个根本问题。当软件包失败时,在修复之前我们根本无法为该运行提供软件包。(我稍后会更详细地讨论这些问题。)对 arm 和 mips 软件包的需求也在增加,提供这些软件包的工作已经成熟到我们希望在官方仓库中提供它们的程度。

构建时间问题过于复杂,无法一夜之间解决。与此同时,我们决定寻找更多硬件以满足需求。移除对 pkg_install 的支持让我们可以将那台服务器重新用于 Pkg x86 构建,并且通过将实验服务器构建的软件包设为默认,我们也消除了对实验服务器的大部分需求。过去一年,FreeBSD 基金会慷慨地为我们提供了四台额外的服务器,每台配备 24 核和 100GB+ 的 RAM。我们为 x86 构建专用了六台服务器,这意味着每台服务器每套只做两次构建。构建的平均时间意味着我们现在通常可以在一天内完成一整套——但由于每套 24 小时的最短时间,我们还不能保证每日构建。截至撰写本文时,我们只安排每隔一天运行构建,以避免系统不同步。我们需要一个构建协调器来处理每次每日构建,这样一旦全部完成,所有构建可以同步推进到下一步。目前每次构建仍是独立的,但同时调度以使用相同的修订版本。构建协调器正在开发中。

新的构建自动化

更多服务器和更高的构建频率带来了许多新挑战。设定了一个新目标:确保我们可以添加任意数量的新服务器,并在初始设置后一切正常运行,另一个目标是允许每次运行之间浮动构建。我们原来的构建脚本基本上只是 crontab 中的 poudriere bulk,加上对 jail 和 Ports 树的更新。很多事情可能出错,也确实出错了,需要人工干预。仅仅添加更多服务器就是一项挑战,尽管我们已经在使用类似 puppet 的系统,但仍需手动设置若干项目,并要 shuffle 软件包、distfile 和链接。我们还有另外八台左右的服务器每天只做测试和 QA 构建。QA 和软件包构建都受益于新的自动化。

第一步是更新配置管理系统,以正确跟踪所有必需的文件并安装它们,使每个系统完全相同。将此与使用 QEMU 的 arm/mips 构建相结合意味着我们不需要为这些系统做手动设置。配置管理和构建脚本现在都能从头正确设置构建所需的一切。在模拟方面,我们还想确保如果将构建器部署到原生 mips 系统,它不会使用 QEMU。Poudriere 和我们的构建脚本都会正确处理这种情况,通过尊重 sysctl kern.supported_archs,并且仅在待构建架构不在列表中时才使用 QEMU。

每次构建时,脚本都会将 jail 更新到它们所跟踪的发布版本的最新版本。对于 head jail,这意味着从 src 构建。如果构建失败,在下一次运行(可能长达一周)之前将不会提供 head 软件包。有了每日构建,我们不希望今天有软件包明天又没有。这会严重影响用户体验。现在如果构建失败,脚本会回滚到之前的 jail 版本。

对于浮动构建的理念,我们希望允许一次构建今天在 serverA 上运行,明天在 serverB 上运行。理由是这让我们管理更少的配置,在移动服务器或添加新服务器时更灵活,并允许构建协调器更大的灵活性。这立即派上用场,因为我们在部署新系统后不久就需要将池中的四台服务器运往全国各地。要允许浮动构建,我们需要在尝试构建之前将 distfile 和之前的软件包集同步到系统中,这样就不会浪费时间重新获取 distfile 或使用正常增量算法重建不需要重建的软件包。在这次改造之前,每个构建服务器都直接从互联网获取 distfile,即使对于多年来未变更的 Ports 也是如此。过去一年我们在 FreeBSD.org 上设置了新的 distfile 缓存。从某种意义上说,这是我们的站点缓存。现在构建会通过 MASTER_SITE_FREEBSD=yes 首先使用此缓存。对于你自己的缓存,你可以使用 MASTER_SITE_OVERRIDE=yoursite.com 来首先尝试从该地址获取,或者直接使用缓存代理。这简化了软件包构建的 distfile 获取,并允许以快速的方式将 distfile 并行播种到新系统中。另一个播种需求是软件包集。现在构建总是会尝试从我们的本地镜像播种软件包,以便从增量重建中受益。

如果某个补丁混入了 Ports SVN 检出,可能会以意想不到的方式影响最终的软件包。这对软件包构建从来不是问题,但对我们的测试构建是个问题,因为我们经常为他人手动应用补丁来测试。在服务器池中,软件包构建可能发生在之前用于测试补丁的系统上。现在脚本确保一切都被还原。

Ports 季度分支的结构使得用于检出的路径每季度都会变化。我们会将 /head 复制到类似 /branches/2015Q2 的位置,然后根据需要从 head cherry-pick 安全和其他关键修复的提交到 2015Q2 分支。然而,在下一个季度,我们会将 head 复制到 2015Q3 并让 2015Q2 废弃。要合并所有 head 以使其替换 2015Q2 中的所有文件并保留分支并不容易。季度分支中的变更可能与 head 不同。直接合并不起作用。我们可以从 head 替换所有文件并提交,但我们暂时选择了更简单的方法。由于“当前”季度分支位置的变动,我们还需要手动 svn switch 软件包构建器的检出。现在这已自动化。(我稍后会更多地讨论季度软件包。)

由于构建频率增加和目前无法为现在失败的软件包保留之前通过的软件包,我们需要确保意外不会清掉一整天的软件包。如果有人更新 gettext 并犯了错误或测试不充分,之前会导致所有直接或间接依赖 gettext 的软件包不被发布,并且在修复并重新构建之前不从仓库提供。我们已实施了一个简单的短路:如果失败的软件包太多,就停止发布。目前这个数字是 1,500 个软件包,占树中 24,000 个 Ports 的 6%。我们讨论过一份阻止者列表,并仍在考虑,但没有实施,因为它可能频繁阻止安全更新。即便是最近,gnome 软件包有很长一段时间是坏的。gnome 坏了是否应该阻止关键的 openssl 更新发布?不应该,但当流行软件包坏了时,确实大大损害用户体验。处理这个问题是一个长期议题,稍后详述。

最后,有了浮动构建,我们可能会让习惯了从同一台服务器查看软件包构建信息的用户困惑。我们有一个处于 beta 阶段的系统,位于 http://pkg-status.FreeBSD.org,它跟踪所有构建并聚合显示,并允许搜索特定软件包以查看之前运行的结果。该站点的代码可在 https://github.com/bdrewery/pkg-status.freebsd.org 获取。出于安全考虑,我们将 Poudriere 的构建时 Web 系统保持为 100% 静态,没有任何服务器端守护进程或每次 web 请求的代码执行。pkg-status 站点通过允许我们为脚本提供查询构建状态的真正 web API,给了我们更大的灵活性。该站点会随时间持续改进。最终我们可能会禁用对构建器的直接访问,让 pkg-status 保留所有软件包的构建日志。这将大大简化用户查找构建结果,并通过禁止访问我们的私有构建器来提高安全性。

增量重建问题

我们目前构建软件包的方式源于历史需要和便利。既然我们已达到能够提供软件包的程度,我们需要做更多研究并重新思考做事方式。下面详细讨论一些问题。需要反馈和帮助。

我们今天面临的大多数增量和构建失败问题源于我将要称之为的“单体式”Ports 和缺乏现代软件包生态系统。Ports 只有作为整个快照才能可靠地工作。失败构建的问题用真实例子最容易理解。假设 Chromium 构建失败。假设它只是因为依赖项变更而重建——Chromium 实际上并未更新,但由于其他原因构建失败。我们有旧的软件包,所以理论上可以恢复它并在仓库中提供。然而,依赖项呢?如果任何库依赖项被更新且 ABI 不兼容,那么恢复的软件包会查找不再提供的旧库。所以我们也必须提供旧的库依赖项软件包。当同一软件包的两个版本在除库之外的所有文件上都冲突时,我们如何提供两个版本?在其他软件包生态系统中,有“alternatives”的概念(不要与 Debian 的具体实现混淆)。这是我们目前缺乏的关键特性。alternatives 将允许同一软件包的多个版本安装到类似 /usr/local/opt/PKGNAME-PKGVERSION 的位置,并允许“默认”项由用户选择并通过 /usr/local 中的符号链接安装。这是自动向同一系统提供两个通常冲突的软件包的唯一方式。

许多人很快就说我们在重新学习其他系统的所有错误,却没看到我们缺少使事情正常运转的关键部分。如果我们有 alternatives 系统,那么理论上我们可以在这里提供之前的库依赖项,并同时提供旧的 Chromium 软件包。然而,在某些情况下,我们需要修改恢复的软件包以更改依赖项,更糟糕的是,我们需要递归地为所有其他失败构建执行同样的恢复。这很容易失控。没有能力同时安装同一软件包/origin 的多个版本,很难测试这些想法并查看真实世界的结果。请注意,我们可以同时安装软件包,但它们必须构建为不冲突。alternatives 是一个迫切需要的系统。解决整个问题的另一种替代方法是类似 PBI 的软件包系统。在 PBI 中,所有依赖项都直接构建到最终的软件包中。所以对于 Chromium,我们确实可以只提供之前的软件包,因为它会有捆绑的依赖项。我对 PBI 不太熟悉,但我的理解是它在安装时智能地去重冗余文件,但仍会有这些大型软件包需要下载和存储。对于 Windows、OSX、Android、iOS 等的软件包,显然是捆绑依赖项的,并且在大多数情况下管理得很好。也许 PBI 和我们做软件包的方式需要重新考虑。

增量重建的另一个问题是我们根本不知道什么需要重建、何时需要。我们过去信任 Ports 提交者通过为他们更新的 Port 所依赖的所有 Ports 提升 PORTREVISION 来“追逐”依赖项更新。然而,假设人工干预会做正确的事并不安全。因此,如果依赖项被更新,Poudriere 会强制重建依赖它的任何东西。如果 Poudriere 有办法知道依赖项对其他 Ports 的构建有实际的构建时或运行时影响,那么它就可以避免强制重建。这种情况通常出现在工具链依赖上,它可能改变软件包二进制文件和库的输出。显然,如果库的主版本号增加,则需要重建。但如果没有提升呢?上游维护者对库版本的操作在 ABI 稳定性方面并不总是正确的。有一些工具叫 ABI 合规检查器,专门用于检查库是否保留了其 ABI。我们可能可以用这些来改进 Poudriere 的增量重建。然而,ABI 合规性之外还有很多不确定性。对 bash 的更新是否需要被视为“工具链”更新?我们如何轻松识别所有“工具链”Ports 而不出错?要求提交者追逐依赖项是不合理的。更新 Ports 的编译器或高度使用的依赖项需要触及多达 24,000 个 Ports。当 Ports 的 GCC 更新时,它从未追逐过其他 Ports。Poudriere 确实会这样做并确保使用新的 GCC。当然,所有这些也大大损害了可重现性。一个软件包的版本今天和明天可能不同,这取决于对它进行的重建。这违背了我们的目标。我们需要在研究增量重建的需求和集成 ABI 合规检查器方面的帮助。缺乏这些,Poudriere 必须在重建时采取积极态度,这浪费了大量时间。

我之前提到了长尾软件包的问题。在典型的软件包构建中,大多数软件包会在 12 小时左右完成,但少数会从第 12 小时继续构建到第 16 小时甚至更久。这些软件包,如 OpenOffice、PyPy 和 Paraview 等,用户恰恰因为它们构建时间长而需要。我们经常在不必要时重建它们。然而,当我们有 24 核可用而 Poudriere 只在为两个 Ports 构建五小时时,有大量资源被浪费。Poudriere 通过 Ports 的 MAKE_JOBS 框架将每次构建限制为 1 个 CPU。当这些长尾软件包构建开始时,系统通常满载。给每个这样的构建分配多个核心有助于加快整体构建,但在其他一切完成之前会很快使系统过载。理想情况下,Poudriere 可以动态地将核心分配给 jails /make 并允许构建上下扩展。我们可能可以稍微玩一下 cpusets,但这需要对这些 Ports 的构建撒谎,告诉它们可以使用 24 核,而实际上我们提供的 cpuset 只有一个核心。我们尚未试验过这个,但似乎可能适得其反。另一个选择是让 Poudriere 更聪明,识别情况并 spawn 下一组来构建。目前这并不简单,因为 Poudriere 是 jail-release-arch-portstree-set 特定的。如果它有一个主队列处理器,我们可以向其传递任意 Port 构建,那么这种情况会更容易处理。这需要保持一些 worker jails 已 spawn 并处于空闲状态。这是合理的,但尚未实现。

软件包重建的最后一个问题是当基本系统更新时。对于 head 来说,这每小时发生一次,重建软件包是合理的。问题在于 SA(安全公告)和 EN(勘误通知)。通常这些会改变基本系统中的某些东西,而这些东西实际上对软件包没有影响。有时确实有影响。这与 ABI 问题非常相似,但范围更大。我们过去将 /usr/include/sys/param.h 中的 __FreeBSD_version 编号视为指示操作系统“ABI”是否改变的标志。然而,它经常被遗漏,通常不会为 SA/EN 修改。所以对于软件包构建,最安全的解决方案是当 jail 更新时就总是重建所有软件包。我确实有一个解决此问题的计划,并打算直接在 Poudriere 中实现。在 freebsdarch@ 邮件列表中有更详细的讨论:https://lists.freebsd.org/pipermail/freebsd-arch/2015-April/017025.html。总结是我们将跟踪操作系统中通常确实会影响软件包的文件,如头文件和工具链。只有当这些文件中任何一个被更新时,我们才会强制重建软件包。同样的逻辑也将扩展到 head 构建,因为对于该分支并不总是需要重建,通常只是内核或文档更新。

稳定软件包

最后一个话题是 stable 分支的理念。在我们提高软件包的构建频率之后,收到的首批反馈之一是现在更新太频繁了。这里没有好的平衡。我们必须能够快速发布安全更新,但我们也不想过于频繁搅动用户的系统。季度分支正是为此而存在,也许应该成为软件包的默认选择。然而,它是稳定软件包的半吊子方案。我们每三个月从 head 分支 100% 更新它。所以你只会真正获得一两个月的稳定性,然后是一阵变更和不稳定。该分支不是从任何稳定点创建的;它只是在时间到时创建。在我看来,如果我们有专人致力于维护长期稳定分支,就像我们对基本系统所做的那样,会更好。对于基本系统,我们将 head 分支锁定一段时间,团队通过时间、测试和评审确保其稳定。然后他们将其复制到新的 /stable 分支以供发布。在基本系统中,我们从不会突然将 /stable/10 变成 /head,就像我们对 Ports 季度分支实际所做的那样。我们让用户为基本系统决定何时更新。Ports 过去有这些锁定时期,但只在发布之前。不知何故,在我们迁移到 SVN 之后,portmgr(负责 Ports 的团队——我是其中一员)采取了错误的分支管理方法,解决它的动力很少。目前我们只为季度分支提供一套变化非常快的软件包,不会带来用户期望的体验。稳定分支需要在我们现在做软件包的方式(新鲜度)和稳定性之间取得平衡。大多数人,包括我在内,反对一个有五年前软件的稳定分支,就像其他一些 OS 发行版那样。我确实认为遵循与基本系统类似的工作流程并做实际的 Port“发布”会是有益的。这涉及大量工作、对 ABI 稳定性的仔细考量和手动回移植补丁。看到的最大问题是提交者缺乏承诺。我们也需要这里的帮助。

BRYAN DREWERY 自 2004 年起使用 FreeBSD 管理共享托管服务。他于 2012 年作为 Ports 提交者加入项目,是 Portmgr 的成员,最近成为 Src 提交者。他是 Portupgrade、Portmaster 的当前上游维护者,也是 Pkg 和 Poudriere 的开发者。在 Portmgr 中,他帮助处理 Ports 框架、管理软件包构建系统、软件包构建、在它们上测试 Ports 补丁。

最后更新于