性能 2026 年 4 月 15 日

2026-04-15:租赁云 Mac CI 上的 Xcode 衍生数据、TMPDIR 与 xcresult 隔离

MacXCode 技术团队 2026 年 4 月 15 日 约 13 分钟阅读

移动发布团队在香港、东京、首尔、新加坡与美国东部租赁Mac mini M4构建机时,常常把“Xcode 很慢”归咎于编译器本身,而真正拖慢流水线的是文件系统争用:两条流水线写入同一片 DerivedData、某个 UI 测试作业把/tmp塞满,或.xcresult结果包在无配额约束下膨胀到6 GB以上,进而让并行的归档任务抢不到 NVMe 写带宽。本文以2026-04-15为时间戳,从谁需要按作业隔离讲起,罗列无头 SSH 构建机上可复现的失败模式,用表格比较目录策略,给出带数字八步 Shell 手册,并串联SwiftPM 缓存与注册表解析以及自动与手动签名两篇姊妹文,让下一次跳转仍然可预测、可审计。

把问题说透之前,先强调一个事实:Apple Silicon裸机能显著降低 CPU 层面的抖动,但 NVMe 的写入吞吐与 inode 压力仍是硬上限。若你仍让所有作业共享~/Library/Developer/Xcode/DerivedData,那么“偶发红构建”其实是统计上必然会出现的尾部事件。把DERIVED_DATA_PATHTMPDIR-resultBundlePath与 SwiftPM 的-clonedSourcePackagesDirPath绑定到同一作业前缀,才能把容量规划从拍脑袋变成可度量的 IO 预算,并在比较不同区域节点时给出可信依据。

谁该在云 Mac 上按作业拆分 DerivedData

当你的队列在滚动24 小时窗口内,可能出现三条及以上并发的xcodebuild进程触碰同一工作区家族(单体仓库、多 scheme、或共享二进制产物)时,隔离就值得投入。主干式 iOS 团队与白牌工厂尤其典型:它们往往在夜间叠加 UI 测试、归档与静态分析。若所有作业继承默认路径,你会看到SwiftEmitModule随机报错、模块图陈旧、XCTest 附件丢失等现象。把临时 IO 收敛到可预测的每作业树,不仅让trap清理变得简单,也能让平台团队在评估是否需要增租第二台 M4 时,先看磁盘曲线而不是凭感觉。

无头 SSH 构建机上我们反复看到的故障

  • 边编边索引——作业 A 的后台索引写入与作业 B 正在读取的索引片冲突,表现为“找不到类型”直到执行一次彻底清理。
  • 共享 TMPDIR——SwiftPM 与 clang 会留下海量小文件;另一条流水线的清理脚本若按前缀删除,可能删掉兄弟进程仍打开的路径。
  • xcresult 无配额——UI 测试默认记录视频与截图;三套套件叠加后,单个包体可轻松超过8 GB
  • 归档与测试交错——夜间先在共享 DerivedData 上归档,又立刻跑集成测试;归档步骤若半途中止,索引与模块缓存更容易出现半写入状态。
黄金法则:DERIVED_DATA_PATHTMPDIR、结果包路径,以及 SwiftPM 的-clonedSourcePackagesDirPath视为同一作业前缀;在入队时创建,在trap EXIT里销毁(长驻代理需先上传再删)。

隔离策略对比

方案 优点 缺点 适用场景
/Volumes/builds下为每作业设置DERIVED_DATA_PATH 清理确定;可按流水线做配额 无预热缓存时首编更慢 共享 Mac mini M4 的并行 CI
只读暖缓存 + 叠加复制 冷启动编译更快 rsync 复杂;纯 SSH 主机更难运维 Xcode 小版本一致的发布列车
拆分 macOS 用户账户 签名身份硬隔离 运维与许可成本更高 受监管的多租户混跑

NVMe 配额:可直接贴进手册的数字

产物 典型稳态 UI 测试尖峰
DerivedData(中型应用,Debug) 6–14 GB 额外约 3 GB临时写入
SwiftPM 检出 + 构建 1–4 GB 解析新标签时再约 2 GB
xcresult 包 单元测试400–900 MB 含视频时3–10 GB
余量建议:作业卷至少保留35%空闲;当可用空间低于50 GB时暂停新作业,并运行只删除36 小时以前前缀、且未被运行中 PID 引用的清扫任务。

