这是我们关于某海外大型音频平台公司如何进行组件集群管理、以及如何大规模管理软件系统的系列文章第三部分。你也可以继续阅读本系列的第一部分和第二部分。
在本系列第三篇文章中,我们将介绍一种被称为“全代码库重构”的实践:也就是在数千个 Git 仓库中批量修改和重构代码。我们会分享,为了让这类大规模代码重构和自动化代码变更成为日常工程实践,我们构建了哪些工具;这些工具是如何落地的;以及在这个过程中,我们学到了哪些经验。

为什么多仓库架构需要全代码库重构?
正如本系列第一部分所述,这家公司的代码分散在数千个 Git 仓库中。整体上,我们采用多仓库架构:每个仓库通常包含一个,或少数几个相关的软件组件。这些组件构成了我们在生产环境中运行的数千个服务和任务。桌面端和移动端客户端代码库则采用另一种架构,主要使用单体仓库。
这种架构最初的设计目标,是让每个团队都能对自己的组件拥有较强自主权,并能够根据自身需求独立开发和部署。
多仓库架构确实有利于团队自治,但许多仓库中的源代码也依赖其他仓库中的库和框架。例如,我们的大部分后端服务都构建在内部服务框架和核心库之上,大多数批处理数据管道也依赖统一的数据处理框架。
如果框架或核心库每次发生变更、发布新版本或修复 bug 时,都需要手动更新所有相关仓库,就会消耗大量工程精力。而且在过去,这类更新也很难快速完成。正如前文所述,一个后端服务框架的新版本,通常需要 6 个月以上,才能在 70% 以上的后端服务中得到采用。
随着生产环境中的组件数量持续增长,并且增长速度超过工程师数量,我们越来越难推进那些涉及全部或大部分组件的变更。
为了将开发实践转向“组件集群优先”,我们在 2021 年决定投入资源,建设能够支持全代码库重构的工具和基础设施。我们的愿景是:工程师应该能够一次性修改所有相关组件,而不是一个仓库一个仓库地逐个调整。
例如,如果一个团队开发了供其他团队使用的库或框架,就不应该认为“发布新版本并宣布可用”就是工作的终点。只有当他们帮助公司内部所有相关代码都升级到新版本之后,这项工作才算真正完成。
同样,平台团队也不应该只是发送邮件,要求其他团队在某个截止日期前完成代码修改。无论一次变更影响多少代码库,也无论这些代码库归谁所有,平台团队都应该主动承担起在整个代码库范围内推进变更的责任。任何发现性能优化机会的人,也都应该有能力在所有适用位置完成优化。
为了让这类变更变得简单、普遍,同时又保留数千个代码库带来的自治优势,我们需要消除“代码库”作为障碍或工作单元所造成的限制,并鼓励工程师停止以“我的代码”和“你的代码”来思考问题。
这项工作主要分为两个方向:
- 通过自动化方式管理大多数组件的依赖关系,让工程师能够轻松更新整个组件集群中使用的特定库版本。
- 让工程师能够轻松向成百上千个代码库发送拉取请求,并管理后续合并流程。
自动化依赖管理:统一组件版本与提升安全修复效率
当我们决定推进全代码库重构时,已经在基于 Maven 的后端服务和库中使用 BOM,也就是物料清单,大约一年时间。
BOM 可以在一个构件中指定多个依赖构件需要导入的版本。我们创建这个 BOM,也就是内部称为 Java BOM 的机制,是为了降低后端服务中升级库版本的复杂度。因为我们有大量相互依赖的库,例如通用工具库、云服务相关库、远程调用库,以及自己的内部框架等。
通过创建一个能够集中控制多个库版本的构件,我们只需要修改 pom.xml 文件中的一行代码,就可以让各个仓库升级到更新的依赖版本。
用 BOM 控制每个组件中常用依赖的具体版本,还有另一个好处:我们可以集中验证哪些库版本彼此兼容,从而避免工程师在多个组件和团队中重复做同样的兼容性验证。
与依赖自动更新工具相比,使用集中式依赖管理清单,例如我们的 Java BOM,可以让我们一次性无缝升级多个库,同时确保它们相互兼容。每当公司内部有人提出更新 BOM,例如升级某个库的版本或添加新的库,我们都会运行一系列测试,检查所有库之间的兼容性。
在全公司范围重构工作中,我们做出的首要决策之一,就是加快 Java BOM 在 Maven 后端服务中的采用。我们不再让团队自行决定是否使用 BOM,而是自动将所有后端组件纳入 BOM,并为每个代码仓库生成拉取请求。
此外,我们还决定:只要所有测试都通过,就自动将这些拉取请求合并到对应代码仓库中。关于这一点,后文“将自动化代码变更部署到生产环境”部分会详细介绍。
目前,我们的生产后端服务中 Java BOM 的采用率非常高:96% 的生产后端服务都在使用 Java BOM,其中 80% 的服务所使用的 BOM 版本距离最新版本不超过 7 天。这个水平已经保持了 18 个月。
有了如此高的采用率,工程师只需要向 BOM 仓库提交一个拉取请求,就可以更新某个依赖的版本,并且有信心相信这次更新会在接下来几天内自动部署到几乎所有后端服务中。
这为那些维护被其他团队依赖的库的团队,提供了一种非常便捷的分发机制,前提是这些变更保持向后兼容。
由于后端组件数量庞大,在微服务架构中可能达到数千个,我们首先重点实现了后端组件的自动化依赖管理。之后,我们又对批处理数据管道进行了类似的自动化管理。
目前,97% 的批处理数据管道都使用一个 sbt 插件来管理管道代码所使用的大部分库版本。这个插件的作用类似于 BOM。平均来看,70% 的管道所使用的组件版本距离最新版本不超过 30 天。
接下来,我们计划将这种“BOM 化”的方法扩展到基于 Node 的 Web 组件,包括内部和外部网站,以及 Python 组件,从而集中管理并更好地控制所有依赖项版本。
能够轻松管理和更新软件组件集群所使用的依赖项,带来了巨大的安全收益。正如第一部分所述,在 2021 年底某个知名日志组件漏洞被发现时,我们 80% 的生产后端服务在 9 小时内就完成了补丁修复。
让大多数组件使用相同版本的常用依赖,还有另一个好处:当大部分组件使用相同版本的依赖时,对整个组件集群进行代码重构就会变得容易得多。
Fleetshift:批量修改多个代码仓库的自动化平台
我们用于自动化处理全局代码变更的核心引擎,是一个名为 Fleetshift 的内部工具。
Fleetshift 最初是一位工程师创建的临时项目,目的是取代此前一系列用于批量生成拉取请求的内部脚本。后来,它被全局重构计划采纳,并逐步成为类似 BOM 的标准化工具。
我们将 Fleetshift 视为一个平台。Fleetshift 和其他重构工具由一个专门团队维护,这个团队的职责是简化全平台变更流程;其他团队则使用这些工具,真正执行各自负责的全平台代码变更。
这种所有权和责任的分离,对于扩大自动化全平台变更的数量,以及在全公司范围内推动“组件集群优先”的理念至关重要。
在公司内部,任何负责开发平台、库、SDK、API 等能力的团队,通常也最适合推进并自动化这些变更在整个组件集群中的落地。我们并不希望由一个单一的“重构团队”负责推广其他团队的变更。那样会造成瓶颈,也会限制我们实际能够完成的全平台变更数量。
Fleetshift 的工作方式,是对每个目标代码仓库运行一个容器镜像来完成代码变更。
想要进行全局代码变更的工程师,需要提供一个能够执行代码重构的容器镜像。这个镜像可以使用现成工具,例如 sed、grep 等,也可以运行自定义代码。
当重构逻辑准备好后,容器镜像会交给 Fleetshift,由它在每个目标代码仓库上执行。Fleetshift 会克隆每个代码仓库,对检出的代码运行容器镜像,记录所有被修改、添加或删除的文件,为变更创建 Git 提交,然后向原始代码仓库发起拉取请求。
变更发起者需要在变更配置中指定提交消息、拉取请求标题和描述,方便自动化变更的接收者理解变更内容和原因。变更发起者也可以在内部开发者门户上跟踪变更进度和其他详细信息。

