在构建中加入 sccache,只需一行配置;迁移到它,则要拿出证据。真正有用的变化,是让原先困在单个 runner 上的编译工作,变成其他兼容 runner 可以安全复用的编译产物。大量 job 在输入稳定时反复执行成本高昂的 Rust 或 C/C++ 编译,这项变化就会带来收益。工具链频繁变化、路径各异、绝大多数工作无法缓存,或一次远程查询的成本超过它所替代的编译时,结果都会令人失望。

因此,迁移应从未命中开始。未命中报告会显示,这套构建中重复且可缓存的工作是否多到值得接入共享基础设施。它也能避开 CI 中常见的次序错误:本地缓存的收益尚未得到证明,存储桶、凭据和又一项网络依赖已经先行加入。

眼下正适合检验这条分界。sccache 0.16.0 于 2026 年 6 月 19 日发布,它让 0.15.0 引入的多级缓存进入稳定阶段,并允许所有存储后端设为只读。[7] 有了这些功能,迁移可以遵循合理次序:先完成本地验证,再接入共享后端;测量结果确实支持时,才在前面增加快速本地层。

图像背景:封面把瑞典皇家理工学院并行计算中心的真实机房放在眼前,“云”在这里落实为具体设备。这张照片贴合此次迁移,因为“共享缓存”意味着实际占用存储、网络和运维容量。目标是缩短编译时间,同时让这项藏在背后的服务仍然清晰可查。[11]

从最小的可回退改动开始

对于 Rust,第一次实验只要设置 RUSTC_WRAPPER=sccache;Cargo 配置也接受 build.rustc-wrapper。对于 C 和 C++,CMake 可以把 CMAKE_C_COMPILER_LAUNCHERCMAKE_CXX_COMPILER_LAUNCHER 设为 sccache。远程后端留空时,工具会使用本地磁盘,按需启动本地服务进程,并通过 sccache --show-stats 查看各项计数。[1] 本地缓存的默认容量为 10 GB。[4]

本地阶段会把两个问题分开检验:

  1. 这类工作负载能否复用编译器输出? 先运行一次冷构建;随后停留在同一 commit,删除常规构建输出后再运行一次;最后做一次具有代表性的源码改动,运行第三次。记录实际耗时、编译请求数、缓存命中数、未命中数、不可缓存调用数与缓存错误数。
  2. 另一台 runner 复用这些输出是否划算? 这是后续的网络与存储实验。本地命中模式即使欠佳,也要在远程缓存结果之外原样记录。

Firefox 的构建文档把这项取舍写得很清楚:初次使用 sccache 构建会更慢,因为工具要处理文件并执行存储操作;后续兼容构建才能从复用中受益。[10] Mozilla 的结果只用来参照曲线形状,采用门槛仍取决于团队自己的工作负载。在有代表性的一周里,节省的 CI 时间所对应的收益,必须超过哈希、预处理、传输、存储与运维的总成本。

同样的原因也说明,恢复整个 Cargo target 目录和使用 sccache 承担着不同任务。Cargo 把 sccache 描述为第三方共享缓存,可以跨 workspace 复用已经构建的依赖项。[8] 恢复 target 树,会保留某一个构建目录里更广泛的状态;sccache 则用内容生成的缓存键,匹配粒度更细的编译器调用。后者更易跨环境复用,覆盖范围则小于前者所携带的全部编译产物。

一次缓存命中,就是对构建身份的声明

sccache 生成缓存键时,输入远不止源文件名。对于 Rust,文档列出的哈希输入包括源文件摘要、rustc 程序路径、host triple、sysroot、sysroot 中的共享库,以及解析后的编译器参数。对于 C 与 C++,缓存键包含预处理后的源文件、编译器二进制文件、语言、架构和依赖参数、相关环境变量,以及其他编译细节。[2]

这一长串输入用于保证缓存复用的正确性,也构成了未命中的主要来源。在运维人员看来等价的两台 runner,交给缓存的代码检出路径、编译器构建版本、sysroot、feature flag、target triple、SDK 或环境值未必相同。

路径身份应当单列为一项迁移任务。默认配置要求绝对路径一致。SCCACHE_BASEDIRS 可以在计算缓存键前剥离一个或多个绝对路径前缀,避免 /work/job-184/project/work/job-927/project 仅因执行器选择不同目录而产生未命中。匹配时以最长前缀为准;Windows 之外的系统区分大小写。[1] 只归一化确实等价的根目录;抹去有意义的路径差异,可使缓存更难审计。

