通用构建分发:游戏规则改变者还是噱头?

远程和分布式构建模式 文章解释了远程构建和分布式构建之间的区别以及每种构建的变体。具体来说,我们区分了“测试分发”和“通用构建分发”。

本文从更广泛的角度讨论了分布式构建,以改善构建反馈时间。我们将首先解释工程师倾向于进行的更改类型,识别典型的瓶颈,并分享这些瓶颈与分布式构建的关系。我们还将研究通用构建分发的性能潜力。最后,我们将探索一种改善构建反馈时间的整体方法。

在下面更详细地,我们将详细阐述这三个发现

  • 分布式构建不能替代经过良好调整的构建流程。
  • 提高增量构建性能,而不是“完全重建”,是改善本地开发人员体验的最重要方面。
  • 除了测试分发之外,将经过良好调整的构建进行一般性构建分发是一个演进过程,而不是革命性过程,它为大多数 JVM 项目带来的性能提升微乎其微。

此处提供的分析和发现特别适用于 JVM 生态系统的项目。未来的后续文章将讨论 Android 和原生/iOS 生态系统。

最需要优化的场景

改善本地开发人员体验的两个关键在于了解工程师面临的典型瓶颈,以及工程师在添加新功能、修复错误和编写测试时所进行的更改类型。

测试执行是瓶颈

测试执行通常是构建时间中最耗时的部分。优化构建以避免不必要的测试执行可以带来巨大的生产力提升。Gradle 构建工具在检测到类路径上没有有意义的更改时会跳过测试,还可以从构建缓存中恢复测试执行结果。文章 停止重新运行测试 很好地解释了通过最小化测试重新执行获得的效率。

在分布式构建的背景下,此瓶颈通过 现代测试分发 来解决,例如 Gradle Enterprise 提供的测试分发解决方案。

增量构建与完全重建的频率

下一个关键点是,在绝大多数情况下,工程师都在构建小的增量更改。我们认为,这些小的增量更改不太可能从将构建步骤分发到测试分发之外中获益。此外,使用现代构建系统的开发人员很少执行“完全重建”,而没有共享构建缓存或保留在同一台机器上先前构建的历史记录的优势。

考虑对 Java 类私有方法主体进行更改:只需要重新编译该类,并重新组装包含它的库。但是,没有理由重新编译该库的下游使用者,因为它无法链接到私有方法。在另一个极端,考虑对由多项目构建中的许多其他子项目使用的“通用”库的公共 API 进行修改。这将导致“多米诺骨牌效应”,导致其下游使用者被重新编译。一般构建分发可能有助于这种情况,但我们认为这只是例外,而不是常态(有关更多见解,请参见下面的并行化因子)。

此外,与“原生”语言相比,Java 编译速度相对较快,这进一步降低了 Java 项目中一般构建分发的优化潜力。

因此,我们鼓励对“从头开始”构建大型项目作为衡量构建系统性能的真正指标,或作为实施远程或分布式构建的理由持怀疑态度。

并行化因子

理解任何构建(本地构建、远程托管或分布式)的最大速度潜力的关键是可视化其输出的相互依赖关系。想象一个相对较小的软件项目,它有三个子项目:A、B 和 C。如果编译子项目 C 需要子项目 A 和 B 的输出,那么 C 依赖于 A 和 B。最重要的是,在 A 和 B 完成之前,我们无法开始构建 C;因此,最佳情况下的构建时间场景可以表示为 max(A, B) + C。鉴于本地或远程构建主机具有无限的 CPU 内核,或者具有无限的分布式构建代理池,构建无法比这个瓶颈进一步并行化。

示例项目结构 图 1:项目结构

由于我们看到这个瓶颈是基于依赖关系而不是基于性能的,因此我们现在能够预测远程或分布式构建的潜在益处。

