一年获得资助的FreeBSD
守护进程快报 来自Colin Percival的思考
一年获得资助的FreeBSD 自2010年首次在Amazon EC2平台上启动FreeBSD以来,我一直在维护它。但在2023年11月,我在自己的职责中增加了FreeBSD发行工程负责人的角色——恰好赶上宣布FreeBSD 14.0的可用性,尽管Glen Barber为该版本完成了所有的发行工程工作。虽然我从Antithesis和我的FreeBSD/EC2 Patreon获得少量资助,但我很快发现,我的发行工程职责与我可用的FreeBSD志愿者时间中的FreeBSD/EC2工作形成了竞争——事实上,是胜过了后者:我长长的“待实现功能”列表停滞不前,并且我越来越多地说“嗯,这很奇怪……哦,好吧,现在没时间调查了”。简而言之,到2024年初,我越来越担心自己无法成为一个好的FreeBSD/EC2平台“所有者”。
在此之前几年,我一直在断断续续地与亚马逊员工讨论亚马逊赞助我进行FreeBSD/EC2工作的可能性;相当可预测的是,大多数对话都以我在亚马逊的联系人以类似“亚马逊绝对应该赞助你正在做的工作……但我预算中没有这笔钱”的押韵句子结束。终于在2024年4月,我找到了一位有预算的人,经过一些关于时间线、范围和流程的讨论,决定亚马逊将通过GitHub Sponsors支持我一年。我不完全确定这一年是从6月到次年5月还是从7月到次年6月——资金需要在亚马逊内部流转,从亚马逊到GitHub,从GitHub到Stripe,最后从Stripe到我的银行账户,所以我收到钱的时间不一定反映亚马逊打算给我钱的时间——但无论如何,赞助要么已经结束,要么即将结束,所以我认为现在是写下我所做工作的好时机。
亚马逊名义上赞助我每月40小时,用于FreeBSD发行工程和FreeBSD/EC2开发——我向他们明确表示,只赞助其中之一而忽略另一个是不切实际的,特别是考虑到两者之间的依赖关系——并要求我跟踪我花在各种事情上的时间。最终,我平均每月花费大约50小时在这项工作上,其中平均每月20小时用于解决EC2特定的问题,20小时用于实现FreeBSD发行版,10小时用于其他与发行工程相关的工作——尽管每月的具体细分情况差异很大。
遵循FreeBSD的季度发行计划(我在2024年7月宣布,但在2024年5月的BSDCan FreeBSD开发者峰会上整理并提交),我在过去一年管理了四个FreeBSD发行版:2024年9月的FreeBSD 13.4;2024年12月的FreeBSD 14.2;2025年3月的FreeBSD 13.5;以及目前计划于6月10日发布的FreeBSD 14.3。管理每个发行版所涉及的工作——催促开发人员及时将代码提交到树中、批准(或拒绝!)合并请求、与其他团队协调、构建和测试镜像(通常是三个Beta版、一个候选发布版和最终发布版)、撰写公告文本,并修复在此过程中出现的任何发行版构建故障——主要发生在发行版发布前的那个月(我称之为每个日历年度的第二个月为“Beta月”),耗时从33.5小时(FreeBSD 13.5)到79小时(FreeBSD 14.2)不等。可以想象,你在稳定分支中越靠后,出错的事情就越少,发行版所需的工作量也就越低;虽然我在管理FreeBSD 14.1时没有跟踪时间,但我怀疑它花费了接近100小时的发行工程时间,而FreeBSD 15.0很可能远远超过这个数字。
在FreeBSD/EC2方面,亚马逊鼓励我优先处理两个主要功能:AWS Graviton实例的“电源驱动”(即“EC2 API如何告诉操作系统关闭”——没有这个,FreeBSD会忽略关机信号,几分钟后EC2超时并强行拔掉虚拟电源线),以及AWS Graviton实例上的设备热插拔。第一个功能相对简单:在Graviton系统上,“电源按钮”是一个GPIO引脚,其详细信息通过ACPI _AEI对象指定。我添加了代码在ACPI中找到这些信息,并将适当的配置传递给PL061 GPIO控制器的驱动程序;当GPIO引脚被触发时,控制器会产生一个中断,导致ACPI“电源按钮”事件被触发,进而关闭系统。这里有一个小插曲:EC2提供的ACPI表指定相关的GPIO引脚应配置为“上拉”引脚,但PL061控制器实际上没有任何上拉/下拉电阻;这在Linux上没有引起问题,因为Linux会静默忽略GPIO配置失败,但在FreeBSD上,我们在配置失败后禁用了该设备。我相信这个EC2错误将在未来的Graviton系统中得到修复;但在此期间,我发布的FreeBSD/EC2 AMI包含一个新的“特殊处理”:ACPI_Q_AEI_NOPULL,即“忽略_AEI对象中GPIO引脚规范的上拉标志”。
让热插拔工作——更具体地说,让热拔出工作,因为这是大部分问题出现的地方——需要相当多的工作,主要是因为存在几个不同的问题,每个问题都出现在EC2实例类型的子集上:
- 在某些Graviton系统上,我们在PCI附加期间泄漏了(虚拟)IRQ预留;在大多数情况下这是无害的,但在附加和分离EBS卷67次后,我们将耗尽IRQ,FreeBSD内核会发生恐慌。这种IRQ泄漏发生在一些“旧式”PCI中断路由代码中,最终我直接添加了一个引导加载程序设置,在EC2中关闭该代码。
- 在某些Graviton系统上,固件使用PCI设备电源状态作为操作系统已完成使用设备并准备好“弹出”设备的指示。这100%是EC2的一个错误,我相信它会适时得到修复;在此期间,FreeBSD/EC2 AMI有一个ACPI特殊处理
ACPI_Q_CLEAR_PME_ON_DETACH,指示它们在弹出一个设备之前翻转PCI电源管理寄存器中的一些位。 - 在最新一代的EC2实例(包括x86和Graviton)上,FreeBSD的nvme驱动程序在PCIe拔出后会恐慌。除了向我们的nvme驱动程序维护者指出这个错误外,我不需要自己修复它。
- 在某些EC2实例(包括x86和Graviton)上,我们会在设备被弹出后在PCI总线上看到一个“幽灵”设备;尝试访问该设备会失败,但这个“幽灵”会阻止任何附加新设备的尝试。这原来是另一个EC2错误:管理PCI总线的Nitro固件与管理PCI设备的固件是异步操作的,因此存在一个几毫秒的窗口期,设备已被拔出,但PCI总线仍报告其存在。在Linux上,这(几乎)不是问题,因为Linux会定期扫描总线,并且通常输掉这场“竞赛”;但在FreeBSD上,我们在分离设备后立即重新扫描PCI总线,因此我们通常在与Nitro固件的“竞赛”中获胜,导致我们“看到”已不再存在的设备。据我了解,这正在Nitro中得到修复,以确保PCI总线在向操作系统确认分离之前“知道”设备分离;但在此期间,FreeBSD/EC2 AMI有一个ACPI特殊处理
ACPI_Q_DELAY_BEFORE_EJECT_RESCAN,它增加了一个10毫秒的延迟,在发出弹出设备的信号和重新扫描PCI总线之间。
除了这些功能修复外,我还对FreeBSD的热插拔处理做了一项“生活质量”改进:PCIe(用于最新一代EC2实例)要求在按下“注意”按钮请求弹出设备后,有5秒的延迟——以防站在机器前的人说“哦不,那是错误的磁盘”——如果按钮第二次被按下,弹出请求将被取消。这个延迟在EC2中完全没有意义,因为没有人在物理上按下按钮(也没有机制可以第二次按下虚拟按钮)——所以我添加了一个引导加载程序可调参数来调整该超时,并在EC2中将其设为零。最后,我整理了一个测试脚本:我现在可以启动一个EC2实例,并通过EC2 API反复插拔一个EBS卷,并确认FreeBSD能成功连续附加和分离它300次。幸运的话,这将使我能够确保热插拔在未来的EC2实例类型上完全可用——至少,假设我在它们发布之前能够访问到它们。
虽然这两项是亚马逊对FreeBSD/EC2工作的最高优先级,但它们远不是我唯一的工作内容;事实上,它们只占用了我花费在EC2特定问题上大约一半的时间。我在2021年和2022年做了很多工作来加速FreeBSD的启动过程,但在2023年底和2024年初我注意到的“这很奇怪但我现在没时间调查”的问题中,有一个是FreeBSD/EC2实例有时启动时间长得惊人。请注意,我没有测量过它们花了多长时间;但作为FreeBSD每周快照流程的一部分,我运行了一些EC2实例类型的测试启动,我需要增加了启动实例和尝试通过SSH连接之间的休眠时间。
那么,处理任何性能问题的第一件事就是收集数据;所以我对可追溯到2018年的每周EC2 AMI构建进行了启动时间基准测试——在此过程中启动了超过一万个EC2实例——并开始生成FreeBSD启动性能图。收集新数据并更新这些图现在是我每周快照测试流程的一部分;但即使不绘制图表,我也能立即看到一些问题。我开始着手:
- 从2024年第一周开始,FreeBSD启动过程突然变慢了约3倍。我开始二分查找提交,并追踪到……一个将根磁盘大小从5 GB增加到6 GB的提交。为什么?嗯,我联系了一些在亚马逊的朋友,结果答案介于“魔法”和“你真的不想知道”之间;但对我重要的是,将根磁盘大小增加到8 GB使性能恢复到早期水平。
- FreeBSD在Graviton 2(又名c6g及类似)实例上启动也很慢,经过一些调查,我追踪到这与内核熵种子的一个问题有关:如果FreeBSD内核没有足够的熵来生成安全随机数,启动过程就会停止,直到它收集到更多熵——在虚拟机中,这可能需要一段时间。现在,我们通过EFI引导加载程序获取熵的代码——这实际上意味着要求Nitro固件给我们一个安全的种子——但遇到了两个问题:首先,它在EC2中实际上没有运行;其次,当它运行时,在Graviton 2上慢得离谱。
第一个问题很容易解决:熵种子请求是从“引导菜单”的lua代码中发出的,并且(讽刺的是,为了提高启动性能)我们在EC2中绕过了那个菜单;将其移动到引导加载器lua代码中的正确位置确保了无论菜单是否启用它都会运行。第二个问题原来是Graviton 2的问题:它可以快速提供少量熵,但需要很长时间才能提供FreeBSD引导加载器请求的2048字节。这个巨大的请求是由于FreeBSD为其熵系统播种的方式;32个池每个都需要64字节的熵。由于这在加密上完全没有必要——多个池的存在只是为了防止池状态泄漏——我重写了代码,使其可以从EFI获取64字节,并使用PBKDF2作为“熵扩展器”,将其转换为我们的熵馈送API所需的2048字节。这使FreeBSD arm64/base/UFS的启动时间从约25秒减少到约8秒。
我还注意到ZFS镜像的启动时间比UFS镜像长很多——有趣的是,这个差异取决于磁盘上的数据量(而不是磁盘大小本身)。我追踪到这是由于我们的文件系统构建代码(makefs)和附加ZFS池时发生的事情之间的奇怪交互:ZFS执行一些文件系统验证步骤,涉及遍历最近的事务组,但makefs将所有内容都放入单个事务组——因此当EC2 ZFS镜像启动时,它们必须读取和处理磁盘上每个文件的元数据(因此磁盘上的文件数量影响所花费的时间)。一旦我找到问题,我就能向FreeBSD的makefs专家(Mark Johnston)报告,他通过简单地在文件系统上记录一个更高的事务组解决了这个问题——这样,单个事务组就“不够新”,无法触发ZFS事务组验证逻辑。ZFS镜像的启动时间迅速从约22秒下降到约11秒。
最后——这个问题是我将启动性能纳入每周测试后及时发现的一个——在2024年12月,我更新了net/aws-ec2-imdsv2-get端口以支持IPv6。该端口提供了对EC2实例元数据服务的命令行界面,这是必要的,因为当亚马逊推出“IMDSv2”以掩饰(但未正确修复)通过HTTP暴露IAM凭证固有的安全问题时,他们使得使用FreeBSD fetch(1)访问IMDS变得不可能。不幸的是,当向aws-ec2-imdsv2-get添加IPv6支持时,犯了两个错误:首先,它尝试首先连接IPv6(即使仅IPv4是默认的IMDS实例配置);其次,它保持了默认的TCP超时(75秒)。多亏了我的测试,我及时修复了这个问题,改为首先尝试IPv4,并将超时减少到100毫秒——考虑到IMDS请求的响应甚至不需要离开物理系统,等待超过100毫秒似乎没有必要!
有一件事长期以来一直在我的FreeBSD/EC2“待实现功能”列表上,但我之前没有时间去做,那就是添加更多的AMI“变体”:一年前,我们有base(FreeBSD基础系统,安装了最小限度的额外端口树代码以使其“行为像EC2 AMI”)和cloud-init(顾名思义,是安装了Cloud-init的FreeBSD)。我又向FreeBSD AMI列表添加了两个变体:small AMI,它与base类似,但去掉了调试符号、LLDB调试器、32位库、FreeBSD测试或Amazon SSM代理或AWS CLI——这共同将磁盘空间使用量从约5 GB减少到约1 GB,同时没有移除大多数人会使用的任何东西——以及builder AMI,它们是FreeBSD AMI Builder AMI,为用户创建自定义的FreeBSD AMI提供了一条简单的途径。
当然,有了4种FreeBSD AMI变体——以及两种文件系统(UFS和ZFS)、两种架构(amd64和arm64)和三个版本的FreeBSD(13-STABLE、14-STABLE和15-CURRENT)——所有的每周快照构建开始累积;所以我在5月终于开始清理旧镜像(及其关联的EBS快照)。虽然我不为这些镜像付费——FreeBSD发行工程的AWS账户由亚马逊赞助——但这仍然花费了某人的钱;所以当我意识到我可以删除336 TB的EBS快照时,我觉得花几个小时编写shell脚本是值得的。
虽然我的大部分时间都花在了管理发布周期和维护FreeBSD/EC2平台上,但我也花了一些时间在更广泛的发行工程问题上——事实上,“季度”发布计划的部分设计是,在完成一个版本和开始下一个版本之间留出几周时间,以便进行无法在发布周期中间有效完成的发行工程工作。我在这里处理的第一个问题是并行化发布构建:由于构建大量的EC2 AMI,发布构建的很大一部分时间不是花费在构建上,而是花费在将FreeBSD安装到VM镜像中。我重新设计了发布代码以并行化此过程,但发现它导致了零星的构建失败——这些问题非常难以隔离,因为它们只出现在完整的发布构建中(这花费了近24小时),而不出现在任何构建子集中。经过许多小时的工作,我终于追踪到问题是一个缺失的Makefile行:我们没有指定一个目录应在文件安装到其中之前被创建。修复了这个问题后,我能够将发布构建从约22小时减少到约13小时,并且也“解锁”了添加更多EC2 AMI变体的能力(之前我无法做到这一点,因为这会增加太多构建时间)。
我开始解决的另一个通用发行工程问题是构建可重现性的问题——得益于我可以利用EC2。作为我每周测试快照镜像的一部分,我现在启动EC2实例,并让它们构建自己的AMI——然后使用diffoscope比较它们构建的磁盘镜像与它们启动时所用的镜像。这已经发现了几个问题——包括一些在年中出现并由于定期测试而被迅速识别的问题——其中我已经修复了一些,并将其他一些问题转交给其他开发人员处理。
当然,除了大项目之外,还有大量较小的问题需要处理。构建故障(每周快照构建很擅长发现这个!);审查ENA驱动程序的补丁;帮助Dave Cottlehuber添加构建OCI容器并将其上传到存储库的支持;教我的bsdec2-image-upload工具优雅地处理内部AWS错误;报告我偶然发现的一个AWS安全问题……有些日子里,一切都可以归入“需要完成的其他事情”这把大伞下,但其中很多与大项目同样重要。
那么接下来呢?嗯,我仍然是FreeBSD发行工程负责人和FreeBSD/EC2平台的维护者——只是投入这项工作的时间要少得多。FreeBSD发行版将继续发布——15.0应该会在12月发布,随后在2026年发布14.4、15.1、14.5和15.2——但我可能没有那么多时间跳进去修复问题,因此,较晚提交的功能更有可能从发布中移除,而不是及时修复以便发布;我们之所以能够从FreeBSD 14.2开始发布OCI容器,是因为我有资助的工作时间来确保所有部分完整到位,而这种程度的参与将不再可能。在EC2方面,既然我已经建立了启动性能的回归测试,我可能会捕捉到任何需要在那里修复的问题;但我“待实现功能”列表的其余部分——在EBS卷扩展时自动增长文件系统、使用多个网络接口(和网络接口热插拔)实现更好的自动配置、滚动“预打补丁”的AMI(目前FreeBSD实例在首次启动时自行更新)、整理一个网站以帮助用户生成EC2用户数据文件(例如,用于安装软件包和启动守护进程)、回到我在FreeBSD/Firecracker上的工作并使其成为受支持的FreeBSD平台等等——很可能会停滞,除非我能找到更多时间。
我非常幸运能从亚马逊获得这个赞助;这比大多数开源开发者曾经获得的都要多得多。我希望它没有结束;但我为自己所做的工作感到自豪,我将永远感谢亚马逊给我这个机会。