為什麼你該試試 Sccache?

語言: CN / TW / HK

Sccache 是由 mozilla 團隊發起的類 ccache 專案,支援 C/CPP, Rust, nvcc 等語言,並將快取儲存在本地或者雲端儲存後端。在 v0.3.3 版本中,Sccache 加入了原生的 Github Action Cache Service 支援;在後續的 v0.4.0-pre.6 版本中,社群對該功能進行了持續的改進,目前已經初步具備了在生產 CI 中應用的能力。

最近我在 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 構建,天然支援各種不同的儲存服務,在未來的 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 反饋,同時歡迎參與貢獻~


接下來的部分我會首先介紹 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 供使用者使用,而 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 實現則是完全基於檔案的:

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。不過我的 PR 目前還沒有合併,可以先使用我的 fork:

- name: Sccache Setup
  # Just for test, come back to upstream after released
  uses: Xuanwo/[email protected]
  with:
    version: "v0.4.0-pre.6"

接下來只需要配置兩個環境變數即可:

env:
    SCCACHE_GHA_ENABLED: "true"
    RUSTC_WRAPPER: "sccache"

在每個 Job 的末尾,sccache-action 會輸出本次 Cache 的使用情況:

/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!

 

作者:漩渦 @databend

Datafuse Labs 開發工程師
開源專案 OpenDAL 的作者
主要研究領域包括儲存,自動化與開源
 

關於 Databend

Databend 是一款開源、彈性、低成本,基於物件儲存也可以做實時分析的新式數倉。期待您的關注,一起探索雲原生數倉解決方案,打造新一代開源 Data Cloud。