PR 已经合并,GitHub Actions 任务却还在排队;发布任务一启动,普通构建就把 Mac 占满。

最快解法:不要按开发者人数买 Mac Runner,而要按峰值任务到达量、任务时长、目标排队时间和备用容量计算;PR、UI 测试、发布任务先分池,再决定长期节点或短期扩容。

这篇文章适合三类人:

  • 正在维护 GitHub Actions 自托管 macOS Runner,却无法判断何时扩容的 DevOps 工程师。
  • 需要为多个 iOS 项目安排构建容量和发布窗口的移动研发负责人。
  • 准备租用远程 Mac,希望先估算节点数量和租赁周期的平台或采购负责人。
01

先确认:在线 Runner 数量为什么不等于有效并发

在 GitHub Actions 中,任务会根据 runs-on 指定的 labels 和 Runner Group,寻找匹配的在线空闲节点。只有真正处于空闲状态、并且标签与权限都符合的 Runner,才算可用容量。GitHub 文档还说明:如果任务被分配后,Runner 在 60 秒内没有接收任务,任务会重新进入队列;没有匹配的在线节点时,任务会继续等待,超过 24 小时才会失败。GitHub 自托管 Runner 路由说明

因此,下面几种情况都不能简单相加:

  • Runner 数量:代表可以承接独立 Job 的节点数,不能直接代表 Xcode 测试 Worker 数。
  • 工作流并发:代表同一时间产生了多少个 Job,但仍要等待匹配节点。
  • 单机内部并行:例如 xcodebuild 启动多个 Simulator Worker,会在一台 Mac 内部争用 CPU、内存、磁盘和图形资源。

容量估算至少需要四类输入:

  1. 每个工作流在高峰窗口内的任务到达时间和数量。
  2. 每类任务的典型执行时长,以及 P95 或更高分位时长。
  3. 从任务进入队列到开始执行的排队时间。
  4. 失败重跑、节点离线、签名失败和发布截止时间带来的额外占用。

平均执行时间只能作为初始参考。集中提交、版本冻结和发布日的短时峰值,往往比全天平均值更能决定你需要几台 Mac。

02

第一步:用工作流历史算出基础节点池

PR 构建通常包括代码检查、单元测试、依赖解析和增量编译。它的特点不是每天固定产生一批任务,而是在工作时间持续到达。基础节点池的目标,是把开发反馈控制在你能接受的窗口内,而不是追求全天零排队。

可以按下面的方式建立表格:

工作流类型 需要记录的证据 容量处理方式 扩容触发条件
PR 检查与增量构建 到达频率、排队时间、P95 执行时长、取消率 作为基础 Mac 节点池,优先保障反馈 高峰排队持续超出目标,且优化后仍无法恢复
单元测试 测试耗时、失败重跑、缓存命中情况 可与普通构建共享,但需观察磁盘和内存 测试重跑明显挤占 PR 构建窗口
Simulator 与 UI 测试 Worker 数量、单机资源、有效完成时间、稳定性 独立测试池或限制单机并行 增加 Worker 后总耗时变长或失败率上升
签名与发布 归档时长、钥匙串访问、等待时间、失败恢复 专用或预留发布容量 发布窗口被普通任务占满,交付受阻
夜间回归与峰值任务 任务截止时间、峰值并发、可延迟程度 采用排程或短期增加远程 Mac 排程后仍无法在截止时间前完成

一个可执行的估算模型是:

基础并发 ≈ 高峰窗口内的任务到达率 × 该类任务的典型占用时长
实际节点数 = 基础并发 + 波动余量 + 失败重跑余量 + 故障备用

这里的“典型占用时长”必须来自你自己的运行记录。它不是 GitHub Actions 的官方容量公式。对于不同项目,依赖规模、编译缓存、测试计划、签名步骤和产物上传方式都会改变结果。

先检查是否存在无效占用,再考虑加节点:

  • ✅ 对同一 PR 的旧提交启用取消策略,避免新提交到达后继续编译过时版本。GitHub 的 concurrency 支持对同组任务执行取消或排队控制。GitHub Actions 工作流语法
  • ✅ 把不需要 macOS 的格式检查、文档检查和部分静态分析移出 Mac Runner。
  • ✅ 合并重复的依赖安装、代码检查和单元测试步骤。
  • ❌ 不要因为某一天出现多个排队任务,就直接把全年基础容量翻倍。
  • ❌ 不要把开发者数量当作并发数。10 名开发者可能只有少量并行任务,也可能在发布前同时触发几十个 Job。
03

第二步:把 Simulator 与 UI 测试从编译容量中单独拆出来

iOS UI 测试最容易制造“看起来并发增加,实际完成更慢”的假象。

