VPSSpark Cloud Mac CI 系列 #9。本篇回答一个被反复问到的问题:Flutter 团队只有 8~15 人,能不能用 2 台 Cloud Mac 把 CI 跑稳? 结论是可以——前提是你把 Android 与 iOS 拆开看:Android 构建留在 GitHub 托管的 ubuntu-latest,macOS 侧用两台自托管 Runner 分 macos-fast 与 macos-archive 两个池,并严格遵守「PR 不进 Archive」的铁律。下文给出负载模型、Job 分层、workflow 片段与落地清单;排队诊断与 Runner 注册细节分别见系列 #2 与 并网 FAQ。
1 · 反直觉结论:Flutter CI 的瓶颈不在 Dart
多数 Flutter 团队的 CI 红灯,不是 flutter test 慢,而是 iOS 侧占满了 macOS Runner 槽位——PR 里误跑了 flutter build ipa。
Flutter 的跨平台优势容易让人误以为「一条 workflow 打天下」。实际工程里,flutter analyze 与单元测试在 Linux 上也能跑;真正必须 macOS 的,是 CocoaPods 解析、Xcode 编译、签名与 IPA 导出。若 PR 路径里混进 Archive Job,一台 Cloud Mac 会被 25~40 分钟的 iOS 打包占满,其余 PR 全部 Queued——这和原生 iOS 团队踩过的坑一模一样,诊断公式仍是 wait time >> run time(详见 macOS runner queued 诊断)。
因此「2 台 Cloud Mac」不是「2 台万能机」,而是刻意设计的双池拓扑:一台专职快反馈,一台专职发版重任务。Android APK/AAB 继续走托管 Linux,不把宝贵的 macOS 槽位浪费在 Gradle 上。
2 · 负载模型:8~15 人团队需要多少 macOS 并发?
我们用中等活跃度的 Flutter 团队做基准(每人每天 1~2 次 PR,主分支 nightly + 每周发版):
快池(macos-fast)上的单次 Job 目标:analyze + 单测 + iOS 模拟器构建,wall time 控制在 8~14 分钟(有 pub/DerivedData 缓存)。Archive 池(macos-archive)上的 Release IPA + 公证,单次 20~35 分钟,但频率低——main 合并、tag、定时 nightly 才触发。两台机器若混池,Archive 会把 fast 池堵死;分开后,PR 的 P95 等待时间通常能压在 10 分钟以内,这对 Flutter 团队的代码评审节奏已经足够。
若团队超过 15 人、或 monorepo 里 matrix 拆多 flavor,才需要考虑第三台 Archive 冗余或弹性扩容——那属于 弹性池 vs 常驻节点 的决策,不是本篇「2 台起步」要解决的问题。
| 平台 | Runner | 典型 Job | 是否进 PR |
|---|---|---|---|
| Android | ubuntu-latest(托管) |
flutter build apk/appbundle、Android 单测 |
是 |
| iOS 快反馈 | Cloud Mac #1 · macos-fast |
analyze、单测、build ios --simulator |
是 |
| iOS 发版 | Cloud Mac #2 · macos-archive |
build ipa、公证、上传 TestFlight |
否 |
3 · Flutter Job 三层:对齐 CI Hard Rules
把 Flutter 流水线映射到系列通用的 L0/L1/L2 分层,便于和 on-call 手册对齐:
| 层级 | Flutter 内容 | 池 | PR 可否触发 |
|---|---|---|---|
| L0 | dart format --set-exit-if-changed、flutter analyze、轻量单测 |
Linux 或 macos-fast |
是 |
| L1 | 集成测试、flutter build ios --simulator、Widget 测试 |
macos-fast |
是 · 禁止 Archive |
| L2 | flutter build ipa、App Store Connect 上传、公证 |
macos-archive |
禁止 · 仅 main/tag/schedule |
三条铁律与原生 iOS 相同:Rule 1 PR 不得跑 L2;Rule 2 L2 必须在隔离池;Rule 3 fast 池不得被 Archive 阻塞。Flutter 特有的一点:L0 里大量步骤其实可以在 Linux 完成——若你把它也绑到 macos-fast,会白白消耗 macOS 并发;建议 PR 上 Linux Job 与 macOS Job 并行,macOS 只跑「非 mac 不可」的步骤。
4 · 双机拓扑:Cloud Mac #1 与 #2 怎么分工
图 1 · 2 台 Cloud Mac 支撑 Flutter 团队 CI 的典型拓扑
macos-fastL0/L1 · 8~14 min/Jobmacos-archiveL2 · main/tag 专用Cloud Mac #1(快池):注册 label [self-hosted, macOS, macos-fast, flutter]。预装固定版本的 Flutter SDK(用 fvm 或镜像内 pin 版本)、Xcode、CocoaPods。Runner 以常驻服务方式运行,开机自启;这台机器可以较激进地保留 pub cache 与 DerivedData,因为 Job 短、周转快。
Cloud Mac #2(Archive 池):label [self-hosted, macOS, macos-archive, flutter-release]。与快池物理隔离——不要在同一台 Mac 上挂两个 pool 的 Runner,否则 Rule 2 形同虚设。Archive 机存放 Distribution 证书与 App Store Connect API Key(用 Keychain + 最小权限令牌,注册步骤见 30~60 分钟并网清单)。发版前可在这台机上跑 flutter doctor -v 与一次 dry-run Archive,避免污染快池环境。
两台机器建议同区域、同 Xcode 大版本,减少「快池绿、Archive 红」的版本漂移。Cloud Mac 相对办公室 Mac 的优势是:镜像规格固定、网络到 GitHub 稳定、无需维护 UPS 与物理安全——适合作为 7×24 的 CI 节点。关于自托管 Runner 的安全边界,务必把 Archive 机权限收窄到单一 repo 或单一 org。
5 · Workflow 示例:PR 与 main 分流
下面片段展示核心分流逻辑(完整 pipeline 还需缓存、密钥与 artifact 步骤)。Flutter 官方持续交付文档对 iOS 签名有详细说明,此处假设证书已导入 Archive 机 Keychain。
jobs: android-pr: if: github.event_name == 'pull_request' runs-on: ubuntu-latest steps: - run: flutter analyze && flutter test - run: flutter build appbundle --release ios-pr-fast: if: github.event_name == 'pull_request' runs-on: [self-hosted, macOS, macos-fast, flutter] steps: - run: flutter analyze - run: flutter test - run: flutter build ios --simulator --no-codesign ios-release: if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') runs-on: [self-hosted, macOS, macos-archive, flutter-release] steps: - run: flutter build ipa --export-options-plist=ExportOptions.plist # 上传 TestFlight / 公证 — L2 only
注意 ios-pr-fast 里没有 build ipa。若产品要求在 PR 上验证真机包,应改为手动 workflow_dispatch 触发、且仍走 Archive 池——而不是默认 PR 事件。GitHub Actions 的workflow 语法里,用 concurrency 组把同一 PR 的旧 run 取消掉,能再省 20% 左右的无效排队。
6 · 缓存清单:Flutter 团队最该 persist 的四类目录
2 台 Cloud Mac 要跑稳,缓存比 CPU 型号更关键。建议在 Runner 用户目录持久化(或 nightly 快照):
PUB_CACHE/~/.pub-cache— Dart 依赖;换pubspec.lock时增量更新- Flutter SDK 目录 — 用
fvm固定版本,避免flutter upgrade漂移 ios/Pods+ CocoaPods 下载缓存 —pod install是 iOS 侧常见长尾- Xcode DerivedData — 对同一
ios/Podfile.lock命中率高时收益明显
快池与 Archive 池不要共享同一份 DerivedData 目录——Debug/Simulator 与 Release/Archive 的编译产物混写会导致诡异的链接错误。Android 侧的 Gradle cache 留在 ubuntu-latest 上用 Actions Cache 即可,无需占用 Cloud Mac 磁盘。
pod repo update,在 Archive 机先验证通过再同步快池。日常 PR 只复用缓存,不把 flutter pub upgrade 写进 CI 默认步骤。
7 · 落地清单:从 0 到双机并网
按顺序执行,可在 1~2 个工作日内完成 PoC:
- 开通 2 台同规格 Cloud Mac(建议 M4 + 16GB 起,iOS 编译内存峰值常见 12GB+)
- #1 装 Flutter/Xcode/CocoaPods,注册
macos-fastRunner;#2 额外导入签名材料,注册macos-archive - 拆分 workflow:Android 留
ubuntu-latest;PR macOS 仅 L1;main/tag 才 L2 - 配置 pub/DerivedData/Pods 持久化;观察 3 天 P95 wait time 是否 < 10 min
- 若 wait >> run 仍成立,先查 workflow 是否 L2 进 PR,再加机器
PoC 阶段可以只买一台 Cloud Mac 跑快池,Archive 暂时用 GitHub 托管 macos-latest(接受排队)——验证分池逻辑后再补第二台。这与「2 台起步」不矛盾:第二台是发版 SLA 的保险,不是 PR 反馈的必需品。
8 · FAQ
Flutter 单测能不能全部放 Linux,一台 Cloud Mac 够不够?
若 PR 不需要 macOS 编译验证,理论上可以——但多数团队会在 PR 上跑 build ios --simulator 抓原生插件问题。一台 Mac 跑 L1 可以,Archive 必须与快反馈隔离,否则发版时 PR 会全线 Queued。稳妥方案仍是 2 台。
2 台 Cloud Mac 和买 2 台 Mac mini 放办公室有什么区别?
拓扑相同;差异在运维:Cloud Mac 免硬件采购、镜像可快照、带宽到 GitHub 通常更稳。办公室 Mac 适合长期 7×24 且有机房条件;短周期或异地团队更常选 Cloud Mac。详见弹性池 vs 常驻。
Codemagic / GitHub 托管 macOS 还要不要保留?
可作 Archive 池的备份或 PR overflow。主路径自托管后,托管 macOS 适合「月发版 < 4 次」的小团队;活跃 Flutter 团队长期成本通常自托管更优。
monorepo 多 app 怎么算 Job 数?
按 flavor/matrix 相乘。若 PR matrix 产生 4 个以上 macOS Job,2 台 fast 池可能不够——应收敛 matrix 或加第三台,而不是把 Archive 塞进 PR。
2 台 Cloud Mac,让 Flutter 双端 CI 各就各位
Flutter 团队的双端流水线里,Android 可以留在 Linux,但 iOS 编译、签名与 IPA 导出离不开 macOS。用 2 台 Cloud Mac mini M4 分别承担 macos-fast 与 macos-archive,PR 反馈与发版重任务互不抢槽——比把 Archive 塞进 PR workflow、然后全员等 Queued 要划算得多。Apple Silicon 统一内存架构让 flutter build ios 与 Xcode 链接阶段更顺畅;约 4W 待机功耗也适合作为 7×24 CI 节点长期在线。
相比自建办公室 Mac 机房,Cloud Mac 按套餐开通、镜像可复刻、网络到 GitHub 更稳定,适合分布式 Flutter 团队快速落地双池拓扑。macOS 原生 Unix 环境对 Flutter、CocoaPods、Fastlane 开箱即用,无需额外虚拟化层。
若你正在规划 Flutter 团队的 iOS CI 迁移,从 2 台 VPSSpark Cloud Mac mini M4 起步是最小可行拓扑——立即了解套餐方案,先跑通快池 PoC,再补 Archive 池发版。