先给结论:每天约 500 次 iOS CI 构建,多数团队不需要 8 台 Mac mini,3 台 Apple Silicon Cloud 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 可再拆,见下图)。
图 1 · 每天约 500 次 iOS CI 构建组成(该团队 GitHub Actions 两周实测)
容量怎么算:从 91 机时压到 63 机时
500 次/天 spread 在 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 资源池决策。
三台 Cloud 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 台专属 Cloud Mac 前,托管 Runner 的 queue_wait P95 曾到 40+ 分钟;自建 fast/archive 双队列后,L1 的 P95 降到 8 分钟以内。
常见做法是:3 台 Cloud Mac 做基线 Self-hosted 池,仅在开源贡献周或极端尖峰用托管 Runner 顶 L0——成本可控,且密钥不必每周迁移。
Self-hosted Runner 与 Xcode Cloud 对比
Xcode Cloud 适合 Apple 生态深度绑定、希望零运维的团队;Self-hosted Mac Runner 适合要控队列、控缓存键、要把 Archive 与内部 Jenkins/Buildkite 混用的组织。对比要点:
| 维度 | Xcode Cloud | Cloud Mac + Self-hosted Runner |
|---|---|---|
| 计费 | 分钟包 + 并发上限 | 订阅/按日 Cloud 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 台 Cloud 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,三台 Cloud Mac 挂 Agent 的标签策略与本文 fast/archive 拓扑一致。
Cloud Mac 与本地 Mac mini CI 的区别
自建机房 8 台 Mac mini:CapEx 高、折旧、断电与证书机房审计。3 台 Cloud Mac:OpEx 可预测、按日可 PoC、区域可选近 Git 仓库。本地 CI 适合每天单机编译 >14 小时且规格多年不变;Cloud 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 小版本,否则升级后会出现「命中但链接失败」。
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) 按日加第四台 Cloud 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 台。
FAQ:Google 常抓的短答
500 次 iOS 构建需要几台 Mac?
在 Job 分层(PR / 单测 / Archive 分列)与 DerivedData 缓存到位后,多数团队 3 台 Apple Silicon Cloud 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 冲突,表现为偶发超时,而不是稳定、可复现的编译错误。
Cloud Mac 比 Xcode Cloud 便宜吗?
对长期高频 iOS CI/CD、需要专属 Self-hosted Runner 与密钥常驻的团队,3 台 Cloud Mac 基线池通常比按分钟计费的 Xcode Cloud 更可控。低频发版、零运维优先的团队,仍应优先试 Xcode Cloud。
Bitrise 和 3 台 Cloud Mac 怎么选?
Bitrise 省运维、适合快速上线;每天约 500 次 Job 且要控队列与缓存时,Self-hosted Mac Runner + Cloud Mac 往往年化更低,且 L2 并发由你定义。
VPSSpark:2 Fast + 1 Release 的 Cloud 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 台。