Bazel:从可重复构建到远程执行的工程化落地
一个同时包含 Java 服务、Go 工具、TypeScript 控制台和 protobuf 模型的大仓,往往不是在第一次全量构建时暴露问题。真正棘手的是另一组现象:开发机增量构建只要几十秒,干净 CI 却要十几分钟;同一个提交在两台机器上生成的制品摘要不同;某个测试本地始终通过,换到远程执行器就找不到 /usr/bin/python;为了提速接入共享缓存后,外部贡献分支竟然可以写入内部构建制品。
这些现象不能靠“再加一层缓存”一起解决。Bazel 把仓库中的源文件和规则组织成目标图,再把已配置目标展开为可执行 action。只有 action 的输入、工具、命令、环境和输出都能被稳定描述,沙箱才有机会揭露隐式依赖,远程缓存才有可信的 key,远程执行器也才能在另一台机器上复现结果。Bazel 因此既适合大型 Monorepo,也适合对可重复构建、跨语言工具链或远程执行有明确要求的多仓项目;仓库很小、构建脚本已足够稳定且没有共享加速诉求时,引入规则生态和迁移治理反而可能得不偿失。
先用 Bazelisk 固定构建器,而不是固定每台机器
Bazel 安装页推荐在 Windows、macOS 和 Ubuntu Linux 上使用 Bazelisk。Bazelisk 是 Bazel 启动器:它读取仓库声明的版本,按需下载对应 Bazel,再把参数原样交给 Bazel。团队由此升级仓库中的一个文本文件,而不是要求所有开发者和 runner 同时手工替换二进制。
macOS 可以使用 Homebrew:
brew install bazelisk
bazelisk bazeliskVersionWindows 可以使用 Windows Package Manager:
winget install Bazel.Bazelisk
bazelisk bazeliskVersion已经具备 Node.js 的跨平台环境也可以从 npm 安装:
npm install -g @bazel/bazelisk
bazelisk bazeliskVersionLinux 还可以从 Bazelisk Releases 下载与 CPU 架构匹配的二进制,校验发布摘要后放入受管的 PATH。当前 npm latest 的 @bazel/bazelisk 为 1.28.1,采用 Apache-2.0 许可证;Bazel 9.1.0 是当前 9.x LTS 发布线的稳定版本,Bazel 源码同样采用 Apache-2.0。Bazel 安装包还可能捆绑 OpenJDK 和其他不同许可证组件,企业重分发前应运行 bazel license 并保留对应许可证与源码入口,不能只看主仓库许可证。
企业网络通常需要把 Bazelisk 下载入口和 Bazel 发布二进制同步到制品代理,再用 BAZELISK_BASE_URL 指向内部镜像;代理认证放在受保护的凭据文件或 CI secret 中,不写入仓库 URL。版本镜像不仅要保存二进制,还要保存官方摘要、签名和目标平台信息,避免同一个版本标签在不同 CPU 或操作系统上指向未经审查的文件。
安装成功只证明启动器在 PATH 中。仓库根目录还需要 .bazelversion 固定一个经过团队验证的精确发布版本:
9.1.0示例版本用于复现实验,真实项目应从 Bazel Releases 选择仍受团队规则和语言规则集支持的版本,并通过升级分支修改。不要提交 latest、rolling 或 9.x:这些浮动标识会让同一提交在不同时间下载不同构建器。Bazelisk 的版本选择顺序还允许 USE_BAZEL_VERSION 覆盖仓库声明,因此 CI 应打印最终版本并限制该变量的来源:
bazelisk version
bazelisk info release预期能看到仓库锁定的 Bazel release。若开发机得到另一个版本,依次检查当前目录是否处在仓库根目录之下、父目录是否存在意外的 .bazelversion、环境变量是否覆盖,以及命令实际解析到 bazel 还是 bazelisk。CI 不应静默回退到 runner 预装的 Bazel。
用 MODULE.bazel 建立依赖根
Bazel 9 已经移除通过 WORKSPACE 引入外部依赖的旧路径,Bzlmod 成为外部依赖入口;外部依赖总览解释了旧系统在传递依赖、菱形版本解析和仓库可见性上的限制。新仓库从 MODULE.bazel 开始:
module(
name = "build_lab",
version = "0.1.0",
)name 是模块身份,发布后不应随意变化;version 是模块元数据,并不会替代应用制品版本。Bazel 8.6.0 与 9.1.0 起,module.compatibility_level 和 bazel_dep.max_compatibility_level 已被弃用并成为 no-op,继续填写不会建立 major 隔离。模块维护者应通过清晰的迁移错误、兼容发行策略和消费者验证管理破坏性变化。需要规则集时,通过 bazel_dep 声明来自注册表的直接依赖:
bazel_dep(name = "rules_java", version = "REPLACE_WITH_APPROVED_VERSION")占位值必须在合并前换成 Bazel Central Registry 中存在且经过兼容验证的版本。MODULE.bazel.lock 记录模块解析和扩展结果;团队应提交它,并让 CI 检查非预期漂移。锁文件只会逐步收录当前 invocation 实际用到的依赖和 module extension 结果,因此更新作业要覆盖受支持的操作系统、CPU 与目标平台,不能在单台 Linux runner 构建一个 target 后就宣布锁文件完整。Bazel lockfile支持 --lockfile_mode=error 阻止构建偷偷改锁文件,依赖升级作业则显式使用更新模式并提交差异:
bazelisk mod deps --lockfile_mode=update
bazelisk mod graph
bazelisk build --lockfile_mode=error //...error 模式在模块解析信息缺失或过期时失败,不修改锁文件,也不在解析阶段发起网络请求;但声明自己可复现的 module extension 仍可能联网,并承诺对相同输入生成相同结果。受限网络不能只依赖这个模式,还要审查 extension 的下载入口、摘要和离线缓存行为。
模块扩展 use_extension、use_repo 解决的是“根据模块图生成外部仓库”,而不是绕过依赖审查的脚本入口。扩展若下载二进制、读取环境变量或探测宿主机,同样会影响可重复性和供应链边界。团队应锁定 URL 与摘要、限制可访问域名、保留许可证和来源清单,并用 bazel mod graph、bazel mod explain 追踪某个模块为何进入图中。
Package、Target 和 Action 是三层不同对象
Bazel 构建概念把含有 BUILD 或 BUILD.bazel 的目录定义为 package。package 是声明和可见性的边界;target 是 package 中具有唯一 label 的文件、规则或 package group;action 则是分析完成后真正执行的命令步骤。//service/order:server 表示仓库内 service/order package 的 server target,并不表示“构建整个目录”。
这三层不能混用:
修改 BUILD.bazel 中的 visibility 会改变谁能依赖 target,不会直接创建 action。一个 target 可以展开为多个编译、链接或代码生成 action。同一 target 在不同 CPU、编译模式或 feature 下会形成不同的 configured target,并产生不同 action。
action 的 key 由声明输入、工具、命令、相关环境和配置共同决定;目录里“碰巧存在”的文件不应成为输入。
建立一个没有第三方规则依赖的实验仓库,目录如下。命令中的 shell action 适合 Linux、macOS 或 WSL;原生 Windows 项目应使用支持 Windows toolchain 的语言规则,而不是把 Bash 假装成跨平台工具链。
build-lab/
├─ .bazelversion
├─ MODULE.bazel
└─ app/
├─ BUILD.bazel
└─ input.txtapp/input.txt:
BUILD_GRAPH_OKapp/BUILD.bazel:
genrule(
name = "message",
srcs = ["input.txt"],
outs = ["message.txt"],
cmd = "cat $(location input.txt) > $@",
visibility = ["//visibility:public"],
)执行:
bazelisk build //app:message
cat bazel-bin/app/message.txt预期第二条命令输出 BUILD_GRAPH_OK。日志中出现 Build completed successfully 只证明请求完成,真正的能力证据还包括 bazel-bin/app/message.txt 存在、内容匹配输入、第二次构建没有重复执行未变化的 action。失败时先运行 bazelisk info bazel-bin 获取真实输出目录,不要把工作区符号链接位置写进脚本。
用反向实验抓住“开发机能过”的隐式依赖
在仓库根目录创建一个没有被规则声明的 secret.txt:
UNDECLARED_HOST_FILE把 cmd 临时改成:
cmd = "cat secret.txt > $@",再执行:
bazelisk clean
bazelisk build --spawn_strategy=sandboxed --sandbox_debug --verbose_failures //app:message预期 action 失败,并在命令或 stderr 中出现 secret.txt 不存在一类证据。失败不是沙箱“少挂载了文件”,而是规则没有把这个输入放入 srcs、deps、tools 或工具链。修复方式是声明真实依赖,例如把文件放进 package 并加入 srcs,而不是追加绝对路径、关闭沙箱或复制整个仓库给 action。
沙箱说明强调沙箱通过限制 action 可见和可写的文件来发现未声明输入,也保护宿主输出不被动作任意修改。它不自动等于完全密封:规则仍可能调用 PATH 中的宿主工具、读取显式透传的环境变量、访问网络,具体隔离能力还受操作系统和执行策略影响。可重复构建要继续完成三件事:把编译器和生成器建模为 toolchain;只传入确实影响输出且值稳定的环境;让依赖下载发生在受摘要校验的仓库规则或模块扩展阶段,而不是普通 action 中临时联网。
反例修复后再做交叉验证:
bazelisk build --spawn_strategy=sandboxed //app:message
bazelisk build --spawn_strategy=local //app:message
sha256sum bazel-bin/app/message.txt在两种策略、干净 runner 和代表性开发机上,输出摘要应由相同输入决定。若只有本地策略通过,检查宿主工具、绝对路径和环境泄漏;若两边都通过但摘要不同,检查时间戳、随机数、文件遍历顺序、压缩归档元数据和非确定性代码生成器。一次命中不能证明 hermeticity,跨机器与冷构建的稳定摘要才接近有效证据。
query、cquery 和 aquery 分别回答三类问题
大仓排障最常见的误区,是看到“依赖图”三个字就只会运行一种 query。三个入口处在不同阶段:
bazelisk query 'deps(//app:message)'
bazelisk cquery 'deps(//app:message)'
bazelisk aquery //app:messagequery 读取未配置目标图,适合回答 package 和 target 之间声明了什么、谁反向依赖某目标、某次变更可能触达哪些标签。Query guide中的 deps、rdeps、somepath、kind 和 attr 是建立影响面工具的基础。
cquery 在分析阶段之后读取 configured target,能看到 select()、平台、feature 和构建选项生效后的依赖。一个目标“声明上依赖 A,Linux release 实际选择 B”时,要查 cquery。cquery 文档也提醒它会执行加载和分析,成本高于 query。
aquery 读取 action graph,能看到 action mnemonic、输入、输出和命令行。它回答的是“为什么执行这个编译步骤”“哪个参数进入了编译器”“输出由哪个 action 生成”。aquery 文档提供按 mnemonic、inputs 和 outputs 过滤的表达式。
例如定位某个生成器为何读取配置:
bazelisk aquery 'inputs(".*config.*", //service/order:package)'定位从 API target 到某个基础库的声明路径:
bazelisk query 'somepath(//service/api:server, //lib/crypto:core)'不能直接把 query rdeps 的结果当成“必须测试的精确集合”。宏、配置分支、运行时加载、测试数据和外部系统契约可能不在静态反向依赖中。影响面计算需要把目标图、配置矩阵、代码所有权和历史失败数据组合起来;高风险基础规则、发布制品和安全边界保留周期性全量验证,避免 affected 计算错误造成漏测。
本地缓存先证明 key 可信,再接共享服务
Bazel server 在同一 output base 中维护增量状态,本地 action cache 会复用已完成 action。磁盘缓存则能跨工作区或分支保存 action result 与内容寻址制品:
bazelisk build --disk_cache="$HOME/.cache/bazel-build-lab" //app:message
rm -f bazel-bin/app/message.txt
bazelisk build --disk_cache="$HOME/.cache/bazel-build-lab" //app:message第二次构建应从缓存恢复输出,而不是重新执行 action。rm bazel-bin/... 只用于这个实验;日常不要直接修改 Bazel 输出树。磁盘缓存必须设置容量和保留策略,远程缓存文档也提供磁盘 cache GC 选项。缓存目录不应进入仓库、CI artifact 或备份默认集合,因为其中可能含有编译制品、生成代码、测试日志和受许可限制的依赖。
远程缓存把同一模型放到团队共享服务:action cache 把 action digest 映射到结果元数据,CAS 以内容摘要保存文件。典型配置可以写入仓库 .bazelrc,但 endpoint 与认证策略要区分开发机、可信 CI 和外部 PR:
build:remote-cache --remote_cache=grpcs://cache.example.invalid
build:remote-cache --remote_timeout=60s
build:remote-cache-readonly --config=remote-cache
build:remote-cache-readonly --remote_upload_local_results=false可信主干 CI 可使用 --config=remote-cache 读写;普通开发机和不可信 PR 使用 --config=remote-cache-readonly 只读,或者完全禁用内部 endpoint。只有经过审核、在隔离 runner 上执行且输出可重复的构建身份才拥有写权限。否则攻击者可以写入与合法 action key 对应的恶意制品,或通过缓存读取内部二进制、源码生成物和 action 的 stdout/stderr。
认证优先使用短期工作负载身份和 Bazel --credential_helper,而不是在 URL、.bazelrc 或 --remote_header 中嵌入静态 token。命令行参考定义了按域名限定 helper 的形式:
common --credential_helper=cache.example.invalid=%workspace%/tools/cache-credential-helper
common --credential_helper_timeout=10shelper 文件可以进入仓库,但它只能从 CI 身份提供者或本机受管凭据存储取得短期令牌,不能包含令牌本身。缓存服务同时需要 TLS、租户隔离、读写分权、审计、配额、生命周期和整库失效入口。远程缓存按摘要组织,污染后通常无法可靠地只删除“某个项目某次构建”的所有对象,因此应预先设计命名空间轮换或整库清空方案。
credential helper 必须实现 Bazel 的 helper 协议并可被当前平台执行;路径可以是绝对路径、PATH 中的命令或 %workspace% 相对路径。它不仅可服务远程缓存,还可服务 repository 下载、远程执行和 Build Event Service;按域名绑定 helper,避免一个高权限令牌被发给无关端点。helper 超时默认是 10s,会直接让 invocation 失败;响应未给出过期时间时,Bazel 默认缓存凭据 30m,因此短期凭据需要同时校准 --credential_helper_timeout 与 --credential_helper_cache_duration。helper 的标准输出是协议响应且含授权材料,不能进入普通调试日志或 BEP artifact。
认证拒绝必须被当成安全反证,而不是“网络偶发告警”。用一个没有 cache read 权限的测试身份运行相同 target:
bazelisk build --config=remote-cache-readonly //app:message \
--credential_helper=cache.example.invalid=%workspace%/tools/deny-credential-helper \
--output_base=/tmp/bazel-deny-cache-test受控的 fail-closed 验证作业应以 UNAUTHENTICATED、PERMISSION_DENIED 或 HTTP 401/403 一类证据失败,并且服务端审计能关联该身份、endpoint 和拒绝动作。独立 output_base 用来排除本地 action cache 干扰;Windows runner 应换成受控临时目录。若命令仍在本地执行并成功,这不是授权通过,而是当前缓存链按 fail-open 降级,流水线必须把相应远端错误事件提升为失败或由网关提供强制拒绝门禁。--remote_local_fallback 主要控制远程执行失败后的本地 fallback,不能把它误当成远程缓存认证的通用 fail-closed 开关。
缓存未命中时不要只看一行进度。先比较两次执行日志或 aquery,确认目标、配置、工具链、环境和 action 输入一致;再区分“action key 不同”与“相同 key 在服务端不存在”。常见根因包括:
把开发机绝对路径、用户名、临时目录或不稳定环境变量写进命令。通过 /usr/bin 调用未声明工具,版本变化却没有进入 key,最危险时会产生错误命中而非未命中。构建期间源文件被其他进程修改,上传了与分析时输入不一致的结果。
读缓存和写缓存的 invocation 构建了不同 target 或使用不同 .bazelrc。cache endpoint、instance name、TLS、代理或凭据失败,被配置成警告后悄悄退回本地执行。
带有秘密、签名密钥、宿主状态或不可重复输出的 target 应使用 no-remote-cache 标签并单独治理。它只是阻止远程缓存,不会自动消除日志泄密、远程执行或本地残留,规则仍需限制输入与输出。
远程执行不是更大的远程缓存
远程缓存仍在本机执行缺失 action;远程执行把 action 的输入与命令发给执行服务,由远端 worker 执行后把结果写入 CAS。它能把大量独立编译和测试 action 分散到 worker,也能统一执行环境,但收益取决于 action 粒度、并行度、上传下载量和关键路径。一个不可拆分的十分钟链接 action,不会因为增加一百台 worker 变成六秒。
远程执行总览使用 Remote Execution API 的 gRPC 协议;真正接入前先满足以下不变量:
所有 action 输入、工具和预期输出都已声明,宿主机上的 SDK、PATH、home 目录和 daemon 不再是隐式依赖。execution platform 明确 worker 的操作系统、CPU、容器镜像或属性;target platform 明确制品运行平台,两者不能混为一谈。编译器、链接器、代码生成器通过 toolchain 解析,不依赖开发机探测结果。远程执行规则指南说明了 host、execution 与 target platform 的差异。
测试没有固定端口、共享数据库、宿主路径和跨 action 可变状态;需要网络或特权能力的测试被显式隔离。worker 镜像、规则集和 Bazel 版本有兼容矩阵,镜像按 digest 发布并保留上一基线。
平台与工具链必须形成可查询的解析链,而不是只写在 runner 镜像说明里。target platform 描述制品运行约束,execution platform 描述 action 在哪里执行;规则声明 toolchain type,Bazel 再用 target 约束、execution 约束、已注册平台和已注册 toolchain 选择具体实现。同一 target 的不同 configuration 或 execution group 可以得到不同的 execution platform 和 toolchain,不能用一次 Linux 构建结果替代整个矩阵。
# platforms/BUILD.bazel
platform(
name = "linux_x86_64_worker",
constraint_values = [
"@platforms//os:linux",
"@platforms//cpu:x86_64",
],
exec_properties = {
"container-image": "docker://registry.example.invalid/build@sha256:REPLACE_ME",
"Pool": "linux-x86_64",
},
)exec_properties 对 Bazel 来说是不透明的键值,只有远程执行服务知道 Pool 或镜像字段的含义;字段拼错可能表现为没有匹配 worker、排队不结束,或服务端忽略配置后落入默认池。规则中的 exec_compatible_with 约束执行平台,target_compatible_with 约束目标平台,toolchain 自身同时声明二者的兼容面。遇到“本地选中编译器 A、远端却选中 B”时,保存以下解析证据:
bazelisk cquery //app:message --output=starlark \
--starlark:expr='str(target.label)'
bazelisk build //app:message \
--toolchain_resolution_debug='.*' \
--extra_execution_platforms=//platforms:linux_x86_64_worker调试输出应说明候选 execution platform 为何被接受或拒绝、每个 toolchain type 最终选择了哪个实现。生产 CI 不应长期保留全量 debug,它可能泄露内部 label、平台属性和仓库结构;只在受控排障作业中采集,并对失败 target 限定过滤表达式。
远程执行配置由服务商和部署实现决定,常见入口如下:
build:remote-exec --remote_executor=grpcs://executor.example.invalid
build:remote-exec --remote_cache=grpcs://cache.example.invalid
build:remote-exec --remote_timeout=120s
build:remote-exec --jobs=auto--jobs 接受整数、auto,也接受 HOST_CPUS、HOST_RAM 及其运算表达式;这里使用 auto 让 Bazel 按宿主资源选择默认值。它是并发上限,不是越大越快。远端队列、worker CPU/内存、CAS 带宽和本地下载都可能成为瓶颈。上线时按 target 群组灰度:先让 hermetic 的编译 action 远程执行,再处理测试和生成器;保留 --spawn_strategy=local 或独立 rc config 作为回退。回退成功并不代表远程服务是唯一根因,仍要用 action 状态区分排队、上传、执行、下载和本地 fallback。
凭据边界比缓存更宽。执行服务不仅接收制品,还可能接收源文件、生成器、测试数据和命令环境。高度敏感仓库需要专用 instance、区域约束、静态和传输加密、worker 临时盘销毁、访问审计与日志脱敏;外部 PR 不得继承能读取私有依赖、签名服务或生产网络的执行身份。一个共享 worker 能联网并持有云凭据时,沙箱内的测试代码就是潜在的凭据读取者。
用 BEP 和 Profile 解释时间花在哪里
“构建变快了”不是可运营结论。Bazel 至少提供两类互补证据:Build Event Protocol 描述一次 invocation 的目标、测试、制品、进度和结束状态;JSON Trace Profile 描述加载、分析、action、远程交互和关键路径耗时。
在 CI 中保存机器可读 BEP:
mkdir -p artifacts/bazel
bazelisk test //... \
--build_event_json_file=artifacts/bazel/build-events.json \
--profile=artifacts/bazel/profile.json.gzBuild Event Protocol用于程序化消费构建事件,避免解析会随 UI 变化的终端文本。BEP 可以进入内部构建分析服务,但上传前要检查 command line、环境、target 名、测试日志 URI 和制品路径是否泄露仓库结构或凭据;设置受控保留期和访问权限,不能把完整 BEP 当成普通公开 artifact。
JSON Trace Profile可以在 Trace Viewer 中查看,也可交给分析工具。诊断顺序应沿关键路径而不是按总 action 数量猜测:
loading 或 analysis 占比持续上升:检查 package 过大、宏展开、递归 glob、配置组合和规则实现。action 在本地执行很慢:检查单 action 粒度、工具自身性能、输入规模和 I/O。REMOTE_SETUP、上传或下载占据关键路径:检查 CAS 命中、制品大小、网络距离和输出下载策略。
worker 大量空闲但关键路径仍长:拆分串行 action 或消除依赖链,单纯增加并发没有价值。GC 或 Bazel server 启动占比异常:检查 runner 生命周期、内存和是否每步都丢弃 output base。
性能基线要比较同一 target 集、同一 Bazel 与规则版本、相同 runner 规格下的冷构建、增量构建和远程缓存命中构建。记录中位数与高分位趋势、关键路径、远程命中 action 比例、上传下载字节、队列时间和失败 fallback 比例。阈值来自仓库历史与交付 SLO,不用一个脱离仓库规模的“构建必须少于五分钟”覆盖所有团队。
CI 把版本、配置、缓存身份和证据绑在一起
一条可治理的 CI 链路可以分为四段:
# 1. 证明使用仓库锁定的构建器
bazelisk version
# 2. 证明模块锁文件没有被构建偷偷改写
bazelisk build --lockfile_mode=error //...
# 3. 测试并产出结构化证据
mkdir -p artifacts/bazel
bazelisk test --config=remote-cache //... \
--build_event_json_file=artifacts/bazel/build-events.json \
--profile=artifacts/bazel/profile.json.gz
# 4. 由发布系统从明确 target 取得制品
bazelisk cquery //release:bundle --output=files外部贡献分支把第三步换成只读 cache config,且不注入私有仓库、远程执行和发布凭据。可信主干也不要让所有 job 共享同一可写身份:预提交、主干、发布和依赖升级分别使用最小权限;发布签名在独立受审步骤完成,不把签名密钥作为普通 Bazel action 输入上传到共享 CAS。
CI 失败后保留 BEP、Profile、失败 action 的有限日志和 invocation ID,避免默认上传整个 output base。缓存可用性下降时,流水线是失败还是降级本地执行应由任务风险决定:普通验证可以降级并报警,发布构建若要求远程执行环境一致性,则不能在无人知晓的情况下回退到 runner 宿主机。
从已有构建系统迁移时保留双轨证据
迁移 Bazel 的高风险点不是写出第一个 BUILD.bazel,而是在较长时间内维护两套构建定义却无法判断结果是否等价。稳妥路径按可独立交付的垂直切片推进:
盘点现有入口、语言版本、包管理器 lockfile、代码生成、测试、制品、发布脚本和隐式宿主依赖,选一个边界清楚且有真实消费者的组件。固定 Bazelisk 与 Bzlmod 基线,只为该组件引入必要规则和 toolchain,不先转换整个仓库。让旧系统和 Bazel 从同一源提交生成制品,比较内容摘要、API/ABI、测试结果、许可证清单和运行行为。压缩包含时间戳时先做规范化比较,不能用“文件大小接近”代替等价证明。
在 CI 中把 Bazel 路径先设为非阻断,收集冷构建、增量构建、缓存命中、失败类型和维护成本;达到稳定趋势后再把该组件的 Bazel 构建设为阻断。消费者切换到 Bazel 产物后保留旧入口一个明确回退窗口。回退同时恢复旧制品来源、缓存 namespace 和发布脚本,不只把流水线命令改回去。一个切片稳定后再扩展相邻依赖,逐步收紧 package visibility 和 owner,避免根 package 变成所有代码都可依赖的公共垃圾场。
旧仓库若仍依赖 WORKSPACE,必须先在 Bazel 7 或 8 的兼容窗口完成 Bzlmod 迁移:Bazel 8 默认禁用 WORKSPACE,Bazel 9 已彻底移除其实现,到了 9.x 再加开关也无法恢复旧解析器。Bzlmod migration guide提供从 http_archive、仓库宏和自定义 repository rule 迁往 bazel_dep、module extension 的路径。双轨期要分别运行 Bzlmod 与旧入口,比较外部仓库、toolchain、生成制品和测试;确认所有规则集支持 Bzlmod 后先删除旧依赖定义,再升级到 Bazel 9。迁移时逐个核对 canonical repo name、repo mapping、patch、摘要、mirror、许可证和可见性;不能因为 target 能编译,就认为外部依赖身份与旧系统完全一致。
Monorepo 迁移还要处理包管理器边界。JavaScript、Python、Java 或 Go 的原生 lockfile 仍可能是依赖解析事实源,Bazel 规则负责把已锁定依赖映射为 target 和 action。禁止一部分任务由包管理器更新依赖,另一部分由 Bazel 模块扩展静默选择不同版本。每类依赖只指定一个 owner 和升级入口,并在 CI 中检查 lockfile、生成的仓库定义和 Bazel 模块锁是否同步。
排障要先判断失败停在哪一层
找不到 package 或 target
no such package 优先检查目录中是否有 BUILD/BUILD.bazel、父子 package 边界和 target pattern;no such target 再检查 label 的 package 与冒号后名称。运行:
bazelisk query //path/to/package:all
bazelisk query //path/to/package:target不要通过删除子目录 BUILD.bazel 来“让父包看到文件”,这会改变整个可见性和 glob 边界。
本地成功,沙箱或远程失败
用 --sandbox_debug --verbose_failures 获取失败 action,再用 aquery 检查 inputs、tools 和 command。重点寻找绝对路径、PATH 工具、home 配置、固定端口、未声明动态库、大小写差异和平台二进制。修复规则或 toolchain,关闭 sandbox 只能作为定位对照,不能作为最终方案。
修改了文件却仍得到旧结果
先确认文件是否真的是 target 的传递输入:
bazelisk query 'somepath(//app:message, //app:input.txt)'
bazelisk aquery 'inputs(".*input.txt", //app:message)'若依赖未声明,补规则;若 action key 应变化却命中旧远程结果,立即切换只读或新 cache namespace,保存两次 action 证据并停止可疑 writer。不要用每次 bazel clean --expunge 掩盖错误 key,它会丢失增量状态,却不会修复共享缓存中的污染对象。
远程缓存一直未命中
确认命令没有认证或连接警告,读写端使用同一 endpoint、instance、target 和 config。比较 execution log、aquery 与 action environment,定位变化字段。若下载时间大于本地执行时间,对大输出 target 使用本地策略或调整下载模式;缓存不是强制收益,网络距离和 CAS 吞吐必须纳入选型。
远程执行排队或反复 fallback
从 Profile 区分 queue、remote setup、execution、download 和本地 fallback;从执行服务查看平台属性、worker 容量和拒绝原因。缺少匹配 worker 与 action 本身失败是两类问题。调整 --jobs、资源声明或平台池前,先确认关键路径是否真的具备并行空间。
模块解析、下载或锁文件失败
运行:
bazelisk mod graph
bazelisk mod explain MODULE_NAME
bazelisk fetch //path/to:target依次检查 registry/mirror、代理、CA、摘要、版本选择、module extension 和锁文件模式。不要把企业 CA 校验全局关闭,也不要为绕过失败把依赖改成未校验的临时 URL。离线或受限网络应使用受管 registry mirror、vendor 或制品代理,并保留来源与摘要。
成本和治理决定 Bazel 能否长期存在
Bazel 的成本不只是一组 runner。还包括规则和 toolchain 升级、BUILD 文件维护、缓存/CAS 存储与出口流量、远程执行 worker、BEP/Profile 存储、IDE 同步、迁移双轨以及平台团队支持。共享加速的收益也不能只算主干 CI:开发者等待时间、失败重跑、缓存污染事故和规则变更造成的全仓失效都应进入模型。
团队职责可以落到明确对象:
平台团队拥有 Bazel/Bazelisk 基线、.bazelrc 公共配置、远程 cache/exec、BEP 服务和升级演练。语言基础设施 owner 维护规则集、toolchain、依赖映射和代表性跨平台验证。业务团队拥有 package 边界、target、测试语义、运行时数据和代码所有权,不把所有 BUILD 维护推给平台团队。
安全团队定义外部 PR、缓存 writer、执行 worker、凭据 helper、敏感制品、网络出口和审计策略。发布团队拥有可发布 target、制品来源、签名、溯源和回滚入口。
升级 Bazel 或核心规则集时,先在代表性仓库矩阵运行冷构建、增量构建、沙箱反例、远程缓存读写、远程执行、IDE 同步和发布制品比较。Bazelisk 可以在升级分支固定候选版本;失败时恢复 .bazelversion、规则版本、worker 镜像和 cache namespace 的上一组合。只降 Bazel 而保留新 lockfile、规则生成物或新 worker 镜像,仍可能无法回退。
清理也要有边界:
# 删除当前 workspace 的普通构建输出并保留下载缓存的可能复用价值
bazelisk clean
# 停止当前 Bazel server
bazelisk shutdown
# 只在损坏、磁盘回收或隔离排障时使用,会删除更完整的本地状态
bazelisk clean --expunge共享缓存由服务端生命周期和 namespace 管理,不能在开发机上靠 clean --expunge 清除。迁移回滚后,删除只属于 Bazel 路径的临时 CI artifact、失效 credential、废弃 cache namespace 和 worker 镜像;保留 BEP/Profile 与制品证据到审计期限结束,再按策略销毁。
当一个团队能够从 label 找到 configured target,从 target 找到 action,从失败 action 找到输入、工具和平台,再用 BEP 与 Profile 证明构建结果和性能变化时,Bazel 才真正成为工程能力。反过来,如果规则仍偷偷依赖宿主机、共享缓存允许任意代码写入、远程执行只有一条 endpoint 配置、迁移也没有制品等价与回退证据,那么再高的缓存命中率也只是把不可重复构建传播得更快。