Fleetshift 的一个创新点,是利用我们现有的大规模容器编排基础设施,来完成克隆代码库、执行代码转换以及将变更推回代码托管平台的工作。这套基础设施包括数十个集群和数千个节点。
过去,类似的内部工具通常要求工程师在自己的笔记本电脑上完成所有工作。这让任何想尝试大规模代码变更的工程师都面临很高门槛。
Fleetshift 则将代码转换作为容器编排作业运行,利用容器编排平台天然的并行能力和自动扩缩容能力,让工程师可以轻松启动针对 10 个,甚至 1000 个代码库的代码转换任务。事实上,这些代码转换或重构作业,是运行在支撑后端服务的同一套计算基础设施的剩余资源上。

Fleetshift 的另一个创新点,是将作者想进行的代码转换建模为容器镜像。只要你能写出实现变更的代码,并把它打包进容器镜像,Fleetshift 就可以支持这种代码变更。
目前,我们用于支持全代码库重构的工具种类繁多。从执行相对简单变更的 shell 脚本,到进行基于抽象语法树(AST)的复杂重构,都可以通过 Fleetshift 运行。
Fleetshift 的任务可以只运行一次,也可以配置为每天,也就是每个工作日,定期运行。周期性任务适用于持续性变更,例如每天更新每个后端组件中的 Java BOM 版本,或者每天将所有内部基础容器镜像的使用者更新到最新版本。
在我们的实现中,Shift 被建模为自定义容器编排资源,而 Fleetshift 本身是一个基于声明式基础设施工具构建的 Operator。Shift 资源以 YAML 文件形式存储在代码仓库中,就像其他代码一样。因此,启动或迭代一个 Shift,也意味着需要提交拉取请求、进行代码审查等常规工程流程。apiVersion: fleetshift.example.com/v1 kind: Shift metadata: name: update-foobar-sdk-to-v2 namespace: fleetshift spec: container: image: example-shift:1.2.3 pullRequest: title: update foobar-sdk to v2 commitMessage: update foobar-sdk to v2 description: | The PR updates the foobar-sdk from v1 to v2 and refactors code calling now-removed methods in v2 to use their counterparts instead. You can see a full changelog [here](link). If you have any questions please reach out to #foobar-team-channel targetRepos: [ … ]
[示例:一个 Shift 资源。]
为了找到需要运行 Shift 的仓库,作者可以使用代码托管平台的搜索功能做基础查询,也可以使用内部数据仓库进行更复杂的搜索。
我们每天都会将所有源代码从代码托管平台导入数据仓库,以便与其他数据集进行搜索和关联。例如,我们可以查找所有调用某个特定方法,并且使用某个库 2.3 版本的生产数据管道仓库。
目标仓库列表可以直接配置在 Shift 资源中,也可以存储在数据仓库表中。对于每天运行的周期性 Shift,这种方式非常有用,因为这类任务每天可能都需要面向不同的仓库集合,而这些仓库的源代码特征也会随时间变化。
和 Java BOM 类似,在我们决定大力投入全代码库重构之前,Fleetshift 就已经存在,并已被一些团队使用。为了降低新用户的上手难度,Fleetshift 平台维护团队还维护了一组可复用的容器镜像,供其他团队直接使用,或在其基础上构建适合自己需求的全代码库重构工具。
常用构建模块包括:用于修改 YAML 文件的容器镜像,用于将指定容器镜像版本升级到最新版本的镜像,以及用于进行 Java AST 重构的镜像。
我们还维护了知识库和文档中心,介绍如何在不同语言中完成特定类型的重构任务,帮助所有需要进行全代码库变更的团队相互学习。
自动合并:让自动化代码变更进入生产环境
如果想有效修改所有代码库中的代码,仅仅使用 Fleetshift 这样的自动化工具一次性生成成百上千个拉取请求还不够。这些变更还需要被合并。
一个典型工程团队可能维护几十个代码库,而每个代码库得到的关注程度,取决于相应组件的稳定性和活跃度。对于发送到大量代码库的自动化代码变更,如果仍然依赖仓库所有者手动合并,可能需要数周甚至数月,才能让大部分拉取请求被合并。
如果我们的目标,是让工程师把全公司范围代码重构视为日常工作,并持续增加机器人生成的代码变更数量,那么我们也必须避免让工程师被大量代码审查压垮。
在我们决定简化全代码库重构之前,当我们使用依赖自动更新工具,或内部脚本发送拉取请求时,就已经遇到了这个问题。要让大量拉取请求真正合并,从而完成全代码库变更,需要大量跟进工作,也要求各个团队逐个审查和合并这些拉取请求。
我们的解决方案,是尽量消除人工审核的必要性。我们构建了一套能力,让变更作者可以配置自己的自动化重构:只要代码变更通过所有测试和检查,就可以自动合并。
为此,我们构建了另一项新的基础设施服务,名为 automerger,也就是自动合并器。它包含自动合并相关逻辑,包括如何自动合并,以及何时自动合并。
虽然一些代码托管平台现在已经为拉取请求提供内置自动合并功能,但自己实现这套能力,让我们能够精确控制变更自动合并的方式和时间。
我们将自动合并的控制权从仓库所有者转移给变更作者,同时也控制变更合并的频率,以避免 CI/CD 系统过载。此外,为了减少意外和事故,我们只会在仓库所有团队的工作时间内自动合并变更,绝不会在周末和节假日自动合并。
只有通过所有测试的变更,才会自动合并到代码仓库中。但如果某个代码仓库的测试不足以发现某类错误变更,这也说明仓库所有者应该为其代码库添加更多、更好的测试。
为了帮助团队跟踪各个组件是否适合自动合并,我们在内部开发者门户中创建了新的组件健康检查项目。团队可以使用这些检查,监控其负责组件的代码健康状况,以及这些组件是否符合我们的测试、可靠性和代码健康标准。
例如,这些检查可以让所有者了解哪些组件正在使用 Java BOM 或其他集中式依赖管理清单,哪些组件可能缺少集成测试,或者某个后端服务是否没有在部署中使用健康检查。