接下来要定义有意保留的隔离。SCCACHE_S3_KEY_PREFIX 可以限定存储桶内对象的范围;当外部兼容条件尚未体现在缓存键中,SCCACHE_C_CUSTOM_CACHE_BUSTER 可以迫使 C 语言家族调用进入新的键空间。[1][6] 合理的划分包括操作系统、架构、工具链通道、release build 与 sanitizer build,或在怀疑出现异常产物后有意开启的新代次。每个前缀都加入 commit SHA,会重新制造 runner 本地隔离,跨 commit 复用也会随之消失。

盘点注定无法命中的工作

未命中数上升也会来自正常行为:部分编译工作被有意放在缓存模型之外。

Rust crate 必须关闭增量编译后才能缓存。会调用系统链接器的 crate——包括生成二进制文件、动态库、过程宏及相关输出的 crate——都在 sccache 的缓存范围之外;读取未声明文件的过程宏也有引发问题的风险。[3] 项目 README 还指出,Cargo 默认会为 debug profile 中的 workspace member 和 path dependency 开启增量编译。[1] CI 实验应记录自身的 CARGO_INCREMENTAL 与 profile 设置,并把开发者增量构建和干净的 release job 作为不同工作负载记录。

C 与 C++ 的边缘条件不同。本地后端可以缓存预处理器输出,但这条快速路径只适用于本地存储,多种编译器选项和依赖时间的宏都会将其关闭。文档还列出了一些条件:预处理器缓存若采用激进配置,存在返回过期输出的风险。[4] 远程命中依然能节省编译时间,但一次未命中仍可带来可观的预处理成本。C++20 module 支持尚不完整:现有文档覆盖部分 Clang named-module flag,GCC 与 MSVC module 调用则会绕过缓存。[1]

接入共享存储以前,先按语言、target 与 job 归类最主要的未命中和不可缓存原因。如果成本最高的工作集中在链接步骤、增量 workspace crate、未支持的 module,或一次性生成的代码上,扩大缓存只会存下更多缓存对象,关键路径仍留在原处。

runner 确有可共享内容时,再升级为共享缓存

单个长期存活的构建节点,可以只用 10 GB 本地默认容量和一套淘汰策略。短生命周期 runner 集群更能体现共享缓存的价值:一个 job 为未变化的依赖项付出编译成本,后续使用兼容工具链的 job 直接取回单个缓存对象,省去重复构建。

Rust 编译器项目的 CI 是一则独立案例,团队仍须用自身工作负载建立基准。它的开发指南写道,CI 会把持久化 sccache 编译产物存入 S3 存储桶;只要源文件与 C/C++ 编译器一致,就能复用单个 LLVM translation unit。[9] LLVM 的编译单元体量大,又会在众多 job 中反复出现。小型应用若依赖项存续时间短,feature 又频繁变化,收益也有大幅减弱的风险。

后端要按照访问模式选择。GitHub Actions cache 在该服务的工作流范围内较为方便。Redis 或 Memcached 适合低延迟的私有 runner 网络。S3 兼容存储拥有范围广、持久的对象命名空间,同时也带来网络距离、生命周期规则与凭据管理。[1][6] “支持哪种后端?”只是开端;工程上还要回答哪些 job 能快速访问、哪些身份可以写入,以及由谁负责保留策略与可用性。

0.16.0 也让本地加远程的拓扑变得实用。disk,s3 这样的缓存链会先检查较快层级;未命中后继续查询下一层,并在较慢层命中后回填前面的快速层。SCCACHE_MULTILEVEL_WRITE_ERROR_POLICY 有三种写入失败处理方式:忽略错误;使用 l0(默认值),只让第一层的写入错误导致失败;使用 all,让每个可读写层的写入错误都导致失败。[5] 层级顺序与策略直接定义生产行为,其作用远超调优点缀。把远端对象存储放在首位,会把其延迟加入每次查询;选择 all,则会让一项可选的加速服务变成构建可用性的依赖。

让信任保持不对称

共享编译产物最终会被链接进程序代码,因此写权限比读权限更敏感。这项判断来自产物流向,属于工程推论;对应的运维安排很直接:受信任的 branch job 与 release job 可以写入缓存,来自不受信任 fork 和 pull request 的 job 通常只开放只读访问,或完全不接入远程缓存。

S3 后端支持 READ_ONLY,可由 SCCACHE_S3_RW_MODE 设置。对于公开只读存储桶,SCCACHE_S3_NO_CREDENTIALS=true 可以避免分发写凭据,键前缀则把缓存与无关对象隔开。[6] 0.16.0 把只读能力扩展到所有后端。[7] 私有缓存也可沿用同一做法,只发放范围严格受限的只读凭据。

