同一提交在云端 Mac 上连续构建三次,第一次正常,第二次突然变慢,第三次又恢复;CPU 没有持续满载,网络下载也早已结束。这类波动常被误判为配置不足,实际原因可能是 Spotlight 正在扫描 DerivedData、某个编辑器递归监听整个工作区,或多个 CI 任务同时写入同一缓存目录。解决它的关键不是先清空所有缓存,而是保留现场、量化 I/O,再逐项缩小竞争范围。
先建立可比较的构建基线
排查前固定代码提交、Xcode 版本、构建目标和缓存路径。不要把首次依赖下载与后续增量构建混在一组数据里,也不要一边构建一边执行清理脚本。
set -o pipefail
WORKSPACE="$HOME/ci/work/app"
DERIVED="$HOME/ci/derived/app-main"
PACKAGES="$HOME/ci/packages/app"
mkdir -p "$DERIVED" "$PACKAGES"
cd "$WORKSPACE"
xcodebuild \
-resolvePackageDependencies \
-clonedSourcePackagesDirPath "$PACKAGES"
for run in 1 2 3; do
/usr/bin/time -lp xcodebuild \
-workspace App.xcworkspace \
-scheme App \
-configuration Debug \
-destination 'generic/platform=iOS Simulator' \
-derivedDataPath "$DERIVED" \
-clonedSourcePackagesDirPath "$PACKAGES" \
build 2>&1 | tee "$HOME/ci/build-$run.log"
done
记录 wall time、最大常驻内存、日志中的依赖解析阶段,以及波动发生时是否存在其他任务。三次结果都慢,通常要检查项目或工具链;只有个别轮次异常,更像后台 I/O 竞争。
| 观察结果 | 优先检查 |
|---|---|
| CPU 不高但构建停顿 | 文件系统等待、目录锁 |
mds、mdworker 活跃 |
Spotlight 索引范围 |
| 编辑器关闭后恢复 | 递归文件监听 |
| 并行任务才出现 | 共享 DerivedData 或包缓存 |
用系统工具保留 I/O 现场
先查看索引状态,再在异常构建期间抓取文件访问。fs_usage 需要管理员权限,输出量很大,应先限制进程,再按目录关键词过滤。
mdutil -s /
sudo fs_usage -w -f filesys mds mdworker_shared |
grep -E 'DerivedData|SourcePackages|/ci/work/'
另开终端检查相关进程:
ps -axo pid,ppid,%cpu,%mem,etime,command |
grep -E 'mds|mdworker|xcodebuild|swift-frontend|SourceKit' |
grep -v grep
如果 mdworker 只是短暂读取新拉取的源码,不足以证明它是根因。更有价值的证据是:异常窗口内持续扫描大量中间产物,并且扫描路径与构建写入路径重合。
不要在取证前执行
rm -rf DerivedData。清空缓存可能让下一次构建更慢,也会抹掉判断“谁在反复访问哪些文件”的线索。
收窄 Spotlight 的索引边界
不建议直接关闭系统卷索引。开发者仍可能依赖系统搜索,而全局修改也会掩盖真正有问题的目录布局。更稳妥的方式是把高频生成、可随时重建的缓存放到独立 APFS 卷,再只调整该卷。
确认缓存卷挂载点后执行:
mdutil -s /Volumes/CICache
sudo mdutil -i off /Volumes/CICache
mdutil -s /Volumes/CICache
源码、文档和需要搜索的资料继续保留索引;DerivedData、包下载缓存、测试附件与归档临时目录放入该卷。若后续要恢复:
sudo mdutil -i on /Volumes/CICache
sudo mdutil -E /Volumes/CICache
不要把签名材料、长期产物和临时缓存混在同一清理边界内。索引排除只解决扫描竞争,不替代权限控制与数据分类。
治理递归文件监听与共享缓存
编辑器、代码生成器和开发服务器常会监听仓库根目录。如果监听规则包含 .git、DerivedData、测试附件或包缓存,每次构建都可能触发数万次无意义事件。
缩小监听范围
将监听目标限制到源码和配置目录,并显式排除以下内容:
.git与检出过程产生的临时文件;- DerivedData、归档和测试结果目录;
SourcePackages与其他可恢复依赖缓存;- 日志、覆盖率报告和生成代码输出。
先退出可疑编辑器或代理,再运行同一基线。如果波动消失,逐个恢复进程,比一次修改所有工具更容易确认责任方。
为并行任务分配独立写路径
串行构建可以复用项目缓存;并行 CI 不应让多个任务写入同一个 DerivedData。路径至少包含仓库与任务标识:
SAFE_REPO="${REPO_NAME//[^a-zA-Z0-9_-]/_}"
SAFE_JOB="${JOB_ID//[^a-zA-Z0-9_-]/_}"
export DERIVED_DATA="$HOME/ci/derived/$SAFE_REPO/$SAFE_JOB"
export PACKAGE_CACHE="$HOME/ci/packages/$SAFE_REPO"
mkdir -p "$DERIVED_DATA" "$PACKAGE_CACHE"
包缓存可共享读取,但更新依赖时仍可能产生写入竞争。高并发环境可以先在受控步骤完成依赖解析,再让构建任务使用稳定的解析结果。
清理与回归验收要分开
清理脚本应避开正在运行的 xcodebuild,按任务目录删除过期缓存,而不是每天无条件清空顶层目录。先只列出候选项:
DERIVED_ROOT="$HOME/ci/derived"
if ! pgrep -x xcodebuild >/dev/null; then
find "$DERIVED_ROOT" \
-mindepth 2 -maxdepth 2 \
-type d -mtime +7 -print
fi
确认层级和保留周期后,再把删除动作放进独立任务。验收时重新运行三次固定基线,并同时观察 fs_usage。合格结果不是某一次特别快,而是索引进程不再持续扫构建目录、并行任务没有共享写路径、连续构建的阶段分布保持一致。
最后把 Xcode 版本、提交号、DerivedData 路径、包缓存路径、活跃监听进程和索引状态写入构建诊断记录。以后再遇到波动,可以先比较环境差异,而不是从清缓存重新猜起。
常见问题
关闭整个系统盘的 Spotlight 能解决构建抖动吗?
可能暂时降低索引活动,但不建议把它作为默认方案。更稳妥的做法是先确认 mds 或 mdworker 是否持续访问构建目录,再只对独立缓存卷调整索引,并保留系统盘的正常搜索能力。
DerivedData 应该按项目共享还是按任务隔离?
串行构建可以按项目复用;并行 CI 应按仓库、分支或任务编号隔离写入路径。多个任务同时修改同一目录,通常会增加锁竞争、缓存失效和难以复现的清理问题。
为下一条开发流水线选择云端 Mac
比较三档固定配置与四个亚洲节点,按实际工作负载选择租用周期。每笔租用对应一台独享物理机,不是虚拟机。