VPSSpark CI キュー診断スタンダード(クラウド Mac CI シリーズ #2)。読み順:Hook → 公式 → 症状 → Failure Model → Hard Rules → Runbook → FAQ。関連:#1 キャパシティ · #5 スケール失敗事例 · #3 セルフホスト TCO · #8 ビルド高速化。
1 · Hook:直感に反する結論
macOS CI で queued が続くインシデントの多くは、「Mac が足りない」のではなく、PR パスで L2(Archive)が動いていることが原因です。 現場の約 9 割はこのパターンに当てはまります。
GitHub Actions のタイムラインを開くと、赤い PR が「コンパイルが遅い」と報告されがちです。しかし 待ち行列(Queued) と 実ビルド(In progress) は別メトリクスです。SRE 視点では、まず「待ちが実行を圧倒しているか」を切り分けないと、Xcode バージョンアップや分課の追加は的外れになります。本稿はその切り分け用の規格です。
2 · コア:たった一つの公式
以降の章はすべて、この判定を展開したものです。オンコール Wiki にそのまま貼ってください。
マスター公式
CI queue problem ⇔ wait time >> run time
エンジニアリング上の読み: macOS runner queued は ランナープールの飽和 を示し、Xcode ビルドそのものの遅さではありません。
公式が false → 本記事の対象外。Failure 3 Layer Model へ進む前に #8(Xcode / キャッシュ)を確認。true → CI Hard Rules を適用。
日本の iOS チームでは、ホステッド macos-latest と自前ランナーを混在させるケースが多く、「分は余っているのに Queued」 という矛盾が起きやすいです。これは課金単位(minutes)と同時実行枠(concurrency)の混同が根底にあります。公式が true のときだけ、プール設計とトリガー設計に手を入れてください。
3 · 症状:Queued ≠ Running
GitHub Actions では Queued はジョブがワークフローに入ったが まだランナースロットがない 状態です。In progress で checkout、xcodebuild、署名が走ります。1 時間赤い PR を「遅いコンパイル」と見なし、タイムラインを開くと 待ち 52 分・実行 11 分 という典型パターンが出てきます。
よくある誤修正:Xcode を上げる、timeout-minutes を延ばす、GitHub の分を買い足す。Queued 時間は、設定したつもりのタイムアウトに含まれないことが多い ので、タイムアウトだけ伸ばしても待ち行列は解消しません。
計測は ジョブ実行時間(queue_wait_seconds と実行時間)で行います。週次で P95 を見ると、金曜のマージラッシュ前に Trigger Explosion を検知できます。
| 待ち時間 | 実行時間 | |
|---|---|---|
| UI | Queued | In progress |
| ボトルネック | GitHub Actions macOS runner queue / self-hosted runner queue | Xcode · 署名 · アップロード |
| 誤った対処 | 分の追加 · 新 Xcode | キャッシュ最適化(→ #8) |
Slack に「CI が遅い」と流れるとき、まず UI のバッジを確認してください。Queued が長いのに開発者が DerivedData を疑うのは、観測点が実行フェーズに偏っているためです。Runbook では待ちと実行を必ず分けて記録します。
4 · 説明:CI Queue Failure 3 Layer Model
wait time >> run time のとき、原因は次の SRE 型レイヤーのいずれか(または積み重ね)です。
| レイヤー | 意味 | シグナル | 今日やること |
|---|---|---|---|
| Capacity Limit | プラットフォーム · macos-latest |
ホステッドのみ macOS runner queued。分 ≠ 同時実行 | fanout 削減。Archive をホステッドから外す → #3 |
| Pool Misdesign | アーキテクチャ · セルフホスト | PR で Archive。fast/archive 共有。1 台占有で全 Queued | Hard Rules → #1 #6 |
| Trigger Explosion | 負荷 · ワークフロー fanout | matrix / paths / 重複 workflow。ジョブ数 > ランナー数 | トリガー絞り込み → #5 |
Capacity Limit — ホステッド macOS ランナー と組織の macOS 同時実行上限(制限)に当たります。Pool Misdesign — 典型根因はジョブ階層 L2 が PR に載っていること、プールの混在です。Trigger Explosion — ハードより YAML を直します。(Failure レイヤー ≠ ジョブ階層 L0/L1/L2。)
3 層は排他ではありません。金曜 17 時に matrix が増え(Trigger)、Archive が fast プールを占有し(Pool)、ホステッド枠も枯れる(Capacity)という「三重苦」は、20 人規模チームの事例記事 #5 でも再現性が高いです。切り分けは上表のシグナル列から始めてください。
5 · 修正:CI Hard Rules(MUST NOT 違反禁止)
Runbook 向けの規範文です。提案ではなく、違反はインシデント扱いにしてください。
Rule 1: PR MUST NOT run L2 Rule 2: L2 MUST run on isolated pool (macos-archive) Rule 3: fast pool MUST remain unblocked (fast ≠ archive) # Mapping PR → L0 + L1 only →macos-fastmain → L2 only →macos-archive
ジョブ階層リファレンス(Hard Rules の実装のみ):
| 階層 | 作業 | プール | ルール |
|---|---|---|---|
| L0 | モジュールビルド、lint、軽量テスト | macos-fast |
Rule 1 · PR で可 |
| L1 | PR 統合、シミュレータテスト | macos-fast |
Rule 1 · Archive MUST NOT |
| L2 | Archive、IPA、TestFlight | macos-archive |
Rules 2+3 · PR MUST NOT |
「PR で Archive を一度だけ」は例外に見えても、38 分の L2 が fast ラベルを奪うと、レビュアー全員の L0/L1 が Queued になります。例外運用はプール設計を壊すので、ナイトリー/main 専用ジョブに寄せてください。
図 1 · Pool Misdesign:L2 がスロット占有 → 全 macOS ジョブが queued
jobs: pr-fast: if: github.event_name == 'pull_request' runs-on: [self-hosted, macos-fast] release-archive: if: github.ref == 'refs/heads/main' || github.event_name == 'schedule' runs-on: [self-hosted, macos-archive]
ランナー構成:弾性プール vs 常時オン比較 · #1 サイジング · キュー健全化後 → #3。
6 · Runbook:1 ページ(オンコール)
0. CI queue problem ⇔ wait time >> run time ? ─No→ #8
1. Layer: Capacity | Pool Misdesign | Trigger Explosion
2. MUST: Rule1 PR no L2 · Rule2 L2 isolated · Rule3 fast unblocked
3. PR workflow has no archive/export/upload ?
4. wait P95 < 8min → then size runners (#3)
一行まとめ: macOS CI の queued の多くは PR パスの L2 であり、ハード不足ではありません。 macOS runner queued =プール飽和。wait >> run → Failure Model → Hard Rules の順で適用してください。
インシデント後の振り返りでは、「台数を増やした」だけでは再発します。Rule 1〜3 がリポジトリに入っているか、PR テンプレに「Archive ジョブを追加していないか」をチェック項目として載せると再発率が下がります。
7 · FAQ
macOS ランナーはなぜキューしやすいのか
Capacity Limit(同時実行上限)と Pool Misdesign(PR 上の L2)の組み合わせです。まず wait >> run で判定してください。
キュー問題か、遅いビルドか
CI queue problem ⇔ wait time >> run time。true → プール設計。false → #8。
セルフホストでも queued になる理由
Pool Misdesign:self-hosted runner queue はラベルとプール構成に従います。Hard Rules 違反はクラウド Mac では直りません。YAML を直してください。
GitHub Actions の分と concurrency の違い
minutes は課金、concurrency は同時スロットです。分を増やしても macOS runner queued は解消しません。
ランナープールの問題かどうか
wait >> run かつ自前 1 台が長時間占有 → Pool Misdesign。ホステッドのみ macos-latest queued → Capacity Limit。Runbook 参照。
キューは直ったがビルドがまだ遅い
#8(速度)。サイジング:#1。チーム拡大:#5。
プール分離の検証:fast プールの PoC が必要ですか
Hard Rules 適用後、ホステッドの GitHub Actions macOS runner queue から L0/L1 を逃がすには、macos-fast ランナーを 1 台立て、wait time P95 が 8 分未満になることを確認してから macos-archive を #1 に沿って追加してください。
オフィスネットワークを変えずに 日次 で分離 fast プールを試す場合は クラウド Mac プラン または VPSSpark トップ をご覧ください(トポロジ検証用。ルール遵守は別途必須)。TCO はシリーズ #3。