为了验证这个理论,我们对并行化因子进行了一些分析1,以确定在上述瓶颈的情况下理论上可实现的最小构建时间。我们与一些合作伙伴一起检查了 Gradle 本身和其他大型构建2 的构建。我们发现了这些有趣的结果

  • 测试执行占用了构建时间的大部分,约占端到端 CI 周期的 80%-90%。
  • 在测试执行之后,最耗时的任务是 CPU 密集型任务,例如编译或验证,其次是磁盘绑定打包/组装任务。
  • 超过一半的非测试任务在一个进程中执行。

最后一点至关重要:作为单个进程运行的任务(没有其他进程同时执行)表明存在瓶颈,例如上面示例中的子项目 C。单进程任务证明无法通过分布式进一步优化。可以通过更强大的远程 CPU 更快地完成编译任务,但这种好处很容易被来回发送数据带来的开销抵消。

Cumulative work time
图 2:累积工作时间,按并发工作者数量分组。一半的工作是在没有其他繁忙进程并行运行的情况下执行的。

撇开测试执行(由测试分布解决,参见 上面),专注于剩余的 CPU 密集型 10%-20% 的构建时间,我们发现优化潜力很低。一半的任务无法与其他进程并行执行,这意味着,在最好的情况下,通用分布式解决方案只能加快 5%-10% 的整体构建时间,同时会带来构建复杂性和管理开销方面的重大成本。

前进方向

正如我们上面讨论的,开发人员进行的大多数更改都是小的、增量的更改,最大的瓶颈通常是测试执行。因此,将构建优化重点放在这些方面通常会产生最佳结果。以下部分列出了构建流程今天可以实施的一些关键步骤。这些构建性能优化的基本原则不仅会改善任何构建(无论是本地、远程还是分布式),而且还会确保在将来可能迁移到远程或分布式环境时获得最佳性能。

按此顺序,我们建议利用这些 Gradle 构建工具功能来优化本地构建反馈时间。大多数这些功能在 提高 Gradle 构建性能 中有更详细的说明。

  1. 增量构建
  2. 编译避免增量编译
  3. 远程构建缓存
  4. 并行执行
  5. 配置缓存(也提高了本地并行性)

此外,Gradle Enterprise 中的以下功能极大地缩短了测试反馈时间,这通常是构建性能中最大的瓶颈。

虽然通用构建分发在隔离情况下测量时可能显示出令人印象深刻的构建性能提升,但我们已经证明,对于大多数 JVM 项目而言,它不太可能为经过良好优化的构建中的典型场景提供显著的额外构建性能改进。这并不是说我们认为通用分发解决方案没有意义。相反,我们认为这在我们长期路线图中是一个演进的解决方案,而不是革命性的解决方案。

总结

轶事证据和行业经验表明了两件事:首先,工程师最有可能迭代和重建小的、增量式的更改 - 而不是从头开始重建整个项目。其次,无论构建的更改类型如何,测试执行都是构建速度慢和开发人员生产力下降的主要原因。

使用活动进程计数作为本地构建潜在并行化的代理,我们已经表明,对于 JVM 生态系统中的许多构建而言,通用构建分发解决方案对构建性能的影响相对较小 - 如果有的话。

以纯粹的分布式方式运行 JVM 构建的所有方面并不是灵丹妙药。现有的 Gradle 构建工具功能,如增量任务执行、编译避免、增量编译、构建缓存和配置缓存,如今已可用,并极大地减少了构建时间,特别是对于最频繁的增量更改。此外,Gradle Enterprise 中的商业功能,如 测试分发预测性测试选择 极大地减少了测试执行时间,而测试执行时间是大多数构建的主要瓶颈。

反馈

如果您对我们的 论坛Gradle 社区 Slack 有任何疑问,请告诉我们。


  1. 我们使用此 工具 从选定的 Gradle Enterprise 服务器池中收集了 30 天的 Build Scan™ 数据。 

  2. 这些服务器捕获了 Gradle、Gradle Enterprise 商业产品、Spring 项目以及使用 Gradle 构建数千个微服务的公司的构建数据。 

讨论