当您拥有大型代码库时,依赖项链可能会非常深。 即使是简单的二进制文件也常常依赖于数万个 build 目标。在这种规模下,在单台机器上以合理的时间完成 build 根本是不可能的:没有任何 build 系统可以绕过机器硬件所受到的基本物理定律。唯一可行的办法是使用支持分布式 build 的 build 系统,其中系统执行的工作单元分布在任意数量且可扩缩的机器上。假设我们已将系统的工作分解为足够小的 单元(稍后会详细介绍),这样我们就可以根据愿意支付的费用,以尽可能快的速度完成任何大小的 build。这种可扩缩性是我们通过定义基于制品的 build 系统一直努力实现的目标。
远程缓存
最简单的分布式 build 类型是仅利用 远程 缓存 的 build,如图 1 所示。
图 1. 显示远程缓存的分布式 build
执行 build 的每个系统(包括开发者工作站和 持续集成系统)都共享对通用远程缓存 服务的引用。此服务可以是快速的本地短期存储系统(如 Redis),也可以是云服务(如 Google Cloud Storage)。每当用户需要 构建制品(无论是直接构建还是作为依赖项构建)时,系统都会先检查 远程缓存,看看该制品是否已存在于其中。如果是,则可以下载该制品,而不是构建它。如果不是,系统会自行构建 制品,并将结果上传回缓存。这意味着, 不经常更改的低级别依赖项可以构建一次并 跨用户共享,而无需由每个用户重新构建。在 Google,许多 制品都是从缓存中提供的,而不是从头开始构建的,这大大 降低了运行 build 系统的成本。
为了使远程缓存系统正常运行,build 系统必须保证 build 是完全可重现的。也就是说,对于任何 build 目标,都必须能够 确定该目标的输入集,以便相同的输入集 在任何机器上都能生成完全相同的输出。这是确保下载制品的結果与自行构建制品的結果相同的方法。请注意,这要求缓存中的每个制品 以其目标及其输入的哈希值为键,这样,不同的 工程师就可以同时对同一目标进行不同的修改,而远程缓存会存储所有生成的制品并适当地提供它们,而不会发生冲突。
当然,为了从远程缓存中获得任何好处,下载 制品的速度需要比构建制品的速度更快。情况并非总是如此, 尤其是当缓存服务器距离执行 build 的机器很远时。Google 的 网络和 build 系统经过精心调整,能够快速共享 build 结果。
远程执行
远程缓存不是真正的分布式 build。如果缓存丢失,或者您 进行了需要重新构建所有内容的低级别更改,您仍然需要 在本地机器上执行整个 build。真正的目标是支持 远程执行,其中执行 build 的实际工作可以分布在 任意数量的工作器上。图 2 描绘了一个远程执行系统。
图 2。远程执行系统
在每位用户的机器上运行的 build 工具(用户可以是人类 工程师,也可以是自动 build 系统)向中央 build 主机发送请求。 build 主机将请求分解为组件操作,并安排 在可扩缩的工作器池中执行这些操作。每个工作器 都会使用用户指定的输入执行要求其执行的操作,并 写出生成的制品。这些制品在执行需要它们的其他 机器之间共享,直到生成最终输出并将其 发送给用户。
实现此类系统最棘手的部分是管理工作器、主机和用户本地机器之间的通信 。工作器可能 依赖于其他工作器生成的中间制品,并且最终输出 需要发送回用户的本地机器。为此,我们可以通过让每个工作器将其结果写入缓存并从缓存中读取其依赖项,在前面介绍的分布式缓存的基础上进行构建。主机阻止 工作器继续执行,直到它们依赖的所有内容都已完成,在这种 情况下,它们将能够从缓存中读取其输入。最终产品也会被缓存,从而允许本地机器下载它。请注意,我们还需要一种 单独的方法来导出用户源树中的本地更改,以便 工作器可以在构建之前应用这些更改。
为了使此方法正常运行,前面介绍的基于制品的 build 系统的所有部分都需要协同工作。build 环境必须完全 自描述,以便我们无需人工干预即可启动工作器。build 过程本身必须完全自包含,因为每个步骤都可能 在不同的机器上执行。输出必须完全确定,以便每个工作器都可以信任从其他工作器收到的结果。对于基于任务的系统来说,提供此类 保证非常困难,这 使得在基于任务的系统之上构建可靠的远程执行系统几乎不可能 。
Google 的分布式 build
自 2008 年以来,Google 一直使用分布式 build 系统,该系统同时采用 远程缓存和远程执行,如图 3 所示。
图 3. Google 的分布式 build 系统
Google 的远程缓存称为 ObjFS。它由一个后端组成,该后端将 build 输出存储在分布在我们生产 机器群中的 Bigtable 中,以及一个前端 FUSE 守护进程(名为 objfsd),该守护进程在每位开发者的 机器上运行。FUSE 守护进程允许工程师浏览 build 输出,就像它们 是存储在工作站上的普通文件一样,但文件内容 仅针对用户直接请求的少数文件按需下载。按需提供文件内容大大减少了网络和磁盘 使用量,与我们将所有 build 输出存储在开发者的本地磁盘上相比,该系统能够以两倍的速度进行构建。
Google 的远程执行系统称为 Forge。Blaze (Bazel 的内部等效项)中的 Forge 客户端(称为 Distributor)向在我们的 数据中心内运行的作业(称为 Scheduler)发送每个操作的请求。Scheduler 维护操作 结果的缓存,如果系统中的任何其他用户已创建该操作,则允许其立即返回响应。如果不是,它会将操作放入 队列中。大量执行器作业不断从此队列中读取操作, 执行操作,并将结果直接存储在 ObjFS Bigtable 中。执行器可使用这些 结果来执行未来的操作,或者最终用户可以通过 objfsd 下载 这些结果。
最终结果是一个可扩缩的系统,可高效支持 Google 执行的所有 build 。Google 的 build 规模确实非常庞大:Google 每天运行数百万个 build,执行数百万个测试用例,并从数十亿行源代码中生成 PB 级 的 build 输出。这样的系统不仅让我们的工程师能够快速构建复杂的代码库,还允许我们实现大量依赖于我们的 build 的自动化工具和系统。