租赁云 Mac 的八步执行手册

  1. 打戳——导出JOB_ID=$(date +%s)-$RANDOMROOT=/Volumes/builds/$JOB_ID;创建"$ROOT/DerivedData" "$ROOT/tmp" "$ROOT/results"
  2. 固定环境——export DERIVED_DATA_PATH="$ROOT/DerivedData"export TMPDIR="$ROOT/tmp",并同步TEMPTMP
  3. trap 清理——确认无其他作业复用同一JOB_ID后注册trap 'rm -rf "$ROOT"' EXIT;长驻代理需先归档结果。
  4. xcodebuild——测试加-resultBundlePath "$ROOT/results/Test-$JOB_ID.xcresult";归档仍把 IPA 推到对象存储,细节见IPA 导出与上传
  5. SwiftPM——传入-clonedSourcePackagesDirPath "$ROOT/spm",并与上文 SwiftPM 文章的注册表鉴权保持一致。
  6. 并行上限——在24 GB内存主机上,重型 UI 测试建议≤2路并发;脚本无法约束时由队列互斥。
  7. 上传——用ditto -c -k --sequesterRsrc --keepParent压缩 xcresult,校验哈希后再删本地 zip。
  8. 遥测——前后各记一次df -h;若墙钟超过42 分钟且磁盘占用高于 90%,多半是附件失控。

export JOB_ROOT=/Volumes/builds/ci-$CI_PIPELINE_ID mkdir -p "$JOB_ROOT"/{dd,tmp,res,spm} export DERIVED_DATA_PATH="$JOB_ROOT/dd" TMPDIR="$JOB_ROOT/tmp" TEMP="$JOB_ROOT/tmp" TMP="$JOB_ROOT/tmp"

平台团队一个下午就能加的指标挂钩

若磁盘隔离无人观测,就等于没做。建议在每个作业包装器里输出三个量:(1)编译结束后du -sk "$ROOT"(2)xcodebuild archive墙钟秒数;(3)/Volumes/builds所在卷的剩余 GB。推送到 Prometheus、CloudWatch,或写入 syslog 由 Vector 采集皆可。若 DerivedData 中位数体积周环比上升超过18%,通常意味着有人关闭了增量编译或新增了重型代码生成,而不是立刻需要加机器。

同时把 Xcode 构建号(xcodebuild -version)与JOB_ID打到同一行日志。同一台主机在16.216.3之间切换时,索引碎片不兼容会放大尾部延迟;把升级与指标尖峰对齐,可避免把问题误判为“云厂商不稳定”。若你夜间执行xcrun simctl delete unavailable,请避开高峰编译窗口,以免与作业队列争用同一条 NVMe 队列。

衔接 SwiftPM 与签名纪律

拆分 DerivedData 并不能替代描述文件与证书治理。若团队每周轮换证书,仍应按自动与手动签名流程操作,避免codesign在无界面环境卡住等待人工。若通过 SwiftPM 引入二进制依赖,请把Package.resolved纳入版本控制,并在每作业目录下镜像检出缓存——全局SourcePackages与按作业 DerivedData 混用仍可能在锁文件上竞态。

常见问题:租赁 Apple Silicon 上的 DerivedData

问题 实务答案
是否应把 DerivedData 软链到 NFS? 除非往返延迟稳定低于2 ms且能接受更慢的索引;更稳妥是本地 NVMe + 产物上传。
Xcode Server 旧布局是否仍相关? 现代 CI 可忽略/Library/Developer/XcodeServer;始终在脚本里显式传路径。
Rosetta 场景要注意什么? 避免 x86_64 模拟器与 arm64 前缀混用;按架构拆分目录,降低胖二进制重建风暴。

为何区域化的 Mac mini M4 仍然重要

目录隔离解决的是软件层面的竞态,但网络物理距离仍在:东京工程师从美国东部拉取 Git LFS 或符号表,RTT 会体现在分钟级等待上。把构建机放在 SCM、测试人员与审计日志同一大洲,才能把/Volumes/builds的价值吃满。MacXCode 在香港/日本/韩国/新加坡/美国的布局,正是为了让磁盘与网络同时贴近你的合规边界。请先阅读帮助中心里的 SSH 基线,再在连续14 个夜间窗口磁盘指标都健康后,才放大并发。

结论:按作业的DERIVED_DATA_PATH + TMPDIR + 显式xcresult路径,把“随机红构建”变成可度量的 IO 预算;随后你才能用定价页的数据决定下季度该租几台 Mac,而不是靠直觉拍板。

在裸机 M4 上扩展隔离构建池

香港 · 日本 · 韩国 · 新加坡 · 美国 · SSH / VNC