- 单台 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 日。