一条工作流可以启动多个测试 Worker,也可以创建多个 Simulator 实例。但这些 Worker 仍然共享同一台 Mac 的 CPU、内存、磁盘 I/O 和模拟器运行资源。Apple 的资料显示,xcodebuild 支持通过 -parallel-testing-worker-count-maximum-parallel-testing-workers 控制并行测试规模;这些参数会影响测试 Worker 数量,而不是增加独立 Mac 节点。Apple Xcode 10 发布说明

建议在真实项目上做三轮试运行:

  1. 固定代码版本、依赖缓存和测试集合,记录单 Worker 的完整耗时。
  2. 逐步提高 Worker 数量,同时记录 CPU、内存、磁盘 I/O、Simulator 启动失败和测试重试。
  3. 比较“总墙钟时间”和“有效通过的测试数量”,不要只看 Worker 数量。

如果从 2 个 Worker 增加到更多 Worker 后,单个任务耗时明显上升,或 UI 测试开始出现随机失败,那么新增 Worker 的收益已经被资源争用抵消。此时应限制单机并行,并增加独立测试节点,而不是继续在同一台 Mac 上堆 Simulator。

Xcode 还提供 -showBuildTimingSummary,可以查看构建阶段的任务耗时。它适合帮助你区分编译慢、模块准备慢、链接慢,还是测试前置步骤慢。Apple Xcode 构建计时说明

这也是判断 Xcode CI 是否需要新增节点的重要证据:如果排队时间长,但单机执行时间稳定,问题主要是容量;如果排队不长、执行时间却持续变慢,问题可能是单机并行、缓存、依赖或磁盘压力。

04

第三步:发布任务不要和普通构建争抢同一容量

签名和发布任务的频率可能不高,但它们的失败代价更高。只按全天平均负载决定 Runner 数量,容易低估发布窗口的风险。

发布任务至少要评估四个隐性成本:

  • 钥匙串与证书边界:发布节点通常需要访问签名证书、Provisioning Profile 或密钥材料。
  • 工作区污染:归档、导出和缓存残留可能影响后续任务。
  • 失败恢复时间:普通构建失败可以重跑,发布失败可能直接延迟交付。
  • 路由等待:如果所有节点都被 PR 任务占用,发布 Job 会进入队列。

GitHub Actions 支持用 labels 和 Runner Group 做路由。你可以把普通构建标记为 macos-build,把 UI 测试标记为 macos-ui,把发布节点标记为 macos-release,再让工作流通过组和标签组合选择节点。GitHub Runner 标签与组路由

Runner Group 不只是容量工具,也是一层访问边界。GitHub 文档说明,Runner Group 可以限制哪些仓库能够使用其中的 Runner,并控制不同任务的访问范围。GitHub Runner Group 管理说明

一次完整发布任务应该验证:

  1. 标签是否只匹配受控发布节点。
  2. 普通 PR 任务是否无法占满发布预留容量。
  3. 钥匙串、证书和工作区初始化是否可重复。
  4. 失败后能否清理环境并在另一台节点恢复。
  5. 发布任务的排队时间是否满足版本窗口。

如果发布节点只在少数时间使用,可以保留一个预留容量,再在版本发布前临时增加 远程 Mac。如果每周都有固定发布窗口,且多个项目共享签名环境,则更适合长期保留独立发布节点。

05

第四步:夜间回归和发布峰值优先采用弹性容量

你不需要按全年最高并发永久配置所有 Mac Runner。

可以把任务分成三组:

  • 即时任务:PR 检查、增量构建、阻断合并的单元测试。
  • 可延后任务:夜间回归、全量 UI 测试、非阻断型兼容性验证。
  • 有截止时间的峰值任务:版本归档、发布前全量测试、集中提交后的批量构建。

即时任务应拥有稳定的基础节点。可延后任务应设置时间窗口,避免在工作时间和 PR 构建争抢。峰值任务则比较三种方式:

✅ 增加固定节点:适合高峰频繁出现、长期利用率也较高的团队。
✅ 调整排程:适合任务可以延迟,且截止时间宽松的团队。
✅ 短周期扩容:适合发布周、验收周或临时增加项目的情况。

GitHub 的工作流并发控制可以帮助你取消过时 PR 任务,也可以让发布任务按顺序等待。但并发控制只能改变任务进入队列的方式,不能替代 Mac 节点本身的计算容量。GitHub Concurrency 说明

06

第五步:用一轮试运行确定基础、测试、发布和备用容量

没有项目数据时,不建议直接给出“需要 3 台”或“需要 5 台”的结论。更稳妥的做法,是先建立一轮小规模试运行。

