先給結論:每天約 500 次 iOS CI 構建,多數團隊不需要 8 台 Mac mini,3 台 Apple Silicon 雲端 Mac 就夠——前提是你們把 GitHub Actions 的 Job 分層,並用 Self-hosted Mac Runner 做快佇列與 Release 佇列隔離,而不是讓每台機器同時跑三個完整 Archive。
三個月前,我們接手一個 iOS 團隊的 Xcode CI 環境。當時他們的配置是:
- 18 名開發者
- 2 個 App(共用 Monorepo)
- GitHub Actions 作為唯一 CI
- 每天約 500 次 macOS Job(含 PR、單元測試、夜間發版)
採購方案寫的是:買 8 台 Mac mini 做機房自建 Runner。我們先把兩週的 workflow 日誌按 Job 類型拆開統計,發現真實構成與直覺差很多——大約 70% 是 PR 驗證與整合構建,20% 是 Simulator 單元測試,只有約 10% 是 Archive + 上傳(若把 TestFlight 上傳單獨記帳,則 Archive 與 Upload 可再拆,見下圖)。這也是為什麼在開發日誌裡常被誤判成「CI 永遠在跑 Archive」。
圖 1 · 每天約 500 次 iOS CI 構建組成(該團隊 GitHub Actions 兩週實測)
容量怎麼算:從 91 機時壓到 63 機時
500 次/天攤在 24 小時,均值約 21 次/小時——但真正要命的是尖峰:歐美上午合併、發版日前 PR 風暴,峰值常常是均值的 3~5 倍。容量必須按峰值排隊算,不能按午夜均值。
第二個變數是單次 Job 機時:Simulator 單元測試約 4~8 分鐘;帶相依解析的 PR 構建 12~20 分鐘;Release Archive + 公證 + 上傳常達 25~45 分鐘。用該團隊兩週 P50 粗算:輕 Job 6 分鐘、重 Job 35 分鐘;若錯誤地假設 30% 都是 Archive,會得出「必須 8 台 Mac」——而真實分項後,500 次裡重 Job 遠少於輕 Job。
無快取時,按 325 輕 + 175 重估算,每日約需 91 機時,超過 3 台 Mac 24 小時物理上限(72 機時)。開啟 DerivedData / SPM 快取、L2 串行與佇列標籤後,輕 Job 均時約 4 分鐘、重 Job 熱路徑約 22 分鐘,總需求降到約 63 機時,在峰值係數 1.3 左右可接受——這就是「3 台夠」的數學依據。
圖 3 · 每日 macOS 機時需求(同一 500 次/天模型)
Xcode CI 分層:別把 Archive 和 PR 塞進同一 Mac Runner 佇列
能撐住 500 次/天的 GitHub Actions 流水線,幾乎都會做三檔 Xcode CI 分層:
- L0:SwiftLint、單模組構建——走
macos-fast標籤。 - L1:PR 整合構建(不 Archive)——同樣走 Fast Mac Runner,每台可並行 2 個輕 Job。
- L2:Archive、公證、TestFlight——走
macos-archive,每台同時最多 1 個。
GitLab CI、Buildkite、Jenkins 的 macOS Agent 同理:用標籤路由,而不是一個大雜燴佇列。Buildkite 突發彈性見 Buildkite Mac Agent 對接雲端 Mac;Runner 池買租見 企業 Mac Runner 資源池決策。
三台雲端 Mac 怎麼分工(含拓撲圖)
我們落地的靜態三節點如下(名稱可改,職責建議不變):
| 節點 | Runner 標籤 | 職責 | 並行 |
|---|---|---|---|
| Mac-A | macos-fast |
PR Build、Unit Test | 2 個 L0/L1 |
| Mac-B | macos-fast |
與 Mac-A 對稱,承接快佇列 | 2 個 L0/L1 |
| Mac-C | macos-archive |
Archive、公證、Upload | 1 個 L2 |
圖 2 · 三節點與佇列拓撲(GitHub Actions Self-hosted Runner)
兩台 fast 扛吞吐,一台 release 扛確定性交付。發版日若 L2 排隊超標,可臨時給 Mac-A 加 macos-archive 標籤,但須關掉其 L0 並行,否則 Keychain 與 DerivedData 鎖競爭會導致簽章偶發失敗。三台 Xcode 小版本須與 Xcode 發行說明 鎖定一致。
GitHub Actions Mac Runner 為什麼容易排隊
很多團隊先買 GitHub 託管 macOS Runner 分鐘包,發現 PR 一多就排隊。原因通常不是「GitHub 慢」,而是:(1) 組織級 macOS 並行上限;(2) 每個 workflow 預設跑全量 Archive;(3) 沒有按 Job 類型拆 Self-hosted Mac Runner,快 Job 被慢 Job 堵住。該團隊在遷到 3 台專屬 雲端 Mac 前,託管 Runner 的 queue_wait P95 曾到 40+ 分鐘;自建 fast/archive 雙佇列後,L1 的 P95 降到 8 分鐘以內。
常見做法是:3 台雲端 Mac 做基線 Self-hosted 池,僅在開源貢獻週或極端尖峰用託管 Runner 頂 L0——成本可控,且金鑰不必每週遷移。
Self-hosted Runner 與 Xcode Cloud 對比
Xcode Cloud 適合 Apple 生態深度綁定、希望零維運的團隊;Self-hosted Mac Runner 適合要控佇列、控快取鍵、要把 Archive 與內部 Jenkins/Buildkite 混用的組織。對比要點:
| 維度 | Xcode Cloud | 雲端 Mac + Self-hosted Runner |
|---|---|---|
| 計費 | 分鐘包 + 並行上限 | 訂閱/按日雲端 Mac,機時可控 |
| 佇列 | 平台統一排隊 | 自管 fast/archive 標籤 |
| 金鑰 | ASC 整合省心 | Match / Keychain 需自建 runbook |
| 適合 | 低頻發版、少自訂 | 每天 500+ 次 iOS CI/CD |
分鐘包打滿後的切換訊號,見 Xcode Cloud 分鐘包與按天雲端 Mac 承接 Archive FAQ。
為什麼這個團隊沒有選 Xcode Cloud
他們評估過 Xcode Cloud,最終仍選 GitHub Actions + Self-hosted Runner,核心理由是:(1) 後端與 Android 已在 GitHub,不想拆雙 CI;(2) 需要自訂快取鍵與 Monorepo 矩陣;(3) 發版週 Job 數超過分鐘包舒適區,排隊不可自管。Xcode Cloud 並非不好,而是與「每天 500 次、要控佇列」的目標錯位。
Bitrise 與自託管 Mac Runner 成本直覺
Bitrise 等行動 DevOps SaaS 省去 Agent 維運,按並行套餐收費。對 18 人、雙 App、每天約 500 次 Job 的團隊,年化費用常高於 3 台雲端 Mac 訂閱,且 Archive 並行仍受平台檔位限制。Bitrise 更適合不願碰 Runner 的新創;願投入兩週搭 Self-hosted Mac Runner 的團隊,通常 3~6 個月收回節點成本。若已用 Bitrise,也可只把 L2 遷到專屬 release 節點,不必一夜全遷。
Buildkite Mac Agent 的優缺點
Buildkite 把佇列放在雲端、Agent 放在你機器上,彈性好、Artifact 留存清晰;缺點是多一層編排學習成本,小團隊可能覺得「為 3 台 Mac 上大砲」。該客戶曾 PoC Buildkite:突發擴容優秀,但最終仍用 GitHub Actions 原生 Self-hosted Runner,因研發只熟一套 YAML。若你已有 Buildkite,三台雲端 Mac 掛 Agent 的標籤策略與本文 fast/archive 拓撲一致。
雲端 Mac 與本地 Mac mini CI 的差異
自建機房 8 台 Mac mini:CapEx 高、折舊、斷電與憑證機房稽核。3 台 雲端 Mac:OpEx 可預測、按日可 PoC、區域可選近 Git 儲存庫。本地 CI 適合每天單機編譯 >14 小時且規格多年不變;雲端 Mac CI 適合峰值波動、外包週、審核季加公證通道。該團隊保留 2 台辦公 Mac 給開發,重 Xcode CI 全上雲,避免「8 台 mini 多數時間空轉」。CircleCI 雲 macOS 與雲 Mac Runner 的 SLO 對比,可延伸閱讀 CircleCI 雲 macOS vs 雲 Mac Runner FAQ。
佇列 SLO:三台機靠資料擴容,不靠感覺
建議監控:queue_wait_seconds(P95)、run_duration 按 L0/L1/L2 分桶、cache_hit_ratio、l2_concurrent(應 rarely > 3)。範例閾值:L1 排隊 P95 < 8 分鐘;L2 P95 < 25 分鐘。連續三天 L2 超標且快取命中已 > 60%,再談第四台 release 節點。
快取:63 機時的關鍵槓桿
DerivedData 按 branch + Xcode版本 做鍵;SPM/CocoaPods 鎖檔變更時 bust;fast 節點共享唯讀快取,release 節點本地 NVMe 存 L2 DerivedData。簽章材料走 vault,不進快取包。快取鍵必須含 macOS/Xcode 小版本,否則升級後會出現「命中但連結失敗」。這是把每日需求從 91 機時壓到 63 機時的主因。
GitHub Actions Self-hosted Runner 最小設定
三台各註冊 Runner:macos-fast ×2、macos-archive ×1。L2 務必加 concurrency,防止發版 Job 互 cancel:
concurrency:
group: ios-archive-${{ github.ref }}
cancel-in-progress: false
jobs:
archive:
runs-on: [self-hosted, macos-archive]
steps:
- uses: actions/checkout@v4
- run: xcodebuild archive -scheme App -archivePath build/App.xcarchive
release 節點用專用 macOS 使用者 + Match;重啟後先跑解鎖腳本再開夜間佇列。無人值守細節見 xcodebuild 官方文件 與 Fastlane 文件。
發版日:500 次變成 650 次怎麼辦
順序:(1) 停非關鍵 L0;(2) 託管 Runner 只頂 L1;(3) 按日加第四台雲端 Mac 48 小時。不要長期把 L2 提到每台 2 並行——會以公證隨機失敗還債。
何時必須第 4 台
- L2 排隊 P95 連續一週 > 40 分鐘,快取已優化。
- Monorepo 超 5 個 App 共一台 release,夜間視窗不夠。
- 堅持每個 PR 全量 Archive——應先改 Job 結構,不是先買機器。
反模式
每個 PR 跑 Archive;GitHub Actions Mac Runner 不拆標籤;兩台 Archive 擠一台 16GB M4;快取鍵不含分支;只看出敗不看 queue_wait——都會讓 3 台看起來「不夠用」,從而誤判要買 8 台。寫進開發日誌的「CI 永遠滿載」,多半是這類配置問題。
FAQ:搜尋常抓的短答
500 次 iOS 構建需要幾台 Mac?
在 Job 分層(PR / 單元測試 / Archive 分列)與 DerivedData 快取到位後,多數團隊 3 台 Apple Silicon 雲端 Mac 即可:2 台 Fast Mac Runner + 1 台 Release Mac Runner。若 500 次裡超過一半是全量 Archive,請先改流水線或規劃第 4 台 release 節點。
GitHub Actions Mac Runner 能同時跑幾個 Job?
16GB M4 上建議:2 個輕量 Job(PR Build、Unit Test),或 1 個 Archive Job。不要長期「2× Archive」並行。
Archive 為什麼不能高並行?
因為記憶體競爭觸發 swap、磁碟佇列延遲上升、Keychain 鎖與 codesign 衝突,表現為偶發逾時,而不是穩定、可重現的編譯錯誤。
雲端 Mac 比 Xcode Cloud 便宜嗎?
對長期高頻 iOS CI/CD、需要專屬 Self-hosted Runner 與金鑰常駐的團隊,3 台雲端 Mac 基線池通常比按分鐘計費的 Xcode Cloud 更可控。低頻發版、零維運優先的團隊,仍應優先試 Xcode Cloud。
Bitrise 和 3 台雲端 Mac 怎麼選?
Bitrise 省維運、適合快速上線;每天約 500 次 Job 且要控佇列與快取時,Self-hosted Mac Runner + 雲端 Mac 往往年化更低,且 L2 並行由你定義。
VPSSpark:2 Fast + 1 Release 的雲端 Mac 基線池
若你也在算「500 次/天要買幾台 Mac」,建議先用兩週日誌畫出圖 1 的分項占比,再按本文模型驗證 63 機時是否成立。VPSSpark 雲端 Mac mini M4 / M4 Pro 支援按日 PoC 與長期訂閱,適合掛載 GitHub Actions Self-hosted Mac Runner,把 PR 與 Archive 分到不同佇列。
查看 Mac 雲主機方案,或在 VPSSpark 首頁 選區域,用真實 workflow 跑一輪分桶——再決定 3 台是否夠用,而不是先買 8 台。