只读消费者只是完整信任设计的一部分。运维人员仍需管理存储桶策略、传输加密、审计日志、保留策略和受约束的写入方,同时准备清理路径。SCCACHE_RECACHE 会覆盖新请求所对应的缓存结果;新的键前缀或 cache-buster 代次则能隔离旧命名空间。[1] 写入权限应按软件包仓库或制品仓库权限管理,级别高于普通 CI 临时空间。

决定缓存故障是否应导致构建失败

wrapper 默认会严格处理与本地服务进程的通信:I/O 失败会让构建失败。设置 SCCACHE_IGNORE_SERVER_IO_ERROR=1 后,流程会回退到编译器。[1] 多级存储另有自己的写入错误策略。[5] 两项控制分别覆盖不同故障路径,但应当表达同一项服务决策。

对于多数把缓存当作加速器的团队,缓存中断时,预期结果应是构建变慢,job 状态仍保持绿色。相应配置包括编译器回退、ignore 或经过仔细测试的 l0 多级策略、通过 SCCACHE_ERROR_LOG 留下可见错误日志,并按错误率告警;job 则继续运行。若缓存填充本身是一项经过测试的交付物,严格策略也成立,前提是团队明确作出这一选择。

回滚应当朴素直接:取消编译器 wrapper,移除远程配置,再用真实编译器运行同一套构建。这条路径若从未经过测试,性能工具就会意外变成构建正确性所依赖的基础设施。

按证据推进的迁移顺序

  1. 固定预编译发行版。 项目建议 CI 使用预编译二进制文件,因为从源码编译 sccache 本身就会消耗大量资源。[1] 在工具链镜像的版本记录中一并注明这个版本。
  2. 在本地测量。 使用同一种 runner 规格,覆盖三组构建:冷构建、同一版本复用,以及一次常规修改。实际耗时与 --show-stats 输出都要保存。
  3. 稳定构建身份。 归一化等价的代码检出根目录,固定编译器与 SDK,记录增量编译设置,并隔离确实不兼容的 target。
  4. 为受信任 job 增加一个共享后端。 跟踪命中率、传输字节数、存储增长、缓存错误与端到端 CI 时间。命中率上升而 job 变慢,实验即告失败。
  5. 谨慎开放读取。 先明确缓存写入方与清理方案,再向不受信任 job 开放只读访问。
  6. 测出延迟问题后再增加缓存层级。 runner 存续时间足以反复使用本地热缓存层时,这一层才有价值;持续时间不足时,它只会多出一个需要配置与检查的目录。

新的兼容 runner 可以跳过高成本编译,缓存故障会退化到一条已知的较慢路径,运维人员也能解释每一类主要未命中;三项条件共同标志着迁移成功。最后一项尤其重要。命中节省时间;未命中则揭示系统对现实的描述是否仍然准确。

来源

  1. Mozilla,sccache 0.16.0 README——受支持的编译器与存储后端、wrapper 设置、本地 server 与统计、路径归一化、故障回退、cache busting、recache 和已知限制。
  2. Mozilla,sccache 0.16.0 “How caching works”——文档列出的 Rust 与 C/C++ 缓存键输入。
  3. Mozilla,sccache 0.16.0 Rust notes——受支持的调用形式、增量编译限制、linker 输出限制和过程宏风险。
  4. Mozilla,sccache 0.16.0 local-cache 文档——各平台路径、10 GB 默认容量、single-server 限制、预处理器缓存条件和只读模式。
  5. Mozilla,sccache 0.16.0 multi-level cache 文档——层级顺序、回填、写入路径和 ignore / l0 / all 错误策略。
  6. Mozilla,sccache 0.16.0 S3 文档——后端配置、key prefix、凭据来源、只读模式和免凭据的公开读取。
  7. Mozilla,sccache v0.16.0 release notes(2026 年 6 月 19 日)——多级缓存后的稳定化、所有后端的只读支持,以及韧性、正确性和客户端效率变化。
  8. The Cargo Book,“Build Cache”——Cargo 构建目录的行为,以及 sccache 如何作为共享依赖缓存跨 workspace 工作。
  9. Rust Compiler Development Guide,“LLVM caching with Sccache”——Rust 项目在 CI 中通过 S3-backed cache 复用 LLVM translation unit。
  10. Firefox Source Docs,“Configuring Build Options”——初次构建成本、后续复用,以及 Firefox 构建系统对 SCCACHE_BASEDIRS 的自动配置。
  11. Johan Fredriksson,“PDC server room”(2010 年 12 月 9 日),Wikimedia Commons——文章图片的摄影来源。