优化Monorepo合并:实现绿色构建的策略

Thenewstack

对于许多小型软件开发团队或管理多个独立代码仓库的团队来说,将新代码变更合并到主代码库中似乎很简单:只需简单点击一个按钮。然而,对于在单一共享代码库(通常称为monorepo)中运作的大型组织而言,这项看似微不足道的任务却变成了软件交付流程中最显著的瓶颈之一。在这里,数十甚至数百名工程师同时贡献代码,使得集成复杂性急剧升级。

尽管关于monorepo和polyrepo的争论非常广泛,但很明显,monorepo提供了独特的优势,例如简化漏洞识别与修补、更轻松的跨项目重构,以及在不同项目中保持一致的工具和共享库。然而,这些优势也伴随着固有的挑战。开发人员经常遇到由于基于过时主分支的拉取请求(PR)导致的过时依赖、同时在相似代码上工作而产生的微妙冲突,以及超时等持续的基础设施问题。此外,内部和第三方依赖的管理变得复杂,共享状态可能导致不稳定的(“flaky”)测试行为。随着工程组织的规模扩大,这些挑战呈指数级增长,常常导致开发人员将大量非生产性时间花在等待构建过程成功完成上。

为了缓解这些日益增长的延迟,现代开发工作流越来越多地采用合并自动化工具,例如GitHub合并队列、GitLab合并列车和类似解决方案。这些系统通过引入自动化门禁来规范变更流入主代码库的流程,从而从根本上改变了游戏规则。该过程通常涉及开发人员将拉取请求标记为可集成状态。系统随后会自动将此PR变基到主分支的最新版本。接着,在此更新的上下文中触发持续集成(CI)过程。如果CI检查通过,系统将继续合并变更。关键的是,如果在CI运行期间有新的PR到达,它们会被排队并等待轮到它们,从而确保有序且经过验证的集成序列。

尽管合并队列提供了基础解决方案,但大型monorepo中庞大的变更量需要进一步优化。一种常见的策略是批量处理拉取请求。它不是一次处理一个PR,而是将几个PR分组在一起,然后进行一次CI运行。这种方法显著减少了CI执行的总次数,并缩短了整体等待时间。如果批次的CI过程成功,所有包含的PR都会同时合并。如果批次内发生失败,系统可以系统地“二分法定位”批次以找出有问题的PR,从而允许其余成功的变更继续进行。虽然在理想条件下,批量处理可以显著缩短合并时间——例如,假设将50小时的等待时间缩短到12.5小时(批处理大小为四)——但在实际场景中,即使只有10%的适度失败率,也可能大大延长总合并时间,可能使其翻倍,并由于重复处理而增加CI运行次数。

为了进一步提高效率,乐观队列引入了范式转变,从串行处理模型转向更并行的处理方法。系统不再等待一个拉取请求的CI过程完全完成,而是乐观地假设它会通过。然后,它会创建一个备用主线分支,立即开始队列中下一个拉取请求的CI过程。如果初始PR通过其CI,它将合并到主分支;同样,后续PR在成功完成后也会合并。如果第一个PR失败,备用主线将被丢弃,并创建一个没有问题变更的新主线,从而允许对剩余PR重新启动验证过程。将这种“乐观”方法与批处理结合起来,就形成了乐观批处理,其中整批PR并行处理,失败时会触发拆分和识别机制。

另一种先进技术是预测建模。这涉及分析历史数据和拉取请求的特征——例如更改的代码行数、修改的文件类型或依赖项数量——以计算一个分数,指示成功或失败的可能性。通过利用这些预测,系统可以优先级排序或重新排序PR,将CI资源集中在最有可能成功的路径上,从而降低整体CI成本并加速合并。

对于真正大规模的monorepo,单个队列仍然可能成为瓶颈。这可以通过多队列和受影响的目标来解决。现代monorepo构建工具,如Bazel、Nx或Turborepo,可以精确识别代码库中哪些部分受到特定变更的影响。这种智能允许系统根据“受影响的目标”将拉取请求分组到独立的并行队列中。例如,如果一个系统产生四种不同的构建类型(A、B、C、D),并且传入的PR只影响其中的一部分,则可以为每种构建类型建立单独的队列。这确保了不相关的变更不会相互阻塞,通过允许独立构建的并发执行,显著加快了整体集成过程。

除了这些基于队列的策略之外,还有其他互补的优化措施可以提高工作流效率。重新排序变更涉及优先处理失败风险较低或业务重要性较高的拉取请求,将它们放在队列前面,以最大限度地减少级联失败。更复杂或不确定的变更则安排在后面。**“快速失败”**原则规定优先执行CI过程中最可能早期失败的测试,确保问题变更被快速识别和解决。最后,拆分测试执行可以包括在合并前运行一组快速、关键的测试以捕获常见问题,而更广泛或更慢的测试(如集成测试或冒烟测试)则在合并后执行。在极少数的合并后失败情况下,自动化回滚机制可以降低风险。

最终,这些合并策略的复杂编排旨在实现一个关键平衡:在保持高可靠性和代码质量的同时,最大限度地提高发布速度。除了节省CI周期外,先进的合并自动化显著减少了开发人员的等待时间,加速了功能交付,并确保了工程团队在应对大规模软件开发复杂性时的良好状态。