エンジニアリング実践

クラウド Mac のビルド I/O 変動を Spotlight とファイル監視から調査する

クラウド Mac のビルド I/O 変動を Spotlight とファイル監視から調査する

同じコミットをクラウド Mac 上で3回続けてビルドすると、1回目は正常、2回目だけ急に遅くなり、3回目には元へ戻ることがあります。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、最大常駐メモリ、ログ上の依存関係解決フェーズに加え、変動が発生した時点でほかのジョブが動いていたかどうかも記録します。3回とも遅い場合は、通常、プロジェクトまたはツールチェーンを確認します。一部の実行だけが異常なら、バックグラウンド 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

ディレクトリ階層と保持期間を確認してから、削除処理を独立したジョブとして実装します。検証時には、固定した基準テストを再び3回実行し、同時に fs_usage も監視します。合格の基準は、特定の1回だけが非常に速いことではありません。索引作成プロセスがビルドディレクトリを継続的にスキャンしないこと、並列ジョブが書き込み先を共有していないこと、連続するビルドで各フェーズの時間配分が安定していることが重要です。

最後に、Xcode のバージョン、コミット番号、DerivedData のパス、パッケージキャッシュのパス、動作中の監視プロセス、索引作成の状態をビルド診断記録へ残します。次に変動が発生したときは、キャッシュを消して推測をやり直すのではなく、まず環境の差分を比較できます。

よくある質問

システムボリューム全体で Spotlight を無効にすべきですか?

既定の対策としては推奨しません。まず mds や mdworker がビルド先へ継続的にアクセスしているか確認し、競合が確定した場合だけ専用キャッシュボリュームの索引設定を変更します。

並列 CI ジョブで DerivedData を共有できますか?

同じプロジェクトを直列にビルドする場合は再利用できます。並列ジョブではリポジトリ、ブランチ、ジョブ ID ごとに分離し、同時書き込みやロック競合、予測不能なキャッシュ無効化を避けます。

専用物理 Mac mini

次の開発パイプラインにクラウド Macを選ぶ

3種類の固定構成とアジアの4ノードを比較し、実際のワークロードに合ったレンタル期間を選択できます。レンタル1件につき専用物理マシン1台が割り当てられ、仮想マシンではありません。

構成を選んでレンタル