這篇是一份 iOS CI/CD 擴編崩潰模型(Scaling Failure Model)——不是容量計算器。我們接手前,團隊十四個月從 8 人擴到 20 人,Runner 拓撲幾乎沒動:發版週 PR 全紅、建置長時間停在 Queued、TestFlight 晚兩天。下文回答「為什麼人一多 CI 就慢」,以及瓶頸落在 pipeline 哪一段。
若你關心「500 次/天要買幾台 Mac」,請看姊妹篇 3 台 Cloud Mac 支撐 500 次 iOS 建置——那是接手後的落地;本篇是接手前為什麼崩。
一眼看懂:CI 如何從「人多」滑向 Failure Zone
圖 1 是整篇的總覽機制图——比 pipeline 細節更適合作 postmortem 開場或團隊 onboarding。崩潰 rarely 是單點,而是下面這條鏈路上的疊加。
圖 1 · CI 崩潰機制總覽(Scaling Failure Cascade)
該客戶 12 人卡在 Queue,16 人在 Runner/Archive 分叉同時亮燈,20 人落入底部紅區。與 healthy baseline 的對照見下文。
iOS CI/CD 架構圖:從 PR 到 TestFlight 的瓶頸地圖
圖 2 是一次 macOS Job 的執行路徑(L0/L1/L2),標註該案例裡兩處最先打滿的位置。它能幫你區分:是編譯慢,還是 Job 根本沒在跑。
圖 2 · iOS CI/CD Pipeline:L0 / L1 / L2 分層與典型瓶頸(該客戶 2024–2025)
L0:lint / 輕量檢查;L1:PR 整合編譯與 Simulator 測試;L2:Archive、公證、上傳。該客戶崩在 Queue 與 Runner 混池,而非 L1 編譯本身——矩陣膨脹常被誤讀成「Xcode 變慢」。
起點:8 人時「夠用」的 CI,為什麼沒跟著長
2024 年中,團隊配置很典型:
- 1 台辦公室 Mac mini M2,兼職 Self-hosted Mac Runner(見 GitHub 自託管 Runner 說明)
- 其餘 Job 走 GitHub 託管 macOS Runner
- Monorepo 裡 2 個 App,workflow 一套 YAML 打天下
- 每天 macOS Job 約 80~120 次,排隊 P95 在 5 分鐘以內
CTO 的原話是:「CI 不是瓶頸,別在這上面花時間。」——8 人時成立;人效翻倍後 CI 成了隱形稅,因為沒人做容量規劃,只有 feature team 在堆 workflow。
Healthy baseline:對照什麼算「正常」?
只看 20 人崩潰週,容易誤判「團隊一大就必須買很多 Mac」。我們補一張健康基線 vs 崩潰週對照——數字來自該客戶 8 人檔實測,以及我們經手過的 mid-size iOS 團隊經驗區間(非業界標準,但足夠做內部 review)。
| 指標 | Healthy baseline(8~12 人 · 拓撲合理) | 該客戶 · 20 人崩潰週 |
|---|---|---|
| L1 queue_wait P95 | < 8 分鐘 | 47 分鐘 |
| L1 run duration P95 | 12~18 分鐘 | 14 分鐘(編譯並未變慢) |
| Archive 類 Job 占比 | < 15% | 38% |
| 單次 push · macOS Job P99 | < 6 | 19 |
| macOS Job / 天 | 80~180 | 500+(發版週) |
| Runner 拓撲 | L1/L2 標籤分池 | hosted + 辦公室 mini 混跑 |
關鍵對照:崩潰週 run duration 幾乎沒變,queue time 飆了近 10 倍——說明問題在排隊與 workflow,不在 Xcode 編譯能力。這也是「CI 變慢」一詞最容易誤導團隊的地方。
時間線:三次預警,三次被當成「偶發」
第一次(12 人,+4 名 iOS):PR 合併頻率從日均 6 次漲到 14 次。託管 Runner 的 build queue 在下午出現 25~40 分鐘等待。團隊反應:「GitHub 今天抽風吧。」沒人拉 queue_wait_seconds 曲線。
第二次(16 人,又招 2 後端共用同一 repo):他們在辦公室加了第二台 Mac mini,兩台都註冊成 self-hosted,沒有標籤分佇列。Archive 和 PR Build 搶同一池子,表現是:輕 Job 偶發 8 分鐘變 35 分鐘,重 Job 公證隨機失敗。反應:「再買個硬碟試試。」
第三次(20 人,雙 App 大版本撞車):發版週 macOS Job 從日均 120 次衝到 單日 280+。Slack 頻道 #ios-ci 開始 24 小時有人 @ 值班。反應:「採購在走 8 台 Mac mini 流程了。」——這就是我們在 500 次/天案例 裡看到的那個 8 台方案;當時還沒人算過 Job 分項。
圖 3 · 團隊規模 vs macOS Job/天 vs 風險檔位(該客戶 2024–2025 實測)
| 團隊人數 | macOS Job / 天 | GitHub Actions queue time(L1 P95) | 風險檔位 | 階段備註 |
|---|---|---|---|---|
| 8 人 | ~100 | < 5 分鐘 | Baseline | 單台辦公室 mini 夠用 |
| 12 人 | ~180 | 25~40 分鐘 | Warning Zone | 排隊首次持續超標 |
| 16 人 | ~260 | ~35 分鐘 | Structural Debt | 混跑 Archive,簽名偶發失敗 |
| 20 人 | 500+ | 47 分鐘 | Failure Zone | 發版週峰值,CI 實質崩潰 |
資料來源:GitHub Actions workflow 執行日誌(14 天滑動視窗);20 人取雙 App 發版週峰值。Job 量增長快於人數,主因是 matrix 膨脹與 PR Archive,而非單純「人多」。
表裡有兩個細節:Job 量在 16→20 人段陡增,Archive 占比從約 18% 升到 38%;queue time 與 Job 量非線性——人頭沒翻倍,排隊先翻倍。這正是擴編預算最容易算錯的地方。
失控閾值模型:12 人 Warning / 16 人 Structural Debt / 20 人 Failure Zone
我們把案例收斂成 weekly review 可用的框架——不是精確公式,但足夠判斷要不要動 Runner 拓撲:
(PR 頻率 × Matrix 寬度 × Archive 占比) ÷ 有效 Runner 池容量
分子三項都可從 GitHub Actions 日誌算出來;分母「有效 Runner 池」不是註冊台數,而是按標籤可分流的併發 Mac 槽位——辦公室 mini 被開發占用時,分母會瞬間打折。當下面任意兩條同時成立,我們建議停止討論「再加一台 Mac mini」,先重構 pipeline:
- queue_wait_seconds P95 > 30 分鐘 且 Archive 類 Job 占比 > 30% → 必須拆 L1/L2,停止 PR 全量 Archive
- 單次 push 觸發 macOS Job 數 P99 > 15 → 矩陣/path filter 失控,先減 Job 再談加機
- L2 連續 3 天 queue P95 > 40 分鐘 且快取命中已 > 60% → 進入結構性擴容,考慮專屬 release 池
該客戶 20 人崩潰當週三條全中——因此屬於 Failure Zone,而非「再買分鐘包」能解決的 Warning。
GitHub Actions queue time:12 人後第一個失控指標
很多團隊把「CI 變慢」理解成 compile 變慢,去調 DerivedData。在這家客戶日誌裡,真正失控的是 queue time——Job 長時間停在 Queued。GitHub 把 queue_wait_seconds 與 run duration 分開統計;若只看後者,會誤修編譯,而瓶頸在組織併發與 Runner 分池。
組織級 macOS 併發見 GitHub Actions usage limits。12 人以後下午 PR 風暴時,不是 Runner 慢,是 Job 在等空位——介面顯示 Queued 轉圈,常被當成網路或 Xcode 問題。排查時對照 Job 執行時間與排隊耗時,把 wait 與 run 拆開。
12 人檔 queue P95 已到 25~40 分鐘。若當時用標籤把 fast/archive 分池(而非兩台 mini 混跑),本可推遲後面的簽名互搶。Runner 拓撲選型見 彈性池 vs 常駐節點決策矩陣。
五個擴編瓶頸(Scaling Taxonomy)
下面五類是我們內部的 mobile CI 故障標籤——與圖 2 pipeline 對應,便於寫 postmortem。
① Capacity bottleneck · 組織 macOS 併發頂滿
託管 Runner 併發打滿時,輕 Job 與 Archive 一起排隊,快佇列被慢 Job 堵住——表現與上一節 GitHub Actions queue time 曲線一致。除加分鐘包外,更穩的是把 L2 遷出託管池;否則組織級併發永遠是第一個天花板。
② Resource contention · 辦公室 Mac mini 被當「備用開發機」
Runner 掛在工程師能 SSH 的機器上,遲早有人本地跑 xcodebuild debug。Runner 與日常開發爭 CPU/磁碟,表現為逾時或偶發 compile error。16 人時兩台 mini 曾「週五全掛、週一又好」,根因是 Remote Desktop 改分支。
③ Workflow design debt · 每個 PR 跑全量 Archive
早期 8 人時,「PR 綠 = 能發版」的偷懶寫法:每個 pull request workflow 末尾加 Archive + 上傳。人數少時一天 6 次還能忍;20 人時Archive 類 Job 占比從 10% 被 workflow 設計抬到 35%+,直接把慢 Job 灌進本已擁擠的佇列。正確做法是把 L1 整合建置與 L2 Archive 拆開——這在接手後的改造裡才落地,詳見姊妹篇的 Job 分層章節。
④ Scaling explosion · Monorepo 矩陣 Job 指數增長
20 人時單次 push 最高觸發 22 個 macOS Job——人多了 2.5 倍,Job 多了 4 倍。這是產品節奏下最容易被忽略的崩點。
⑤ Crypto / signing contention · 發版日 Keychain 與簽名互搶
兩台 Self-hosted Mac 同時跑 Archive,16GB 記憶體上偶發還能撐;一旦疊加 Match 解鎖、公證、TestFlight 上傳,Keychain 鎖與 codesign 競爭 會變成「同一錯誤碼隨機出現」。發版週他們出現過連續 4 次「憑證找不到」,本地 Mac 重跑卻成功——典型多 Job 搶簽名環境,而非憑證真過期。配置與能力說明見 Apple Xcode Signing and Capabilities 文件。
他們試過的「修復」,為什麼讓情況更糟
加買 GitHub 分鐘包:只緩解託管佇列,不改變 Archive 占比,錢花了,P95 仍超 60 分鐘。
辦公室第三台 Mac mini:仍無 fast/archive 標籤,三台混跑,簽名失敗率反而上升。
禁止週五合併:人為壓制 PR 頻率,發版週集中爆發,尖峰更高。
每個 Job 加 timeout-minutes: 180:Queued 時間不算 timeout,只會讓更多 Job 長時間占坑。
唯一有效但未做完的嘗試:把 Release 遷到單獨機器——可機器仍是辦公室 mini,夜間無人解鎖 Keychain,L2 佇列週一積壓。
監控裡該看、但沒人看的三個數
崩潰前他們的 Grafana 只有「成功率」和「平均耗時」。接手後我們第一週只加三個指標,就定位了 80% 問題(queue_wait_seconds 的拆分方式見上一節 GitHub 官方監控文件):
- queue_wait_seconds P95(按 L0/L1/L2 分桶)——區分「編譯慢」與「在排隊」
- Archive 類 Job 占比(每週)——workflow 設計是否失控
- 單次 push 觸發的 macOS Job 數 P99——矩陣是否指數膨脹
20 人崩潰當週的資料:L1 的 queue_wait P95 是 47 分鐘,Archive 占比 38%,push 觸發 Job P99 是 19。三個數任意兩個超標,就該動拓撲,而不是先開採購。
止血順序:先改拓撲,再談買幾台 Mac
我們接手後頭兩週沒買新機器,只做四件事:
- PR workflow 去掉 Archive,L2 僅 nightly + release 分支
- 收緊 path filter,README/後端目錄不再觸發 iOS 矩陣
- 辦公室 mini 下線 Runner,避免與開發爭用
- 託管 Runner 只頂 L1 尖峰,L2 全部遷專屬節點
僅這四項,發版週 queue_wait P95 從 47 分鐘降到 22 分鐘——仍然不夠,但證明了崩潰主因是拓撲與 workflow,不是「Mac 台數神秘公式」。之後才做 Cloud Mac 的 fast/release 分池與容量驗證;彈性池與常駐節點怎麼選,見 GitHub Actions 自託管 macOS Runner:彈性池與常駐節點決策矩陣。
給正在擴編團隊的三個信號
若你正在從 12 人往 20 人走,出現以下任一情況,建議當週開 30 分鐘 CI 專項,而不是等發版撞牆:
- 開發者開始問「能不能跳過 CI 先 merge」
- 辦公室 Mac 上出現「請勿重啟,Runner 在跑」的便條
- TestFlight 建置成功但「排隊等上傳視窗」成為常態
這三個信號背後,通常已經踩中了上文五個崩點裡的至少兩個。
Postmortem Summary(可引用)
Root cause
團隊擴編時未同步擴容 CI 拓撲:PR 頻率與 matrix 寬度上升,但 Runner 仍為「hosted + 辦公室 mini 混池」,且 PR workflow 持續跑 L2 Archive,導致 queue 與 signing 雙重飽和。
Contributing factors
- 無
queue_wait_seconds監控,把 Queued 誤判為 compile 慢 - 第二台 Mac mini 無 fast/archive 標籤,Archive 與 PR 搶同一池
- Monorepo path filter 過寬,README 改動觸發 iOS 矩陣
- 發版週 Job 尖峰未與採購/拓撲變更聯動
Fixes tried(無效或加重)
- 加買 GitHub 分鐘包——只緩解 hosted 佇列,Archive 占比不變
- 第三台辦公室 mini 混跑——簽名失敗率上升
- 禁止週五合併——尖峰轉移到發版週
timeout-minutes: 180——Queued 時間不計入 timeout
What worked(接手後前兩週)
- PR 去 Archive;L2 僅 nightly + release 分支
- 收緊 path filter;辦公室 mini 下線 Runner
- L2 遷專屬 release 節點——queue P95 從 47min → 22min
FAQ
GitHub Actions 一直 Queued、不是編譯慢怎麼辦?
先看 queue_wait_seconds 與 run duration。Queued 占比高說明瓶頸在排隊或 Runner 池,而非編譯。對照圖 2 的 Queue 節點與圖 3 健康基線排查。
iOS 團隊多少人時 CI 最容易崩?
本案例三檔:12 Warning / 16 Structural Debt / 20 Failure Zone。更可靠的是指標:queue P95 > 30 分鐘且 Archive > 30% 時,優先重構 pipeline。
團隊擴編時應該先加機器還是先改流水線?
先改 Job 分層、停 PR Archive、收緊 matrix。否則加機器只是推遲崩潰,queue 尖峰會在下一次發版週回來。
這和「500 次/天要幾台 Mac」是一篇文章嗎?
不是。本篇是為何崩(Failure Model);姊妹篇是崩後容量與台數(Sizing)。建議先讀本篇 + 診斷,再讀容量篇。
Failure Zone 處方:先 L2 隔離,再驗證 Runner 池
若同時滿足以下三條,說明已進入 Failure Zone(與圖 1 底部一致)
- queue_wait_seconds P95 > 30 分鐘
- Archive 類 Job 占比 > 30%
- 單次 push 觸發 macOS Job > 15 個
工程側優先動作(先於採購)
1. L2 isolation——PR 去掉 Archive;release/nightly 獨占 macos-archive 標籤。
2. Dedicated Mac pool——L2 遷出辦公室開發與 hosted 混池;L1 可繼續 hosted 或彈性 self-hosted 頂尖峰。
3. 驗證——用隔離的 release 節點跑一輪發版 workflow,對照圖 3 健康基線看 queue P95 是否回到 < 8 分鐘檔。
台數與 fast/release 分池演算法見 3 台 Cloud Mac 支撐 500 次 iOS 建置;Runner 彈性 vs 常駐見 GitHub Actions 自託管 macOS Runner 決策矩陣。
若需要按日 PoC 一台 release 專用節點做 L2 isolation 驗證,可在 Mac 雲主機方案 或 VPSSpark 首頁 開隔離 Archive 佇列的 Cloud Mac——這是驗證拓撲,不是第一步就買滿叢集。