这篇是一份 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——这是验证拓扑,不是第一步就买满集群。