工程實務

雲端 Mac 建置 I/O 波動排查:Spotlight 索引與檔案監聽治理

雲端 Mac 建置 I/O 波動排查:Spotlight 索引與檔案監聽治理

同一個提交在雲端 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 使用率不高但建置停頓 檔案系統等待、目錄鎖定
mdsmdworker 活躍 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、測試附件或套件快取,每次建置都可能觸發數萬個沒有意義的事件。

縮小監聽範圍

將監聽目標限制在原始碼與設定目錄,並明確排除以下內容:

先結束可疑的編輯器或代理程序,再執行相同的基準測試。如果波動消失,逐一恢復程序會比一次修改所有工具更容易確認責任來源。

為平行工作分配獨立寫入路徑

循序建置可以重複使用專案快取;平行 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 mini

為下一條開發流程選擇雲端 Mac

比較三種固定配置與四個亞洲節點,依實際工作負載選擇租用週期。每筆租用都對應一台獨享實體機,不是虛擬機器。

選擇配置並租用