分布式 build

报告问题 查看源代码 每夜 build · 8.0 · 7.4 · 7.3 · 7.2 · 7.1 · 7.0 · 6.5

如果代码库很大,依赖项链可能会非常深。即使简单的二进制文件也可能依赖于数万个构建目标。在这种规模下,单台机器根本不可能在合理的时间内完成构建:没有任何构建系统可以规避对机器硬件施加的基本物理定律。要实现这一点,唯一的方法是使用支持分布式 build 的构建系统,其中系统要完成的工作单元分布在任意数量的可伸缩机器上。假设我们已将系统的工作拆分为足够小的单元(稍后会详细介绍),这样一来,我们就可以根据需要,尽快完成任何大小的任何 build。这种可伸缩性是我们一直以来通过定义基于工件的构建系统而努力追求的目标。

远程缓存

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

使用远程缓存的分布式构建

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

执行构建的每个系统(包括开发者工作站和持续集成系统)都共享对通用远程缓存服务的引用。此服务可以是 Redis 等快速的本地短期存储系统,也可以是 Google Cloud Storage 等云服务。每当用户需要构建工件(无论是直接构建还是作为依赖项构建)时,系统都会先与远程缓存进行检查,以确定该工件是否已存在。如果是,它可以下载工件,而不是构建工件。如果没有,系统会自行构建工件,并将结果重新上传到缓存。这意味着,不经常更改的低级依赖项可以构建一次并供所有用户共享,而无需每个用户都重新构建。在 Google 内部,许多工件都是从缓存中提供的,而不是从头构建的,这大大降低了运行构建系统的费用。

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

当然,为了让远程缓存发挥作用,下载工件需要比构建工件更快。但实际情况并非总是如此,尤其是当缓存服务器距离进行构建的机器较远时。Google 的网络和构建系统经过精心调整,能够快速共享构建结果。

远程执行

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

远程执行系统

图 2. 远程执行系统

在每个用户的机器上运行的构建工具(用户是人力工程师或自动化构建系统)会向中央构建主服务器发送请求。build master 会将请求拆分为其组件操作,并安排在可伸缩的工作器池中执行这些操作。每个 worker 都会使用用户指定的输入执行所要求的操作,并写出生成的工件。这些工件会在执行需要它们的操作的其他机器之间共享,直到能够生成最终输出并将其发送给用户。

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

为此,之前介绍的基于工件的构建系统的所有部分都需要协同工作。构建环境必须完全自描述,以便我们能够在不人为干预的情况下启动工作器。构建流程本身必须完全独立,因为每个步骤都可能在不同的机器上执行。输出必须完全确定性,以便每个工作器都能信任从其他工作器收到的结果。基于任务的系统极难提供此类保证,因此几乎不可能基于此类系统构建可靠的远程执行系统。

Google 的分布式 build

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

高级构建系统

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

Google 的远程缓存称为 ObjFS。它由后端和前端 FUSE 守护进程组成,后端会将 build 输出存储在分布在我们整个生产机群中的 Bigtable 中,前端 FUSE 守护进程名为 objfsd,会在每个开发者的机器上运行。借助 FUSE 守护程序,工程师可以像浏览工作站上存储的普通文件一样浏览 build 输出,但只会按需下载用户直接请求的少数文件的文件内容。按需提供文件内容大大减少了网络和磁盘用量,与将所有 build 输出存储在开发者的本地磁盘上相比,系统的构建速度提高了两倍。

Google 的远程执行系统称为 Forge。Blaze 中的 Forge 客户端(Bazel 的内部等效项)称为“分销商”,会将每个操作的请求发送到我们数据中心中运行的作业(称为“调度程序”)。调度程序会维护操作结果缓存,以便在系统的任何其他用户已创建操作时立即返回响应。如果没有,则将操作放入队列。大量的 Executor 作业会持续从此队列中读取操作、执行这些操作,并将结果直接存储在 ObjFS Bigtable 中。这些结果可供执行器用于日后执行操作,或供最终用户通过 objfsd 下载。

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