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

目录

介绍

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

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

在下面的更详细内容中,我们将详细阐述以下三个发现

  • 以分布式方式构建不能替代良好调整的构建过程。
  • 改进增量构建性能,而不是“完全重建”,是改善本地开发者体验的最重要方面。
  • 对于大多数 JVM 项目而言,超出测试分发的良好调整构建的通用构建分发是一个进化而非革命性的过程,它产生的边际性能收益。

此处提出的分析和发现尤其适用于 JVM 生态系统的项目。未来的后续文章将探讨 Android 和 native/iOS 生态系统。

最需要优化的场景 #

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

测试执行是瓶颈 #

测试执行通常是构建时间中最耗时的部分。优化构建以避免不必要的测试执行可以带来巨大的生产力提升。《Gradle Build Tool》已经可以在类路径上未检测到有意义的更改时跳过测试,并且还可以从构建缓存中恢复测试执行结果。《停止重新运行你的测试》这篇文章出色地解释了通过最小化测试重新执行可以实现的效率。

在分布式构建的背景下,此瓶颈通过 现代测试分发(例如 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 构建数千个微服务的公司 的构建数据。 

讨论