分布式构建

如果您有一个大型代码库,依赖项链可能会变得非常深。即使是简单的二进制文件通常也依赖于数以万计的构建目标。在这种规模下,在一台机器上以合理的时间完成构建简直是不可能的:任何构建系统都无法规避对机器硬件施加的物理基础定律。完成此工作的唯一方式是,使用支持分布式构建的构建系统,其中系统完成的工作单元分布在任意数量的可伸缩机器上。假设我们将系统的工作分成了足够小的单元(稍后将详细介绍),这将使我们能够在愿意支付的速度的前提下完成任何大小的任何构建。这种可扩缩性是我们通过定义基于工件的构建系统,一直致力于提升性能的。

远程缓存

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

使用远程缓存进行分布式构建

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

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

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

当然,要获得远程缓存的任何好处,下载工件需要比构建工件更快。但情况并非总是如此,尤其是当缓存服务器远离执行构建的机器时。Google 的网络和构建系统均经过精心调整,能够快速分享构建结果。

远程执行

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

远程执行系统

图 2. 远程执行系统

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

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

为此,需要将前面描述的基于工件的构建系统的所有部分组合在一起。构建环境必须完全自描述,以便我们无需人工干预即可启动工作器。构建流程本身必须完全独立,因为每个步骤可能会在不同的机器上执行。输出必须是完全确定性的,以便每个工作器能够信任从其他工作器接收的结果。这种保证对于基于任务的系统而言是非常困难的,这使得在这种系统的基础上构建可靠的远程执行系统几乎是不可能的。

Google 的分布式构建

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

高级构建系统

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

Google 的远程缓存称为 ObjFS。它包括一个后端,将构建输出存储在 Bigtable(分布在我们整个生产机器群中)和一个前端 FUSE 守护程序(名为 objfsd,可在每个开发者的机器上运行)。借助 FUSE 守护程序,工程师可以浏览构建输出,就像它们是工作站上存储的正常文件一样,但仅针对用户直接请求的少数文件按需下载文件内容中披露政府所要求信息的数量和类型。按需提供文件内容会大大降低网络和磁盘使用量,而且,与将所有构建输出存储在开发者的本地磁盘上相比,系统构建速度的速度可以提升一倍。

Google 的远程执行系统叫做 Forge。Blaze 的 Forge 客户端(称为 Bazel 的内部等效项)称为 StatefulSet ,将每项操作的请求发送到我们的数据中心中运行的名为 Scheduler 的作业。调度程序会维护操作结果缓存,以便在操作已由系统的其他任何用户创建时立即返回响应。否则,它会将操作置于队列中。庞大的执行器池会不断读取该队列中的操作,执行这些操作,并将结果直接存储在 ObjFS Bigtable 中。这些结果可供执行程序执行将来的操作,或供最终用户通过 objfsd 下载。

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