将自动合并引入开发实践,既起到了激励作用,也起到了推动作用。
如果你的组件拥有足够完善的测试,我们就可以为你自动执行代码维护任务。同时,你的代码也需要通过足够多的测试,避免错误变更被合入。
我们看到,许多团队会主动提高组件健康分数,以便平台团队和其他团队能够管理并自动重构他们代码中的更多部分。
虽然大多数组件都有较完整的合并前测试套件,可以捕获潜在错误变更,并阻止其自动合并,但我们并不希望只依赖合并前测试来发现自动化集群重构中的问题。
一旦自动生成的拉取请求被自动合并,我们会通过一个名为 Firewatch 的服务,接收来自后端部署系统和数据管道执行系统的事件,从而监控受影响组件的运行状况。
每个自动合并的拉取请求都会注册到 Firewatch。这样,Firewatch 就可以把失败的后端服务部署或数据管道执行,与这些代码库中潜在的自动合并变更关联起来。
如果 Firewatch 发现过多自动合并提交都与失败的后端部署或管道执行相关,它会向这些自动变更的所有者发出警报,提醒他们调查问题。

自动合并能力最初的推广非常缓慢,起初只用于 Java BOM 更新。后端组件会根据重要程度被划分成多个阶段,BOM 更新的自动合并能力也按阶段逐步启用。
这让我们能够逐步建立对自动合并安全性的信心,并在提升 BOM 采用率和使用率的同时,监控合并后组件的运行状况。如今,BOM 的采用率和使用率已经超过 90%。
最初缓慢推出自动合并能力,并且不再要求每个拉取请求都由人工审核,这证明了自动合并概念的实用性和安全性。如今,我们每天在数十个 Shift 中使用自动合并,无论是简单的依赖变更,还是更复杂的重构,都会用到它。
在自动合并能力的早期推广阶段,我们有意将其设计为“默认启用、需要时退出”的功能,而不是“需要主动加入”的功能。
我们相信,正是这个决定促成了自动合并后来的成功和广泛应用,因为它从一开始就被设定为默认选项。
当团队希望让某个代码库退出自动合并时,我们要求他们记录退出原因,并鼓励他们为退出设置截止日期。这样做是为了推动他们修复导致自己不信任自动化能力的根本问题。
此外,对于最关键的代码库,在任何变更能够自动合并之前,还必须通过另一组约束和检查。
由于自动合并被设计为默认启用、需要时退出,目前在数千个代码库中,只有约 50 个代码库选择退出。我们也经常看到一些团队在添加退出选项后,会在退出过期前主动移除它,因为他们已经修复了根本问题,并准备重新启用自动合并。
我们从为某些类型的自动化变更引入自动合并中学到的一点是:一旦人们克服了最初对“机器人未经批准就修改代码”的恐惧,他们对这件事的总体态度并不是抗拒,而是希望获得更多自动化支持。
比我们预期更多的团队和工程师会询问,为什么某些自动化变更还没有自动合并。变更接收方团队也曾主动要求变更提供方启用自动合并。
我们发现,大多数工程团队并不希望费力审查和合并每一个“将某个库更新到最新补丁版本”或“升级构建中使用的容器镜像版本”的拉取请求。他们也乐于让代码消费者团队在需要时重构代码,以适配新版本框架或 API。
并不是所有通过 Fleetshift 发出的自动化代码变更都会配置为自动合并。Fleetshift 也常用于以“建议”的形式发送代码变更。
例如,有团队曾使用 Fleetshift 为对象存储桶推荐更合适的存储类别,或者调整运行在容器编排平台中的 Pod 大小,以减少 CPU 或内存资源使用。
我们发现,通过向团队提交拉取请求来提出“建议”,可以显著降低建议被采纳的门槛。负责团队只需要查看差异,然后点击合并按钮即可。
Fleetsweep:提交 PR 前验证全代码库重构
Fleetshift 为正在迭代初始版本全代码库变更的作者提供了“预览”模式。通过这个模式,作者可以在真正向任何人发送拉取请求之前,检查其代码转换对所有目标仓库产生的差异和日志。
然而,我们很快发现,仅仅能看到重构之后实际会修改哪些代码还不够。作者还想知道,在向任何人发送拉取请求之前,所有受影响仓库的测试是否仍然能够通过。
为了满足这一需求,我们开发了另一个工具 Fleetsweep。你可能已经注意到,它和 Fleetshift 在命名上保持了一致。
Fleetsweep 允许工程师针对一组代码仓库测试其 Shift 的容器镜像。Fleetsweep 作业运行在与 Fleetshift 相同的容器编排基础设施上,因此可以扩展到大量代码仓库。
Fleetsweep 不会直接向原始仓库发起拉取请求,而是在仓库中创建一个包含自动化变更的短期分支。然后,Fleetsweep 会在 CI 系统中触发该分支的构建,并将汇总结果报告给变更作者。作者可以据此检查哪些仓库在自动化重构后测试失败,并按需查看每个仓库的构建日志。
Fleetsweep 的存在,让工程师能够在将变更提交给 Fleetshift、创建可能打扰其他工程师的拉取请求之前,先测试这些变更在整个代码集群中的兼容性。
例如,对于 Java BOM,如果作者不确定某个新版本是否包含向后不兼容变更,通常会运行 Fleetsweep 来评估潜在的库升级风险。
渐进式发布:降低全代码库变更风险
随着我们将全代码库重构和自动化依赖管理扩展到更多组件类型,我们发现,不能总是对合并前测试发现问题的能力抱有很高信心。
例如,我们有很多批处理数据管道运行在云端数据处理平台上。虽然各个组件的测试套件通常专注于验证数据管道的业务逻辑,但要可靠测试管道在云端运行时的具体情况,以及数十个运行时依赖之间的交互方式,就困难得多。
批处理数据管道组件还面临另一个挑战:与后端服务不同,代码变更合并后不会立即执行。典型的批处理数据管道每小时或每天运行一次,这会显著拉长“代码变更合并”到“结果显现”之间的反馈周期。
最后,一些错误代码变更并不会导致组件崩溃或返回错误。某些错误变更可能会导致数据悄无声息地损坏。
为了解决这个问题,同时继续满足工程师一次性重构和更新所有组件代码的需求,我们逐步改进 Fleetshift 系统,使其支持“渐进式发布”。
作者可以配置 Shift,将目标代码库划分为多个组。Shift 所做的变更,例如将某个库从 X 版本升级到 Y 版本,只有当前一组中足够多代码库成功应用之后,才会应用到下一组。
为了判断某一组仓库在某个版本上是否“足够健康”,Fleetshift 会咨询 Firewatch 系统。它会为每个组找到一个最大版本,在这个版本中,前面组的一定数量仓库成功部署,或批处理数据管道成功执行的比例,高于可配置阈值。
Fleetshift 和 Firewatch 之间的连接,在自动化系统中形成了一种反馈循环:昨天自动化变更的成功或失败,会影响今天是否把同样的更新应用到更多仓库。

