# 2023-04: 为什么你该试试 Sccache？

[Sccache](https://github.com/mozilla/sccache) 是由 mozilla 团队发起的类 [ccache](https://ccache.dev/) 项目，支持 C/CPP, Rust, nvcc 等语言，并将缓存存储在本地或者云存储后端。在 v0.3.3 版本中，Sccache 加入了原生的 Github Action Cache Service 支持；在后续的 [v0.4.0-pre.6](https://github.com/mozilla/sccache/releases/tag/v0.4.0-pre.6) 版本中，社区对该功能进行了持续的改进，目前已经初步具备了在生产 CI 中应用的能力。

最近我在 [PyO3/maturin](https://github.com/PyO3/maturin) 的 CI 中引入了 sccache 做了测试，发现它有如下优势：

- **部署配置更容易**：无需指定 `shared-key`，不需要操心 GHA 内部的 cache-from/cache-to 逻辑，配置 `SCCACHE_GHA_ENABLED: "true"` 即可
- **支持多种语言**：sccache 同时支持缓存 C/CPP，Rust，nvcc 多种语言的不同编译器，以 Rust 为例，配置 `RUSTC_WRAPPER: "sccache"` 即可
- **大部分场景下更快**：sccache 缓存的是编译产物，不需要提前加载全部的缓存，也不需要在构建完成后上传缓存内容
- **并发任务友好**：sccache 能在多个并发执行的 job/workflow 之间共享缓存，不需要等到构建结束
- **无缓存冲突**：sccache 对每个编译产物的输入（参数，环境变量，文件等）进行 hash 计算，能构建一个全局的无冲突缓存，不会出现缓存冲突，不需要额外指定不同的 cache key
- **无供应商锁定**：sccache 基于 [opendal](https://github.com/datafuselabs/opendal) 构建，天然支持各种不同的存储服务，在未来的 CI 演进中，可以无缝切换到 s3/gcs/azlob 等服务中，不依赖 GHA Cache 服务
- **活跃维护中** ~~(图穷匕见)~~：sccache 目前由作者本人活跃贡献中，使用中遇到的问题可以直接提交反馈

以下是在 maturin 项目中的测试结果：

| Cases                         | Sccache vs rust-cache (2nd) | Sccache vs rust-cache (3rd) |
|-------------------------------|----------------------------:|----------------------------:|
| Test (ubuntu-latest, 3.7)     |                      59.72% |                  **-1.68%** |
| Test (ubuntu-latest, 3.8)     |                  **-4.70%** |                  **-8.22%** |
| Test (ubuntu-latest, 3.9)     |                      30.72% |                      10.81% |
| Test (ubuntu-latest, 3.10)    |                       1.03% |                      12.15% |
| Test (ubuntu-latest, 3.11)    |                 **-10.16%** |                 **-29.35%** |
| Test (ubuntu-latest, pypy3.8) |                      18.34% |                  **-3.84%** |
| Test (ubuntu-latest, pypy3.9) |                       5.13% |                      22.90% |
| Test (macos-latest, 3.7)      |                      11.87% |                       5.65% |
| Test (macos-latest, 3.8)      |                  **-7.82%** |                 **-13.65%** |
| Test (macos-latest, 3.9)      |                 **-17.98%** |                 **-45.20%** |
| Test (macos-latest, 3.10)     |                 **-13.20%** |                 **-15.38%** |
| Test (macos-latest, 3.11)     |                 **-17.44%** |                 **-29.55%** |
| Test (macos-latest, pypy3.8)  |                      14.83% |                 **-23.32%** |
| Test (macos-latest, pypy3.9)  |                 **-28.03%** |                 **-38.56%** |
| Test (windows-latest, 3.7)    |                      30.08% |                      24.22% |
| Test (windows-latest, 3.8)    |                      35.11% |                      41.14% |
| Test (windows-latest, 3.9)    |                       9.24% |                  **-5.28%** |
| Test (windows-latest, 3.10)   |                  **-8.56%** |                 **-15.81%** |
| Test (windows-latest, 3.11)   |                  **-1.39%** |                 **-36.49%** |
| Test (windows-latest, 3.8)    |                 **-19.99%** |                 **-35.54%** |
| Test (windows-latest, 3.9)    |                      18.95% |                  **-8.55%** |

表格中对比的是使用 sccache 第二次/第三次运行与使用 rust-cache 时候的差异，加粗的复数条目表示 sccache 比 rust-cache 更快。可以看到，随着 Cache 命中率的提高，sccache 对比 rust-cache 最大能取得将近 50% 的提升。

试试 Sccache 吧，任何不爽的地方请到 [issues](https://github.com/mozilla/sccache/issues) 反馈，同时欢迎参与贡献～

---

接下来的部分我会首先介绍 Github Action Cache Service 的内部 API 和它的工作原理，然后对比 rust-cache / sccache 实现的差异来说明为什么 sccache 更好/更快。

## Github Action Cache Service 原理

Github Action Cache Service 本质上是一个支持 prefix query 的 immutable 存储服务，它提供了如下非公开的 API：

### 查询缓存

`GET /cache?keys=abc,ab,a&version=v1`

- `keys`: 指定一组逗号分割的查询 key，返回的结果是相同 version 下前缀匹配的最新 key
- `version`：指定 cache key 所使用的 namespace

### 预留缓存

`POST /caches`

- inputs: `{key: <cache_key>, version: <cache_version>}`
- outputs: `{cache_id: <cache_id>}`

每一组 `(key, version)` 被预留之后，后续所有携带相同 key & version 的请求都会返回 `already exists` 错误，也就是说缓存无法覆盖。请求成功后会返回数字的 cache_id，后续上传缓存和创建缓存都会用到它。

### 上传缓存

`PATCH /caches/[cache_id]`

上传具体的缓存内容，使用 `Content-Range` 来标记本次上传的缓存位置。

### 创建缓存

`POST /caches/[cache_id]`

在所有的缓存内容都创建完毕后，可以使用这个 API 来创建缓存。只有在这个 API 响应成功后，缓存才能被查询到。

## rust-cache 实现

在这套内部 API（合理怀疑是 Azure DevOps 服务提供的）的基础上，Github 提供了 [actions/cache](https://github.com/actions/cache) 供用户使用，而 [rust-cache](https://github.com/Swatinem/rust-cache) 正是基于 `@actions/cache` 实现的。

rust-cache 会基于 github job_id，rustc 版本，环境变量，`Cargo.lock` 等信息计算出一个 cache key，然后把 `~/.cargo` 和 `./target` 打包到一起上传。如果 cache 命中就加载到本地并解压缩使用，如果 cache miss，则会在 post action 中将 cache 上传。

rust-cache 的优势是整个过程只会调用一次 GHA Cache 的 API，很少会触发 rate limit，缺点是一旦 cache 没有命中，就需要完全从零构建。

## sccache 实现

sccache 的 GHA 实现则是完全基于文件的：

![](https://xuanwo.io/reports/2023-04/sccache.png)

sccache 会根据每一次 rustc 调用时传入的环境变量，二进制，编译参数，输入文件等信息计算一个 hash 作为 cache key，如果文件存在就直接从存储服务中加载，跳过本次编译操作，否则就进行编译并将结果写入到存储服务中。这就意味着：

- sccache 不需要处理不同输入带来的缓存冲突，可以使用总是唯一的 hash 作为 cache key
- sccache 可以在 rustc 编译时下载需要缓存，不需要提前加载全部的内容，也不需要在 Job 完成后全部上传
- sccache 缓存加载逻辑不强依赖于 Cargo.lock 本身，即使在依赖出现大量变化的情况下，也能够重用缓存
- sccache 可以在并发的 Job 之间重用缓存，因为大家共享同一个无冲突的存储空间

不仅如此，sccache 还能够应用于 c/cpp 等语言的编译缓存，如果项目中同时还存在 gcc/clang 等编译器的使用，只需要简单配置即可共享缓存。

为了帮助用户更好地在 Github Action 中使用 Sccache，我们开发了 [sccache-action](https://github.com/mozilla/sccache-action)。不过我的 PR 目前还没有合并，可以先使用我的 fork：

```yaml
- name: Sccache Setup
  # Just for test, come back to upstream after released
  uses: Xuanwo/sccache-action@c94e27bef21ab3fb4a5152c8a878c53262b4abb0
  with:
    version: "v0.4.0-pre.6"
```

接下来只需要配置两个环境变量即可：

```yaml
env:
    SCCACHE_GHA_ENABLED: "true"
    RUSTC_WRAPPER: "sccache"
```

在每个 Job 的末尾，`sccache-action` 会输出本次 Cache 的使用情况：

```shell
/opt/hostedtoolcache/sccache/0.4.0-pre.6/x64/sccache --show-stats
Compile requests                   1887
Compile requests executed          1035
Cache hits                          836
Cache hits (C/C++)                   22
Cache hits (Rust)                   814
Cache misses                        189
Cache misses (Rust)                 189
Cache timeouts                        0
Cache read errors                     0
Forced recaches                       0
Cache write errors                    0
Compilation failures                 10
Cache errors                          0
Non-cacheable compilations            0
Non-cacheable calls                 852
Non-compilation calls                 0
Unsupported compiler calls            0
Average cache write               0.051 s
Average compiler                  1.132 s
Average cache read hit            0.000 s
Failed distributed compilations       0

Non-cacheable reasons:
crate-type                          521
-                                   320
unknown source language              11

Cache location                  ghac, name: sccache-v0.4.0-pre.6, prefix: /sccache/
```

用户可以根据这些信息来调整自己使用 sccache 的策略。

---

## 总结

Sccache 以一种全新的方式使用 GHA Cache 来对 Rust 项目进行编译加速，相比于现存的方案有如下优点：

- 部署配置更容易
- 支持多种语言
- 大部分场景下更快
- 并发任务友好
- 无缓存冲突
- 无供应商锁定
- 活跃维护中

欢迎大家在自己的项目中尝试和使用 Sccache！
