- 單台 Mac 上的 DerivedData 快取在第二台 Runner 上預設不存在——多節點 iOS CI 的隱性成本是每台機器各自冷啟動
- 可共享的是建置產物層(DerivedData、SPM 解析結果、Pods/),不是「多台同時寫同一目錄」;pull → build → push 或區域快取樞紐是 2026 年主流做法
- 在 Cloud Mac 機群上,用 lockfile 雜湊 + Xcode 版本做 cache key,配合 rsync/物件儲存,warm 建置常見可再省 5–15 分鐘(在單機快取最佳化之上)
你把 GitHub Actions 的 actions/cache 配好了,warm 建置從 18 分鐘降到 9 分鐘——然後橫向加了三台自託管 M4 Runner,P95 卻又回到 16 分鐘。每台機器都是「第一次見這個 commit」。
這不是回歸原點,而是進入了多節點建置的第二階段:Xcode 快取共享。本文不講「為什麼要快取」(見 CocoaPods / SPM / DerivedData 單機指南),而講如何在多台 Mac 之間同步建置資料,讓任意 Runner 都能吃到隊友編譯過的模組。
1. 多節點 CI 為何比單機更慢
負載平衡把 job 隨機打到 Runner A/B/C 時,每台機器的磁碟狀態彼此獨立。Runner A 剛編完的 MyAppKit.swiftmodule,對 Runner B 毫無意義——除非你把產物搬過去。
GitHub 託管 runner 用 actions/cache 把 tarball 存到雲端,job 開始時 restore。自託管或 Cloud Mac 機群往往沒有等價的託管層,團隊要麼:
- 接受每台機器各自 warm(浪費算力)
- 把快取目錄掛 NFS,多台同時寫(易爆庫)
- 自建快取樞紐:建置前拉、成功後推
第三種在 iOS 團隊裡最常見,也最適合 多區域節點部署:美東三台 M4 共享一個快取桶,亞太兩台共享另一個,跨區只同步 SPM/Pods 層。
2. Xcode 建置資料分哪幾層
同步之前先分清「什麼值得傳、什麼不能混傳」。
2.1 DerivedData
xcodebuild -derivedDataPath 指向的目錄,含編譯後的 .o、.swiftmodule、連結中間產物、部分索引。體積常達數 GB,是warm 建置加速的最大頭,也是對 Xcode 版本最敏感的一層。
建議在 CI 上固定路徑,例如 /Volumes/CI/DerivedData/<scheme>,與同步腳本裡的 REMOTE 變數一致——單機快取文裡強調過,路徑不一致等於沒快取。
2.2 SPM 與 CocoaPods
SPM:~/Library/Caches/org.swift.swiftpm + 倉庫內 .build(若提交了解析結果)。CocoaPods:Pods/ 與 ~/Library/Caches/CocoaPods。
這兩層變更頻率低於 DerivedData(隨 lockfile 變),更適合跨機器、甚至跨 region 同步。很多團隊只把 SPM/Pods 推到 S3,DerivedData 留在區域內 rsync。
2.3 ModuleCache 與 Xcode 全域快取
~/Library/Developer/Xcode/DerivedData/ModuleCache.noindex 與各種 SDKStatCaches。通常隨 DerivedData 一併處理;單獨同步時務必保證同一 Xcode 小版本。
3. 四種多節點同步架構
| 模式 | 做法 | 優點 | 風險 |
|---|---|---|---|
| 本地-only | 每台 Runner 獨立磁碟,無共享 | 零維運 | 橫向擴展不省時間 |
| NFS 共享掛載 | DerivedData 掛網路碟,多台唯讀或讀寫 | 設定簡單 | 並行寫入易損壞;網路延遲拖慢連結 |
| rsync 樞紐 | 中央目錄或 leader 機;建置前 pull、成功後 push | 可控、與 Xcode 相容性好 | 需寫腳本與鎖 |
| 物件儲存 tarball | 按 cache key 打包上傳 S3/R2;job 開始下載解壓 | 跨區域、與 GHA cache 模型一致 | 大 DerivedData 上傳費時;需壓縮 |
2026 年實踐裡,同機房 / 同區域 Cloud Mac優先 rsync 樞紐;跨國團隊用物件儲存同步 SPM/Pods,DerivedData 區域自治。勿把 NFS 當「多寫共享 DerivedData」的銀彈。
禁止兩台 Runner 同時對同一 DerivedData 樹執行 xcodebuild。若必須共享碟,用檔案鎖或單寫多讀(一台 designated warmer 編完再 rsync 分發)。
4. Cache key 與失效策略
多節點共享的 cache key 應比單機更保守——錯命中比冷啟動更糟(詭異連結錯誤、簽章失敗)。
建議納入 key 的欄位:
xcodebuild -version或XCODE_VERSION環境變數hashFiles('**/Podfile.lock')或Package.resolved- Scheme 名或 target 集合(多 scheme 倉庫分開快取)
arm64(Apple Silicon 專用前綴,避免歷史 x86 污染)
不建議把完整 github.sha 作為唯一 key——否則每台機器永遠 miss。可用分層 restore-keys:dd-arm64-main-<lockhash> → dd-arm64-main- → dd-arm64-。
失效時機:Xcode 升級、lockfile 變更、切換 SWIFT_VERSION 或重大 Build Settings、人為 clean build 後應 bump key 後綴或刪遠端目錄。
5. 實戰:rsync pull/push 腳本
下面是一套在 M4 Cloud Mac 機群驗證過的最小腳本,快取樞紐在 cache-leader.internal:/cache/ios/(可以是機群中一台大磁碟機器或 NAS)。
#!/usr/bin/env bash
# cache-sync.sh — 在 xcodebuild 前后调用
set -euo pipefail
ACTION="${1:?pull|push}"
CACHE_ROOT="${CACHE_ROOT:-cache-leader.internal:/cache/ios}"
SCHEME="${SCHEME:-MyApp}"
KEY="${CACHE_KEY:-arm64-$(md5 -q Podfile.lock 2>/dev/null || echo nolock)-$(xcodebuild -version | head -1 | tr ' ' '-')}"
LOCAL_DD="${DERIVED_DATA_PATH:-/Volumes/CI/DerivedData/${SCHEME}}"
REMOTE="${CACHE_ROOT}/${KEY}/${SCHEME}/DerivedData"
LOCAL_SPM="${HOME}/Library/Caches/org.swift.swiftpm"
REMOTE_SPM="${CACHE_ROOT}/${KEY}/swiftpm"
rsync_opts=(-az --delete-delay --contimeout=10)
case "$ACTION" in
pull)
rsync "${rsync_opts[@]}" "${REMOTE}/" "${LOCAL_DD}/" || true
rsync "${rsync_opts[@]}" "${REMOTE_SPM}/" "${LOCAL_SPM}/" || true
;;
push)
# 仅成功构建后调用
rsync "${rsync_opts[@]}" "${LOCAL_DD}/" "${REMOTE}/"
rsync "${rsync_opts[@]}" "${LOCAL_SPM}/" "${REMOTE_SPM}/"
;;
*) echo "usage: $0 pull|push" >&2; exit 1 ;;
esac
CI 流程:
- Job 開始 →
cache-sync.sh pull xcodebuild -scheme "$SCHEME" -derivedDataPath "$LOCAL_DD" build- 僅當 build 成功 →
cache-sync.sh push
多台並行時,push 可能競態——用 flock 或「最後寫入 wins」接受即可;DerivedData 在同 lockfile + 同 Xcode 下內容應相容。若 push 極頻繁,可改為非同步上傳(建置結束後背景 rsync,不阻塞 pipeline)。
6. 跨區域 Cloud Mac 快取樞紐
美東、美西、亞太各放 Runner 時,跨洋 rsync 全量 DerivedData 往往不划算(延遲 + 出口流量)。建議:
- 每區域一個 cache bucket(S3 / R2 / 提供商物件儲存)
- 同步內容:
Podfile.lock雜湊對應的Pods/tarball +Package.resolved對應的 SPM 快取 - DerivedData 僅在區域內 rsync;首個 job 冷啟動,後續 job warm
- 鏡像版本與 黃金映像 tag 對齊,避免 Xcode 漂移導致全庫失效
上傳前用 tar czf 壓縮 SPM 快取,常能減 40%–60% 體積;DerivedData 內部已是大量小檔案,tar + 並行上傳比裸 rsync 跨區更穩。
7. 接入 GitHub Actions / 自託管 Runner
自託管 runner 上可在 workflow 裡包一層:
- name: Restore shared Xcode cache
run: ./scripts/cache-sync.sh pull
env:
CACHE_ROOT: ${{ secrets.IOS_CACHE_SSH }}
SCHEME: MyApp
DERIVED_DATA_PATH: /Volumes/CI/DerivedData/MyApp
- name: Build
run: xcodebuild -scheme MyApp -derivedDataPath /Volumes/CI/DerivedData/MyApp build
- name: Save shared Xcode cache
if: success()
run: ./scripts/cache-sync.sh push
若仍用 GitHub 託管 actions/cache,自託管機群可雙軌:GHA cache 作兜底,rsync 樞紐作同區域低延遲層。注意兩套 key 命名空間分開,避免互相覆蓋。
監控建議:記錄 pull/push 耗時、傳輸位元組、build 是否 incremental(看 xcodebuild 日誌裡 CompileSwift 數量)。P95 變慢時先查 cache miss,再查機器負載——與 建置忽快忽慢排查同一套方法論。
8. 踩坑清單
- 並行寫 DerivedData:隨機
stat cache file corrupted→ 改 pull/push - Xcode 小版本不一致:模組 ABI 對不上 → key 含版本,鏡像統一 tag
- 路徑不一致:快取到 A 路徑,編譯用 B 路徑 → 全量重編
- clean 後仍 push:把空目錄推上去沖掉好快取 → 僅 success 且非 clean 時 push
- 簽章產物進快取:Provisioning 變更後沿用舊簽章 → 快取層排除
Build/Products或按設定分 key - 磁碟滿:DerivedData 膨脹 → 定期按 LRU 清遠端
${KEY}目錄,保留最近 N 個 lockhash
9. FAQ
多台 Mac 能直接共享同一個 DerivedData 目錄嗎?
不建議同時寫入。唯讀掛載或單寫者多讀者可以,但連結階段仍可能踩鎖。生產環境優先本地 DerivedData + rsync。
Xcode 快取共享需要統一 Xcode 版本嗎?
必須。升級 Xcode 後應使用新 cache key,舊目錄可非同步刪除。
和 Bazel / Tuist 遠端快取比怎麼樣?
Bazel/Tuist 遠端快取粒度更細(action 級),適合超大 monorepo。標準 Xcode 工程用 DerivedData 同步改造成本更低,與現有 xcodebuild 流程相容。
多台 Cloud Mac 需要多大磁碟?
每台本地至少預留 50–100GB 給 DerivedData + SPM;樞紐機或物件儲存按「並行 scheme 數 × 單 scheme 快取體積 × 保留版本數」估算,M4 + 2TB 擴充碟對中型團隊通常夠用。
結語
Xcode 快取共享解決的是多 Runner 時代的「第二次冷啟動」:單機快取最佳化之後,橫向加機器不應再付出全額編譯稅。記住三層分離(Pods / SPM / DerivedData)、一種同步紀律(pull-build-push)、一條鐵律(勿並行寫 DerivedData)。
快取是分散式編譯裡最便宜的算力倍增器——比再買兩台 M4 更快見效。
從一台 cache leader 和 cache-sync.sh 開始;用 lockfile 雜湊命名遠端目錄;build 綠了再 push。下週 P95 曲線會告訴你值不值。
為多節點 CI 預留磁碟的 Cloud Mac
Vuncloud Mac mini M4:美東、美西、亞太 SSH 就緒,適合掛載大碟 DerivedData、部署 rsync 快取樞紐與自託管 Runner 機群。
延伸閱讀
- CocoaPods / SPM / DerivedData 單機快取指南
- 黃金映像與 Cloud Mac 自動化
- 跨國 iOS 團隊統一建置環境
- 為什麼 iOS CI/CD 跑在 Mac mini M4 上
Apple 工具鏈行為以官方發布為準;快取腳本請按團隊安全策略稽核 SSH 與物件儲存憑證。最後更新:2026 年 7 月 27 日。