试运行步骤

  1. 按场景分组:至少分为 PR 构建、UI 测试、发布任务和夜间回归。
  2. 记录原始数据:保存任务到达时间、排队时间、执行时间、失败原因和重跑次数。
  3. 先优化工作流:取消旧提交,移除不必使用 macOS 的步骤,合并重复检查。
  4. 逐步增加并发:每次只改变一个变量,例如 Runner 数量或 UI Worker 数量。
  5. 观察资源状态:记录 CPU、内存、磁盘空间、Simulator 状态和节点在线情况。
  6. 验证故障恢复:主动停止一台节点,确认任务能否重新排队并由备用节点接管。
  7. 确定容量角色:输出基础节点、测试节点、发布预留和故障备用四类结论。

GitHub 还建议关注自托管 Runner 的在线、活动和离线状态,并查看 Runner 应用日志与 Job 日志。GitHub 自托管 Runner 监控与排障

在安全敏感的发布环境中,可以进一步评估临时 Runner。GitHub 说明,临时 Runner 可以在处理一个 Job 后自动注销,有助于降低前一任务残留信息影响后续任务的风险;但日志必须转存到外部位置,否则故障排查会变困难。GitHub Self-hosted Runner 参考

07

常见问题:从队列现象反推节点数量

一台自托管 Mac Runner 通常能处理多少个并行 Job?

容量规划建议先按一台 Runner 同时处理一个活动 Job 的保守模型计算。GitHub 会把任务分配给匹配标签和 Runner Group 的在线空闲节点;如果在同一台 Mac 内启动多个测试 Worker,那属于机器内部并行,不等于获得了多个独立 Runner。

Xcode CI 的等待时间达到什么程度,才值得增加节点?

不要用单次排队时间做决定。你应按 PR、UI 测试和发布工作流分别观察高峰窗口;如果高分位排队时间持续超过团队可接受的反馈窗口,且取消旧任务、合并检查和调整排程仍无效,再增加基础节点或测试节点。

UI 测试和编译任务适合放在同一批 Runner 上吗?

低负载团队可以共享,但 UI 测试会额外争用 Simulator、磁盘和内存资源。只要 UI 测试导致编译变慢、失败率升高,或发布任务等待,就应通过 labels 和 Runner Group 分流,而不是继续提高单机并行度。

任务到达量和执行时长怎样换算成节点需求?

先统计高峰窗口内每类任务的到达率和典型执行时长,再估算基础忙碌度,并加入失败重跑、任务波动和故障备用余量。这个计算是项目内部的容量模型,不是 GitHub 或 Apple 给出的固定公式。

什么时候适合保留长期节点,什么时候适合临时增加容量?

持续到达的 PR 构建适合保留基础节点;夜间回归和版本发布的短时峰值,更适合排程或短周期增加远程 Mac。只有当高峰反复出现、基础节点长期接近满载,并且发布隔离与备用需求稳定存在时,才适合转为长期容量。

08

你的节点数量应该如何落地

触发证据 优先动作 适合的容量方案 不建议的做法
排队主要来自过时 PR,失败率正常 优化取消和并发策略 维持现状 立即购买或长期租用更多节点
PR 高峰持续排队,但 UI 与发布影响不大 增加基础容量 扩充 PR 构建节点 让 UI 测试和发布继续混用
UI Worker 增加后任务变慢或不稳定 降低单机并行并分池 独立 UI 测试节点 把 Worker 数量当成新增 Mac 数量
发布窗口经常等待普通任务结束 设置 labels、Runner Group 和预留节点 独立发布容量 只按全天平均负载规划
夜间或版本发布才出现短峰值 先调整排程,再临时扩容 短周期远程 Mac 按全年最高并发长期配置
节点离线后任务无法按时恢复 增加故障备用并演练恢复 备用节点或临时节点 把所有节点都当作 100% 可用

如果你正在从零搭建 远程 Mac 构建池,可以先参考 KVMNODE 的远程 Mac 方案,再按项目真实的任务时长和发布窗口选择租赁周期。对于需要固定 Apple Silicon 环境的团队,也可以查看 Mac mini M4 租赁选项,但最终节点数量仍应由你的队列记录决定,而不是由设备名称决定。

如果你当前使用的是单台本地 Mac 或普通云主机,常见问题是资源被开发任务占用、UI 测试与编译互相拖慢、发布凭据难以隔离,峰值期间还缺少故障备用。这类方案短期能运行,长期却很难同时满足反馈速度、发布可用性和多项目并发。对需要临时增加容量、验证新项目或覆盖版本发布窗口的团队,租用 KVMNODE 的真实远程 Mac,更适合先做短周期试运行,再依据排队和失败数据决定是否保留长期节点。