Bazel 本地执行场景下的远程缓存命中率排查:从命中统计到执行日志对账的完整调试指南
【免费下载链接】bazela fast, scalable, multi-language and extensible build system项目地址: https://gitcode.com/GitHub_Trending/ba/bazel
本指南聚焦一个非常具体的问题:当构建与测试在本地执行、但结果写入远程缓存时,如何确认远程缓存被有效利用,并系统性地排查缓存未命中(cache miss)。你将掌握如何阅读 Bazel 的进程统计行、如何甄别远程缓存通信故障、如何借助执行日志(execution log)与execlog工具对账两次构建,以及如何让"写缓存"与"读缓存"的两次 Bazel 调用真正共享命中。
本文以 cache-local.mdx 为骨架,结合 cache-remote.mdx、caching.mdx 及仓库内execlog、diskcache工具的源码与说明展开。
适用前提:本地执行 + 远程缓存
本指南面向以下场景:
- 你已有一个能够在本地成功构建的 build 或 test;
- 该构建已配置使用远程缓存(例如通过
--remote_cache或--disk_cache); - 你希望确认远程缓存正在被有效利用,而不是每次构建都在本地重跑。
需要特别区分的是:远程执行(remote execution)与本地执行(local execution)走的是两条不同的路径。远程执行会把 action 分发到远端机器,构建失败可能直接导致构建整体失败;而本地执行即使无法读写远程缓存,构建依然可以成功,缓存问题只会静默地表现为"命中率低于预期"。这正是本地执行场景下缓存问题更难发现的原因,也是本文需要单独成篇的价值所在。
远程执行场景的完整排查流程见 Debugging Remote Cache Hits for Remote Execution,其中介绍的方法(检查命中率、对比执行日志、跨机器验证等)对本地执行同样适用;本文在继承这些方法的基础上,补充本地执行特有的排查要点。
第一步:检查缓存命中率
成功命中远程缓存的 action 会出现在 Bazel 运行日志的进程统计行(status line)中。在本地执行 + 远程缓存场景下,典型输出如下:
INFO: 7 processes: 3 remote cache hit, 4 linux-sandbox.含义解读:
- 共 7 个待执行的 action;
- 其中 3 个命中了远程缓存(显示为
remote cache hit),结果直接从缓存取回,无需本地执行; - 4 个未命中,回退到本地执行策略
linux-sandbox(即在本机沙箱中运行); - 数字
7大致对应本次构建中的 action 数量。
几点需要特别注意:
- 本地缓存命中不计入该统计。如果你上一次构建已经在本地留下产物,Bazel 会先复用本地结果,这部分不会出现在"remote cache hit"中。因此,若想纯粹地评估远程缓存命中率,应先用
bazel clean清掉本地缓存再运行。 - 如果统计显示
0 processes或数字明显低于预期,同样建议执行bazel clean后再跑一次构建/测试命令,排除本地缓存对统计的干扰。 - 远程执行场景的统计行格式与之类似,例如
INFO: 11 processes: 6 remote cache hit, 3 internal, 2 remote.,其中remote表示 action 在远端执行、internal表示创建符号链接之类的微小内部 action,可忽略。 - 不要用 action 的 stdout/stderr 来估算命中率。远程缓存会额外存储每个 action 的 stdout 与 stderr(见 caching.mdx),因此某条日志被打印出来不代表该 action 真的被重新执行过。
第二步:确保与远程缓存端点的通信成功
本地执行下,无法与远程缓存通信不会导致构建失败,这是与远程执行最关键的差异。因此排查的第一件事,就是检查 Bazel 输出中是否有如下警告:
WARNING: Error reading from the remote cache:或
WARNING: Error writing to the remote cache:这类警告之后通常会紧跟具体的连接错误信息,例如:
- 远程缓存端点地址拼写错误(mistyped endpoint name);
- 凭据配置错误(incorrectly set credentials);
- 网络不通或代理配置问题。
定位到错误后逐一修复即可。如果错误信息不够详细,可追加--verbose_failures标志让 Bazel 输出更完整的失败细节。
本地执行 vs 远程执行:失败模式的差异
在远程执行中,如果 Bazel 无法与远端端点通信,构建会直接失败——因为 action 必须被分发到远端才能执行;而在本地执行中,action 本来就要在本机运行,缓存只是"锦上添花",因此失败被降级为警告。理解这一点后,排查时就要主动在输出中搜索警告,而不是等到构建失败才意识到缓存可能有问题。
第三步:参照远程执行的通用排障清单
在确认通信正常后,如果命中率仍然低于预期,需要按 cache-remote.mdx 中的步骤系统排查:
3.1 在同一台机器上复现命中
- 运行你期望填充缓存的构建/测试命令。第一次在全新机器栈上运行通常不会有远程缓存命中,这是正常现象;
- 执行
bazel clean清除本地缓存,避免本地命中掩盖远程命中的真实情况; - 再次运行相同的构建/测试命令;
- 检查
INFO行。如果除remote cache hit和internal外再无其他进程,说明缓存已被正确填充并被访问; - 若两次运行的 action key 不一致,大概率是构建中存在非密闭(non-hermetic)因素。此时进入第 3.2 节,通过执行日志定位差异。
3.2 用执行日志定位非密闭因素
生成两次运行的执行日志:
bazel clean bazel <var>--optional-flags</var> build //<var>your:target</var> --execution_log_compact_file=/tmp/exec1.log然后对比两次运行的日志(对比方法见下文"对比执行日志"一节)。确保两个日志文件中的 action 完全一致,任何差异都提示了两次运行之间发生了什么变化,据此修正构建配置。
3.3 检查 action 是否可缓存
如果两次运行的 action ID 完全一致却依然没有命中,可能是配置层面阻止了缓存。先检查执行日志中每个 action 的cacheable字段是否都为true:
- 若某 action 的日志中不包含
cacheable,说明对应规则可能在BUILD文件中带有no-cachetag; - 可通过日志中的
mnemonic和target_label字段定位该 action 来源于哪个规则。
3.4 检查缓存读取是否被显式关闭
如果 action 完全一致且cacheable为 true 却仍无命中,检查命令行中是否包含--noremote_accept_cached——该标志会为本次构建禁用缓存查找。
如果难以确认实际生效的命令行,可以使用 Build Event Protocol 获取规范化命令行(canonical command line):
- 在 Bazel 命令中追加
--build_event_text_file=/tmp/bep.txt,生成文本格式的 BEP 日志; - 打开该日志,搜索
structured_command_line消息,其中command_line_label: "canonical"对应的消息会列出展开后的全部选项; - 在其中搜索
remote_accept_cached,确认其是否被设置为false; - 若为
false,进一步确定它是在命令行还是 bazelrc 配置文件中被设置的。
第四步:让"读缓存"的 Bazel 调用也能命中
很多团队采用"CI 写缓存、开发者读缓存"的分工模式。这类只读缓存的 Bazel 调用与写缓存的调用往往有着不同的命令行配置,因此需要额外确认以下几点:
- 确保
--remote_cache标志已设置,且输出中没有相关警告(见第二步); - 确保读缓存的调用构建的目标与写缓存的调用一致——目标不一致自然谈不上共享命中;
- 按照 确保跨机器缓存 的步骤,验证从"写缓存机器"到"读缓存机器"的缓存共享是否成立。
跨机器验证步骤
对构建做一个小改动,避免命中已有缓存;
在第一台机器上运行:
bazel clean bazel ... build ... --execution_log_compact_file=/tmp/exec1.log在第二台机器上运行(确保包含第 1 步的改动):
bazel clean bazel ... build ... --execution_log_compact_file=/tmp/exec2.log对比两份执行日志。若日志不一致,排查两台机器的构建配置差异,以及宿主机环境属性(如
$PATH、环境变量)是否泄漏进了构建。
第五步:对比执行日志,定位缓存未命中的根源
执行日志是排查缓存问题最有力的工具。每条记录描述了一个 action 的输入(不仅是文件,还包括命令行参数、环境变量等)与输出,因此通过检查日志可以揭示某 action 为何被重新执行。
执行日志的三种格式
执行日志可通过以下三个标志之一生成,格式互通:
| 标志 | 格式 | 说明 |
|---|---|---|
--execution_log_compact_file | compact(紧凑) | 官方推荐,文件体积小、运行时开销极低 |
--execution_log_binary_file | binary(二进制) | 与 compact 同源的二进制编码 |
--execution_log_json_file | JSON | 便于其他工具直接消费 |
三种格式之间可以通过//src/tools/execlog:converter工具互相转换(详见 execlog/README.md),例如二进制转 JSON:
bazel build //src/tools/execlog:converter bazel-bin/src/tools/execlog/converter \ --input binary:/tmp/binary.log --output json:/tmp/json.log用 execlog parser 生成可 diff 的文本
对比两份日志(例如分别保存为/tmp/exec1.log和/tmp/exec2.log)时,直接 diff 二进制日志并不现实。仓库自带解析工具//src/tools/execlog:parser(定义于 execlog/BUILD,主类com.google.devtools.build.execlog.ExecLogParser),构建并运行:
bazel build //src/tools/execlog:parser bazel-bin/src/tools/execlog/parser \ --log_path=/tmp/exec1.log \ --log_path=/tmp/exec2.log \ --output_path=/tmp/exec1.log.txt \ --output_path=/tmp/exec2.log.txt该工具会把两份日志都转换为人类可读的文本,并将第二份日志中的 action 重新排序以匹配第一份的顺序(依据首个输出文件是否相同进行匹配,未匹配的 action 排到末尾),使 diff 更有意义。之后用你习惯的文本 diff 工具对比/tmp/exec1.log.txt与/tmp/exec2.log.txt即可。
其他实用选项(见 execlog/README.md):
--restrict_to_runner="linux-sandbox":只输出在指定执行策略下运行的 action,便于聚焦排查;- 处理超大日志时可加大 JVM 堆,如
--jvm_flag=-Xmx4g; - 注意:由于 Bazel 本身具有不确定性,不同次运行产生的日志中 action 顺序可能不同;重排序便于 diff,但也可能打乱第二份日志的逻辑顺序。
本地执行场景的典型非密闭因素
对比日志时,以下因素经常导致 action key 变化或缓存污染,需要重点检查:
- 环境变量泄漏进 action:action 定义中包含环境变量,不同机器
$PATH不同就可能导致无法共享命中。只有通过--action_env显式白名单化的环境变量才会进入 action 定义(见 caching.mdx)。如果命中率异常偏低,检查环境中是否存在旧的/etc/bazel.bazelrc(Bazel 的 Debian/Ubuntu 包曾通过它注入包含$PATH的白名单); - 工作区外的工具未被追踪:Bazel 不追踪工作区之外的工具,例如使用
/usr/bin/下的编译器时,两台机器编译器版本不同会产生相同 action hash 但不同输出,造成错误的"命中"(反过来也会掩盖真实差异); - 构建期间输入文件被修改:可能向缓存上传无效结果,可启用
--experimental_guard_against_concurrent_changes进行变更检测;一般情况下应避免在构建过程中修改源文件。
辅助手段:磁盘缓存及其垃圾回收
除了网络远程缓存,Bazel 还支持把文件系统目录当作缓存(--disk_cache),适用于切换分支、同一项目的多个 checkout 等场景。它在排查本地执行缓存问题时也非常有用——你可以把磁盘缓存当作"本地可检视的远程缓存"来观察命中行为:
build --disk_cache=<var>path/to/build/cache</var>相关要点:
不带路径运行时使用默认位置
<outputUserRoot>/cache/disk(与 repository cache 同级,输出用户根目录见 user-manual);build --nodisk_cache可显式禁用;路径支持
~别名(替换为当前用户主目录),便于在项目提交的.bazelrc中为所有开发者统一开启;从 Bazel 7.4 起,可用
--experimental_disk_cache_gc_max_size与--experimental_disk_cache_gc_max_age限制缓存总大小或单个条目存活时长,Bazel 会在构建空闲期自动执行垃圾回收,空闲等待时长由--experimental_disk_cache_gc_idle_delay控制(默认 5 分钟);需要按需手动回收时,可使用仓库自带的独立工具(见 diskcache/README.md):
bazel run //src/tools/diskcache:gc -- \ --disk_cache=/path/to/disk/cache \ --max_age=7d --max_size=2G--max_age与--max_size至少需指定一个。
缓存机制背景:命中背后的两个存储
理解命中率之前,有必要了解远程缓存的数据组织方式(详见 caching.mdx)。Bazel 把构建拆分为离散的action,每个 action 显式声明输入、输出文件名、命令行与环境变量。远程缓存存储两类数据:
- action cache:action 哈希到 action 结果元数据的映射;
- 内容寻址存储(CAS):输出文件本身。
HTTP 缓存协议下,action 结果元数据存放在/ac/路径下,输出文件存放在/cas/路径下,Blob 通过 PUT 上传、GET 下载(HTTPS、grpc、grpcs协议同样受支持)。一条"命中"意味着:某个 action 的输入哈希能在 action cache 中查到结果,且对应的输出文件已在 CAS 中可获取——任何输入(文件、命令行、环境变量)的变化都会改变 action 哈希,从而错过命中。这正是本文所有排查手段围绕的核心模型。
总结:一份可照做的排查清单
| 步骤 | 检查项 | 关键命令/标志 |
|---|---|---|
| 1 | 查看进程统计行,确认remote cache hit数量 | 观察INFO: N processes: ...行 |
| 2 | 确认与远端通信成功 | 搜索WARNING: Error reading/writing to the remote cache,必要时加--verbose_failures |
| 3 | 同一机器复现命中 | bazel clean后重跑 |
| 4 | 检查 action 是否可缓存 | 执行日志中的cacheable、mnemonic、target_label |
| 5 | 确认未被--noremote_accept_cached关闭 | --build_event_text_file=/tmp/bep.txt+ 查找structured_command_line中remote_accept_cached |
| 6 | 让读缓存调用可命中 | 确认--remote_cache、相同 targets、跨机器日志一致 |
| 7 | 对比执行日志定位差异 | --execution_log_compact_file+//src/tools/execlog:parser |
按照上述顺序排查,绝大多数"本地执行 + 远程缓存命中率低"的问题都能被定位到具体环节:是端点配置错误、非密闭输入导致的 action key 漂移,还是规则被标记为不可缓存。最后记得:本地缓存命中不计入统计,评估远程命中率前先bazel clean。
【免费下载链接】bazela fast, scalable, multi-language and extensible build system项目地址: https://gitcode.com/GitHub_Trending/ba/bazel
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考