分布式构建

报告问题 查看源代码

如果您的代码库很大,依赖项链会变得非常深层。即使是简单的二进制文件也往往可以依赖数万个构建目标。在这种规模下,不可能在一台计算机上以合理的时间完成一项构建:任何构建系统都无法避免对机器硬件施加的基本物理法则。实现此目的的唯一方法是使用支持分布式构建的构建系统,在这种模式下,系统的工作单元分布在任意数量和可伸缩的机器上。假设我们已将系统的工作拆分为足够小的单元(稍后会详细介绍),这将使我们能够在我们愿意付费的情况下,尽快完成任何规模的任何构建。这种可伸缩性是我们通过定义基于工件的构建系统而逐渐实现的目标。

远程缓存

最简单的分布式构建类型是仅使用远程缓存的构建,如图 1 所示。

具有远程缓存的分布式构建

图 1. 显示远程缓存的分布式构建

执行构建的每个系统(包括开发者工作站和持续集成系统)都共享对通用远程缓存服务的引用。此服务可能是快速本地本地短期存储系统(如 Redis)或云服务(如 Google Cloud Storage)。每当用户需要构建工件(无论是直接构建还是作为依赖项)时,系统都会先使用远程缓存进行检查,以确定该工件是否已存在。如果是这样,它可以下载该工件,而不是进行构建。否则,系统会自行构建工件并将结果上传回缓存。这意味着,变化不太频繁的低级别依赖项可以构建一次并跨用户共享,而不必由每个用户重新构建。在 Google,许多工件都是通过缓存(而非从头开始)提供的,大大降低了运行构建系统的成本。

为了让远程缓存系统正常工作,构建系统必须保证构建完全可重现。也就是说,对于任何构建目标,都必须能够确定该目标的一组输入,以便同一组输入将能够在任何机器上产生完全相同的输出。这是确保下载工件的结果与自行构建工件的结果相同的唯一方法。请注意,这需要缓存中的每个工件都同时对其目标及其输入的哈希进行键控,这样不同的工程师就可以同时对同一目标进行不同的修改,而远程缓存将存储所有生成的工件并适当地提供这些工件,而不会产生冲突。

当然,为了获得远程缓存带来的任何好处,下载工件的速度必须快于构建工件的速度。这并不总是如此,尤其是在缓存服务器远离执行构建的机器时。我们精心调整了 Google 的网络和构建系统,以便能够快速分享构建结果。

远程执行

远程缓存不是真正的分布式构建。如果缓存丢失,或者您需要进行一项低级更改(需要重建所有内容),您仍然需要在机器上本地执行整个构建。真正的目标是支持远程执行,即执行构建的实际工作可以分布在任意数量的工作器上。图 2 描绘了一个远程执行系统。

远程执行系统

图 2. 远程执行系统

在每位用户的机器上运行的构建工具(用户是人工工程师或自动构建系统)会向中央构建主服务器发送请求。构建主实例会将请求拆分为对应的组件操作,并安排在可扩缩工作器池上执行这些操作。每个工作器都会使用用户指定的输入执行它要求的操作,并写出生成的工件。这些工件会在其他机器上共享,执行相应操作需要用到这些输出,直到可以生成最终输出并发送给用户。

实现此类系统最棘手的部分是管理工作器、主实例与用户的本地机器之间的通信。工作器可能依赖于其他工作器生成的中间工件,而最终输出需要发送回用户的本地机器。为此,我们可以让每个工作器将结果写入缓存并从缓存中读取其依赖项,从而在前面所述的分布式缓存的基础上进行构建。主实例会阻止工作器继续运行,直到其依赖的所有组件均完成运行为止,在这种情况下,工作器能够从缓存中读取输入。最终产品也会被缓存,这样本地机器就能够下载它。请注意,我们还需要一种单独的方式来导出用户源代码树中的本地更改,以便工作器可以在构建之前应用这些更改。

为此,需要整合之前所述的基于工件的构建系统的所有部分。构建环境必须完全自描述,以便我们可以在没有人工干预的情况下启动工作器。构建流程本身必须完全独立,因为每个步骤都可能在不同的机器上执行。输出必须具有完全确定性,以便每个工作器都可以信任它从其他工作器收到的结果。这种保证对于基于任务的系统来说非常困难,这使得基于它构建可靠的远程执行系统几乎是不可能的。

Google 的分布式 build

自 2008 年以来,Google 一直在使用同时采用远程缓存和远程执行的分布式构建系统,如图 3 所示。

简要构建系统

图 3. Google 的分布式构建系统

Google 的远程缓存称为 ObjFS。它由一个后端组成,该后端将构建输出存储在我们生产机器的 Bigtable 中,还有一个前端 FUSE 守护程序,名为 objfsd,该守护程序在每个开发者的机器上运行。通过 FUSE 守护程序,工程师可以浏览 build 输出,就像它们是存储在工作站上的普通文件一样,但文件内容仅按需下载,而需用户直接请求。按需提供文件内容可以显著减少网络和磁盘使用量,而且系统的构建速度是我们在开发者本地磁盘上存储所有构建输出时的两倍。

Google 的远程执行系统名为“Forge”,Blaze 中名为 Distributor 的 Forge 客户端(Bazel 内部等效项)将每项操作的请求发送到我们的数据中心内运行的作业程序(名为“调度器”)。调度程序会维护操作结果的缓存,以允许在系统的任何其他用户创建该操作后立即返回响应。否则,会将操作放入队列。大量执行器作业会持续读取此队列中的操作,执行这些操作,并将结果直接存储在 ObjFS Bigtable 中。这些结果可供执行程序在未来操作中使用,也可供最终用户通过 objfsd 下载。

最终,该系统可以高效地扩缩,以支持在 Google 执行的所有构建。Google 的构建规模非常庞大:Google 运行数百万个构建,并执行数百万个测试用例,每天从数十亿行源代码中生成 PB 级构建输出。这样的系统不仅让我们的工程师能够快速构建复杂的代码库,还能让我们实现大量依赖我们构建的自动化工具和系统。