构建队列越来越长,开发机之间的 Bazel action 却经常互相命不中缓存。
最快解法:不要直接全量启用 Bazel 9 远程缓存,先用固定 Mac 基线池做只读试点;只有可复现、跨节点重复率足够且网络稳定时,才扩大缓存写入范围。
先用三个症状决定是否试点
这篇文章适合已经使用 Bazel 构建大型 iOS 工程、希望降低重复编译时间的研发效能负责人;也适合需要在新增 Mac 节点和共享缓存之间做预算取舍的企业 IT、FinOps 负责人。
如果你负责缓存写入权限、构建产物隔离或供应链安全,下面的权限矩阵和污染验证步骤也可以直接纳入评审材料。
| 现场症状 | 优先动作 | 暂缓动作 |
|---|---|---|
| 同一提交在固定 Mac 上重复构建,action 结果稳定,跨节点也有明显重复 | 启用只读缓存试点,再验证受控写入 | 不要直接让所有开发机写入 |
| 构建经常受环境变量、Xcode、SDK 或外部工具差异影响 | 先固定工具链和执行环境 | 暂不根据缓存命中率做采购结论 |
| 队列主要消耗在签名、归档、模拟器测试或发布验证 | 优先测算 Mac 节点容量 | 不要把远程缓存当成远程执行 |
| 缓存服务与 Mac 节点跨地域,下载耗时和失败重试明显 | 先做同地域缓存对照 | 不要仅凭“缓存可共享”判断值得部署 |
| 命中率不稳定,且出现异常产物或无法解释的 action 差异 | 关闭写入、隔离节点、保留证据 | 不要继续扩大缓存范围 |
PUT 和 GET 传输数据,并区分 /ac/ 的 action result 与 /cas/ 的输出对象;这意味着缓存层和真正承担 Apple 工具链任务的 Mac 构建层必须分开核算。 [Bazel 远程缓存协议与对象路径说明](https://bazel.build/remote/caching)
你应先收集四类证据:重复构建比例、队列等待、干净构建频率、跨节点任务重合度。没有这些记录时,所谓“构建变快”很可能只是本地缓存、增量状态或某次提交规模变化造成的假象。
按可复现性检查 Bazel 9、rules_apple 与 Xcode
远程缓存最怕的不是没有缓存,而是不同节点生成相同 action key,却实际使用了不同工具链。Bazel 官方排查文档把非 Hermetic 构建、环境差异、命令行参数和不可缓存 action 都列为跨机器命中失败的重要排查方向;执行日志可用于比较两次运行的 action 是否真的一致。 Bazel 跨机器缓存命中排查文档
截至 2026 年 9 月 1 日,rules_apple 仓库的版本说明列出 Bazel 9.x 的支持范围为 rules_apple 2.x 起至当前版本,但这是版本层面的支持信息,不等于你的 Xcode、SDK、macOS 和构建参数组合已经具备生产可用性。rules_swift 的仓库说明则列出 Bazel 9.x 的最低支持版本为 3.5.0,升级时仍需以对应 Release Notes 和团队验证结果为准。 rules_apple 支持矩阵 · rules_swift 支持矩阵与工具链说明
建议把兼容性拆成三层,而不是简单写成“支持 Bazel 9”:
| 组合层级 | 可以写入生产基线吗 | 验证重点 |
|---|---|---|
| 官方 Release Notes 明确支持的 Bazel、rules_apple、rules_swift 组合 | 可以进入候选基线 | 同一提交、同一目标、跨 Mac 重复执行 |
| 官方支持,但团队更换了 Xcode、SDK 或 macOS | 先进入验证池 | 编译、链接、测试、归档和签名结果 |
| 开发分支、预发布版本或未合并修复 | 只能隔离试验 | 是否出现错误复用、action key 变化和产物差异 |
rules_apple,却让不同 Mac 自由升级 Xcode 或系统。 [Apple Xcode SDK 与系统要求](https://developer.apple.com/xcode/system-requirements/)
重点检查以下输入是否被稳定管理:
DEVELOPER_DIR、PATH、SDKROOT、TOOLCHAINS等环境变量;- Xcode、Command Line Tools、SDK 和 Swift 编译器版本;
- action 使用的外部脚本、生成器、代码签名工具及其绝对路径;
- CPU 架构、目标平台、最低部署版本和构建模式;
- 是否存在构建过程中修改源文件、生成文件或依赖目录的行为。
rules_swift 官方文档还特别说明,macOS 节点默认使用 Xcode 提供的工具链,也可以通过 TOOLCHAINS 指定自定义 Swift 工具链。对企业 CI 来说,这意味着工具链选择必须进入节点镜像、启动检查和审计记录,而不能依赖某台 Mac 当前的用户环境。 [rules_swift 工具链配置说明](https://github.com/bazelbuild/rules_swift)
⚠️ 经验提醒:版本表只能回答“这两个版本理论上能协同工作”,不能回答“你的生产流水线能否安全复用缓存”。生产准入至少还要覆盖测试、归档、签名和失败恢复。
用命中、传输和失败率判断缓存收益
不要用单次构建总时长作为唯一指标。你需要把构建拆成远程命中、远程下载、远程上传、本地执行、排队等待和失败重试几个部分。
Bazel 的标准输出会区分 remote cache hit 与实际执行的 action;官方排查流程还建议先清理本地缓存,再重复同一构建,以免本地命中掩盖远程缓存效果。对于跨节点验证,则应比较执行日志中的 action 是否一致,再判断是 action key 不同、缓存读取未开启,还是缓存服务本身没有返回对象。 Bazel 命中率与执行日志排查方法
建议至少记录以下字段:
- 同一提交的 action 总数,以及远程命中 action 数;
- 命中对象下载耗时、未命中 action 执行耗时;
- 上传耗时、传输失败次数和重试次数;
- 缓存对象数量、增长速度、删除后的冷启动表现;
- CI 队列等待与 Mac 节点实际占用;
- 无缓存、只读缓存、读写缓存三组构建的产物摘要和测试结果。
按权限边界控制缓存写入与产物安全
企业不应默认允许所有开发机写入共享缓存。写入者一旦使用了未封版的 Xcode、错误的环境变量或被篡改的外部工具,就可能把不应复用的结果放入共享命名空间。
Bazel 官方文档指出,HTTP Basic Authentication 应配合 HTTPS 使用;用于对象存储的凭证如果具备读写权限,泄露后可能被用于读取或写入缓存数据。因此,缓存凭证应放在专用 CI 节点的安全存储中,不能通过普通开发机配置文件或公开日志传播。 Bazel 远程缓存认证与 HTTPS 说明
| 节点类型 | 可执行任务 | 缓存权限 | 审计证据 | 异常动作 |
|---|---|---|---|---|
| 固定 CI 写入节点 | 标准编译、单元测试 | 读写 | 节点身份、工具链摘要、提交号 | 立即撤销写入凭证并隔离 |
| 固定 CI 只读节点 | 重复构建、验证、部分测试 | 只读 | 命中记录、产物摘要 | 保留日志并复测 |
| 开发机 | 本地开发与调试 | 默认只读或禁用写入 | 用户、分支、命令行 | 发现差异时禁止继续复用 |
| 临时远程 Mac | 试点、峰值构建、环境验证 | 只读,必要时短时授权 | 租赁周期、节点、任务范围 | 到期回收凭证并清理工作区 |
| 签名与发布节点 | 归档、签名、发布 | 按 Target 禁止或严格隔离 | 签名身份、归档摘要、审批记录 | 产物异常时停止发布链路 |
验证缓存没有污染产物时,按以下步骤执行:
- 固定提交、Bazel、
rules_apple、rules_swift、Xcode、SDK 和构建参数; - 在基线 Mac 上执行无缓存构建,保存执行日志、产物摘要和测试结果;
- 在第二台 Mac 上执行只读缓存构建,比较 action key、产物摘要和测试结果;
- 使用受控写入节点生成新结果,再由只读节点复用;
- 人为改变一个工具链输入,确认缓存不会错误返回旧产物;
- 发现输出差异时,停止写入,隔离异常节点,保留缓存访问日志和执行日志。
把缓存、Mac 节点与运营成本放进同一模型
远程缓存不是“增加一台服务器就能少买几台 Mac”。它只优化可复用 action,正式归档、签名、模拟器测试、设备连接、Xcode 工具链验证和未命中 action 仍然需要真实 Mac。
用以下变量核算,不要在没有企业日志的情况下预填节省比例:
Ccache:缓存服务、对象存储、备份和生命周期管理成本;Cmac:Mac 构建节点采购或租赁成本;Cops:工具链升级、凭证管理、故障处理和审计工时;Cnet:缓存与 Mac 节点之间的传输成本;Cqueue:排队造成的研发等待和发布延迟成本;Cfail:缓存不可用、错误产物和节点重建带来的恢复成本。
| 方案 | 能解决的问题 | 不能解决的问题 | 适合的证据条件 |
|---|---|---|---|
| 只增加 Mac 节点 | 并行执行能力、签名、归档、模拟器任务 | 重复编译仍会重复发生 | 队列等待持续来自并发不足 |
| 只部署远程缓存 | 复用可缓存且可复现的 action | Xcode、模拟器、签名和物理设备任务 | 跨节点命中稳定,传输失败可控 |
| 远程缓存+固定 Mac 基线池 | 同时控制复用与工具链一致性 | 不能消除所有峰值容量需求 | 有明确的只读、写入和回退策略 |
| 远程缓存+弹性远程 Mac | 应对短期峰值和隔离试验 | 长期重负载未必比自购更优 | 峰值具有周期性,且需要快速扩容 |
用 A/B 试点形成生产准入结论
试点不要按“部署前、部署中、上线后”的流水线叙事推进,而应围绕同一组指标完成三路对照:
- A:无缓存。清理本地和远程缓存,固定 Mac 配置,记录完整构建、测试、归档和签名结果;
- B:只读缓存。由经过验证的写入节点预热,测试节点只能读取,观察命中、下载、失败与产物一致性;
- C:受控读写缓存。只开放给固定 CI 写入节点,开发机和临时远程 Mac 保持只读,比较缓存增长、污染风险和恢复动作。
条件式决策清单
- 若跨节点 action key 一致、只读缓存的产物摘要与无缓存基线一致,且传输失败不会明显拉长队列,则进入受控写入试点;
- 若命中率看似不错,但签名、归档或模拟器任务仍占据主要队列时间,则优先扩容 Mac,而不是继续扩大缓存;
- 若相同 action key 产生不同输出,立即停止共享写入,隔离节点并修复工具链或环境输入;
- 若缓存服务不可用时构建可以安全回退到本地执行,则保留缓存作为加速层;否则先补齐故障降级;
- 若跨地域下载耗时高于未命中 action 的本地执行成本,则改为同地域缓存,或停止该地域部署;
- 若
rules_apple、rules_swift、Bazel 或 Xcode 升级后 action key 大面积变化,先完成新基线验证,再决定是否清理对应缓存命名空间; - 若试点期间排队主要来自缓存未覆盖的签名、归档、模拟器和设备任务,则把预算转向 Mac 节点容量。
最后再决定缓存投入还是 Mac 容量
如果你的当前方案是“每台开发机自行编译+少量固定 Mac 打包机”,真实缺点通常有三类:工具链容易漂移,重复 action 无法稳定复用;峰值提交时队列集中在有限 Mac 节点;开发机写入共享产物后,异常结果和敏感信息更难追溯。
如果你的当前方案是“直接购买更多 Mac”,缺点则是容量按峰值建设,低峰期利用率可能不足;节点升级、磁盘、证书和故障恢复都由团队承担;当需求只是短期试点或发布高峰时,长期采购会把临时容量变成固定资产。
因此,完成 A/B 试点后,先把峰值队列、签名任务、缓存未覆盖任务和恢复演练结果换算成真实 Mac 容量。若现有节点无法提供隔离试验或短期峰值资源,再评估按周期启用 MACGPU 的远程 Mac 作为基准节点或弹性构建池;在缺少命中和队列数据前,不建议直接长期采购缓存基础设施或一次性扩充 Mac 集群。