以前面提到的批处理数据管道为例,我们会根据信号将代码库集合划分成多个组。例如,可以根据管道生成数据的重要程度划分,先对低价值层级组件应用自动变更,再对高价值层级组件应用变更;也可以根据组件合并前测试和合并后监控的完善程度划分。
再举一个例子,每日 BOM 更新的 Shift 会对后端代码库进行分组,确保最关键系统只有在大多数其他后端组件成功部署后,才会接收新版本。
结果:研发效能与代码质量提升
借助上述基础设施和工具,这家海外公司在过去两年中,在全代码库重构方面取得了显著进展。
2022 年,Fleetshift 创建了超过 27 万个拉取请求,其中 77% 自动合并,11% 由人工合并。这两项数据相比 2021 年都增长了 4 到 6 倍。
使用 Fleetshift 的团队创建的 24.1 万个已合并拉取请求,总共修改了 420 万行代码。调查显示,超过 80% 的工程师表示,组件集群管理对他们的代码质量产生了积极影响。
比代码修改量或拉取请求数量更重要的是,这些工具和新的思维方式,对我们在整个服务集群中部署变更的能力产生了可衡量的影响。
正如本系列第一篇文章所述,我们内部后端服务框架新版本发布后,在 70% 以上已部署服务中得到采用所需的时间,已经从大约 200 天缩短到不到 7 天。

