原文:Bazel’s Remote Caching and Remote Execution Explained
作者:Brentley Jones · 原文发表于 2022-03-16。
译注:文中的“我们”指原作者或 BuildBuddy 团队,时效性表述以原文发布日期为准。Bazel 专有名词保留英文,代码和命令未作翻译。
译注:本文的 flag 名称和默认行为对应写作时的 Bazel 5.0;相关文档与代码链接也指向该版本。文中保留这些历史名称,以便与原文的执行流程和脚注对应。
Bazel 著名的 remote caching 和 remote execution 功能可以改变游戏规则,但如果您不熟悉它们的工作原理,它们可能会有点神秘。
别担心。我会从基础讲起,介绍 remote caching 和 remote execution,再深入到具体细节,也就是 actions 和 spawns 😄,带你了解 Bazel 的远程能力。
Actions
对于一次请求的 构建,构建 graph 中的每个 rule target,都会在 analysis phase 生成零个或多个 actions。这些 actions 组成 action graph,表示 execution phase 期间需要执行的全部 actions。
示例 action graph。 Actions 按创建它们的 targets 进行分组。箭头连接 actions,显示它们之间的依赖关系。
在 execution phase,Bazel 会遍历 action graph,逐个判断 action 是否需要执行。如果 output base 的 action cache 中没有对应的 action 结果,或者 output base 中的输出与 action 结果记录的输出不一致,就需要执行。
Spawns
如果 Bazel 需要执行一个 action,就会创建一个 spawn,其中包含执行该 action 所需的全部信息,包括 spawn 的 strategy1。strategy 会决定多种行为,例如 spawn 是否使用 remote caching 或 remote execution,以及具体如何使用。
spawn 的 strategy 会决定,Bazel 是否需要在本地执行 action 之前、之后或代替本地执行时做额外的工作。例如,执行采用 remote-cache strategy 的 spawn 时,Bazel 可能先检查外部缓存的 action cache 中是否已有 action 结果。如果有,就可能直接下载列出的输出,而不执行 action。我会在 后文 详细说明。
无论输出是下载的还是在本地生成的,一个 action 的输出一旦在 output base 中就绪,action graph 中依赖它的 actions 就可以启动。
并行性
本地能够同时执行的 actions 数量由 --local_cpu_resources flag 限制,能够同时执行的 spawn 数量则由 --jobs flag 限制。spawn 的执行相比 action 的本地执行,可能包含更多或不同的工作,因此把 --jobs 设得比 --local_cpu_resources 更大可能有益。这种情况下,spawn 尝试在本地执行 action 时,可能会阻塞,等待 CPU 资源释放。这时 remote execution 就能发挥作用。
Remote caching
Bazel 所说的 remote cache,可能指机器本地的 disk cache,也可能指通过网络协议访问、通常不在本机上的 remote cache。为避免混淆,本文把这两类缓存统称为 external cache,因为它们都位于 output base 之外。
使用外部缓存,Bazel 会使用外部缓存的 action cache (AC) 和 content-addressable storage (CAS) 来增强其 output base。这意味着如果 Bazel 想要执行的 action 的 action 结果不在 output base 的 action cache 中,则 Bazel 可以检查 AC 是否有它。 action 的输出也是如此;如果 output base 没有预期的输出,则 Bazel 可以检查 CAS 是否有预期的输出。
remote-cache
Bazel 通过 remote-cache spawn strategy 实现这种行为,并同时使用一种本地 strategy,例如 sandbox、worker 或 local 等。
一个采用 remote-cache strategy 的 spawn 包含以下步骤:
- 如果action的action结果不在output base的action cache中,尝试从AC检索它(
ActionCache.GetActionResult)2 - 如果从任一缓存检索到 action 结果…
- 并且 action 的输出已经位于 output base 中,spawn 就完成了
- 否则,尝试从 CAS (
ByteStream.Read) 检索它
- 如果成功取得输出,spawn 就完成了
- 否则,action在本地执行
- 如果 action 成功完成,则 action 的输出将上传至 CAS (
ByteStream.Write),其 action 结果将上传至 AC (ActionCache.UpdateActionResult)3- 如果使用了
--experimental_remote_cache_async4,spawn 就完成了;上传继续在后台进行,依赖它的 actions 可以启动 - 否则,必须先完成上传,依赖它的 actions 才能启动
- 如果使用了
Flags
设置 --disk_cache 标志会导致 Bazel 使用文件系统上的该目录作为外部缓存。设置 --remote_cache 标志会导致 Bazel 通过 HTTP(S)、gRPC(S) 或 UNIX 套接字连接到外部缓存。设置这两个标志会导致 Bazel 同时使用 disk cache 和 remote cache,形成“组合缓存”。
combined cache 同时读写 disk cache 和 remote cache,整体按 remote cache 对待,除非设置 --incompatible_remote_results_ignore_disk flag。设置后,disk cache 仍按 local cache 的方式工作:即使设置了 --noremote_accept_cached,它也可以返回结果;即使设置了 --noremote_upload_local_results,它也可以保存结果;带有 no-remote-cache/no-remote 标记的 actions 也可以读写其中的结果。 5
设置 --experimental_guard_against_concurrent_changes 标志有助于保护外部缓存免受 构建 期间发生的输入文件更改的破坏。如果开发人员启用了外部缓存,我强烈建议设置此标志,即使它只是 disk cache。
大多数 remote cache 实现会根据 --remote_instance_name flag 的值隔离 AC,有些也会隔离 CAS。这样做可以区分项目,也可以临时缓解非 hermetic toolchains 带来的问题。
--remote_cache_header 标志导致 Bazel 将请求中的额外标头发送到外部缓存。通过多次指定标志可以传递多个标头。相同名称的多个值将转换为逗号分隔的列表。可以使用 --remote_header 标志来代替将 --remote_cache_header 和 --remote_exec_header 设置为相同的值。
Remote execution
Bazel 可以通过 remote execution,在远程 executor 上执行 actions。由于这些 actions 不消耗本地资源,可远程并行执行的 actions 数量只受 --jobs 和可用远程资源限制,不受 --local_cpu_resources 限制。如果构建有足够的并行度,就可能更快完成。并行性一节有更详细的说明。
remote
Bazel 通过 remote spawn strategy 实现远程执行 actions。它包含 remote-cache strategy 的大部分行为,并且会代替本地 strategy,例如 sandbox、worker 或 local 等。
一个采用 remote strategy 的 spawn 包含以下步骤:
- 如果action的action结果不在output base的action cache中,尝试从AC检索它(
ActionCache.GetActionResult)6 - 如果从任一缓存检索到 action 结果…
- 并且 action 的输出已经位于 output base 中,spawn 就完成了
- 否则,尝试从 CAS (
ByteStream.Read) 检索它
- 如果成功取得输出,spawn 就完成了
- 否则,确定是否需要上传 action 的任何输入(
ContentAddressableStorage.FindMissingBlobs),然后上传它们(ByteStream.Write) - 然后远程执行action(
Execution.Execute) - 如果 action 成功完成,请从 CAS 下载 action 的输出 (
ByteStream.Read)
Disk cache
从 Bazel 5.0 开始,disk cache(或更具体地说,组合缓存)可与 remote execution 一起使用。在 Bazel 5.0 之前,如果您还想在本地缓存内容,则必须设置 remote cache 代理 sidecar。
Dynamic execution
Bazel 支持 remote execution 的一种模式,称为 “dynamic execution”,其中同一 action 的本地和 remote execution 并行启动,使用第一个完成的分支的输出,并取消另一个分支。
为了完整起见,我想提及它,因为如果调整得当,它可以比单独使用本地执行或 remote execution 更快地构建。但是,它可能无法与 Remote Build without the Bytes 配合良好,因为本地执行分支可能需要下载先前远程完成的 actions 的输出,如果调整不当,可能会导致构建速度变慢。
Flags
--remote_executor
设置 --remote_executor 标志会导致 Bazel 通过 gRPC(S) 或 UNIX 套接字连接到远程 executor。如果未设置 --remote_cache,则默认为为 --remote_executor 设置的值。大多数 remote execution 设置将在同一端点具有 remote cache 和远程 executor。
--remote_instance_name flag 除了影响 remote cache,还可能决定 构建 使用哪个 remote execution 集群。有些 actions 可能需要特定硬件或软件,因此要把 target 指向 executors 的特定子集,这可以通过平台属性实现。
--remote_default_exec_properties
平台属性可以用 --remote_default_exec_properties flag 全局设置,但前提是尚未在 platform 或 target 层面设置。action cache 中保存的 action 结果包含平台属性,因此这些属性会影响 action cache 的命中率。如果只在某些条件下启用 remote execution,又需要设置平台属性,可以考虑始终设置相同属性,以便复用缓存的 action 结果。有些 remote execution 实现允许通过 --remote_exec_header flag 设置全局平台属性,避免这类 cache misses。
--remote_timeout
--remote_timeout flag 控制 Bazel 等待 remote cache 操作完成的时间。这个超时不适用于 Execution.Execute 调用 7,但 remote execution 可能需要上传或下载本地 构建 无需传输的 artifacts,因此默认的 60 秒可能不够。
--remote_retries
--remote_retries 标志控制 Bazel 在发生瞬时错误(例如超时)时重试远程操作的次数。该标志默认为 5,并且根据您计划如何使用 remote execution,您可能希望将其增加到更大的值。 Bazel 使用指数退避进行重试,但目前将调用之间的延迟限制为 5 秒。
--remote_exec_header
--remote_exec_header 标志导致 Bazel 将请求中的额外标头发送到远程 executor。通过多次指定标志可以传递多个标头。相同名称的多个值将转换为逗号分隔的列表。可以使用 --remote_header 标志来代替将 --remote_cache_header 和 --remote_exec_header 设置为相同的值。
远程 Build without the Bytes
Bazel 对 remote caching 和 remote execution 都支持 Remote Build without the Bytes(BwtB)。启用后,Bazel 只下载指定 targets 的直接输出(--remote_download_toplevel),或完成 构建 所必需的最少输出(--remote_download_minimal)。这样能大幅减少网络流量,也可能加快构建。
这个功能也有局限。按本文写作时的实现,BwtB 要求 remote cache 永不淘汰输出。下载集中到某个阶段可能拖慢构建。它也不支持指定下载其他输出,等等。和 dynamic execution 类似,BwtB 在使用得当时可以加快构建,但不能盲目启用。
补充话题:Build Event Service
Bazel 可以将 构建 结果(特别是 build event protocol (BEP))传输到build event service (BES)。根据服务的功能,这可以带来很多好处。
以下列出了各种 BES 产品(包括 BuildBuddy!)提供的一些优势:
- 轻松分享构建日志
- 查看历史 构建 数据,包括聚合和趋势
- 查看终端未展示的细节,例如无需使用
--announce_rc,就能查看所用的全部命令行 flag,以及设置的全部环境变量。 - 查看action时序数据(同
--experimental_generate_json_trace_profile) - 可视化查询
- 查看错误和测试日志
- 下载 action 输出
- 查看 remote cache 统计数据
- 查看remote execution相关数据
- actions 已执行列表
- 各个 action 详细信息(例如命令行参数、环境变量、平台属性、计时数据以及下载输入和输出)
Flags
设置 --bes_backend 标志会导致 Bazel 通过 gRPC(S) 连接到 BES 后端并将 构建 结果流式传输到它。设置 --bes_results_url 标志会导致 Bazel 将正在进行的 构建 的 BES UI 的 URL 输出到终端。
使用 BES,Bazel 将上传BEP中引用的所有文件,除非--experimental_build_event_upload_strategy=local8 已设置。或者,如果您设置 --incompatible_remote_build_event_upload_respect_no_cache9,并且 actions 标记为no-cache/no-remote-cache-upload/no-remote-cache/no-remote,那么这些 actions 的输出仍将被排除在上传之外。
--bes_timeout flag 控制 构建 和测试结束后,Bazel 等待 BES 上传完成的时间。默认没有超时,这可能不符合你的需求。如果保留默认值,可以考虑调整 --bes_upload_mode flag:它控制 Bazel 是否等待 BES 上传完成才结束 构建(默认行为),或让上传在后台继续。
--bes_header 10 flag 让 Bazel 在发送到 BES 后端的请求中附加额外 header。其行为与 --remote_header 相同。
就这样吧,现在
希望有了这些信息,Bazel 的 remote caching 和 remote execution 功能就不那么神秘了。
脚注
Julio 写了一篇很好的 spawn strategy 概述,强烈建议阅读。↩
除非设置了
--noremote_accept_cached。请参阅 标志部分 。 ↩除非 action 标记为
no-remote-cache-upload,或者可能设置了--noremote_upload_local_results。请参阅 标志部分 。 ↩除非设置了
--noremote_accept_cached。 ↩如果设置
--experimental_remote_execution_keepaliveflag,Execution.Execute和Execution.WaitExecute调用会以更复杂的方式考虑--remote_timeout和--remote_retries的值。即使设置了这个 flag,目标仍是不限制执行时间,因为不同 action 所需的执行时间可能相差很大。↩但警告:设置
--experimental_build_event_upload_strategy=local将阻止上传一些好东西,例如计时配置文件或测试日志。 ↩
本文译自 BuildBuddy 博客,原文及译文依据 MIT 许可 提供。Copyright (c) 2020-present Iteration, Inc.