同一個提交在雲端 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 是否持續存取建置目錄;若已證實索引造成競爭,只調整獨立快取卷的索引設定,保留系統磁碟的正常搜尋能力。
平行 CI 工作可以共用同一個 DerivedData 嗎?
同一專案的循序建置可以考慮重用;平行工作應依儲存庫、分支或工作編號分開路徑,避免同時寫入造成鎖定競爭、快取失效與不可預期的清理結果。
為下一條開發流程選擇雲端 Mac
比較三種固定配置與四個亞洲節點,依實際工作負載選擇租用週期。每筆租用都對應一台獨享實體機,不是虛擬機器。