同样,内部数据管道框架以及用于管理批处理数据管道依赖项的 sbt 插件,新版本达到 70% 采用率所需时间,也已经从大约 300 天减少到约 30 天。

对于希望提升研发效能的团队来说,这类全代码库重构并不只是工具问题,还需要把目标制定、需求拆解、研发排期、开发测试、发布上线、知识沉淀和数据流转串联起来。像 PingCode 这样的智能化研发管理工具,就可以帮助团队以更自动化、数据化的方式管理研发全生命周期,让跨团队的大规模工程变更更容易被规划、追踪和复盘。
这些例子只来自众多 Shift 中的两个。在过去 12 个月中,共有 49 个不同团队使用 Fleetshift 向其他团队发送拉取请求。仅过去 30 天,就有 27 个团队通过 Fleetshift 发出拉取请求。
在此期间,每个 Shift 涉及的仓库数量中位数为 46 个,其中有 22 个 Shift 修改了 1000 个或更多仓库的代码。
将 Fleetshift 确立为全代码库重构的标准工具,并由一个全职团队负责维护,对于推动团队接受“组件集群优先”理念至关重要。
如今,团队不再需要询问可以使用哪些工具来修改他们并不直接拥有的代码。我们也看到,准备发布新版本或进行重大变更的团队,现在会主动规划如何使用 Fleetshift 推广这些变更。
尽管我们对目前的进展感到满意,但我们希望未来能将 Fleetshift 相关重构工作提升到新的水平。
我们一直在寻找降低复杂代码重构编写门槛的方法,让团队更容易上手 Fleetshift,并在 API 弃用和代码迁移方面更有信心。
此外,我们还可以做很多工作,帮助 Fleetshift 使用者更轻松地找到需要重构的代码。毕竟,编写 SQL 查询去搜索存储在数据仓库表中的代码,并不是一件简单的事。
最后,虽然我们通常拥有多个代码库,每个代码库包含一个或少数几个组件,但我们的一大部分代码仍然位于单体仓库中。许多团队会在同一个代码库中工作,例如客户端和移动端代码库。Fleetshift 目前对这种工作流的支持还不够完善。
这是我们组件集群管理系列文章的最后一篇。我们在组件集群管理方面仍然面临许多挑战,未来也会继续分享更多经验。
文章包含AI辅助创作:全代码库重构实践:大规模组件集群管理与自动化代码变更,发布者:su,转载请注明出处:https://worktile.com/kb/p/3973252
微信扫一扫
支付宝扫一扫