Эта статья — модель обвала iOS CI/CD при масштабировании (Scaling Failure Model), а не калькулятор ёмкости. До нашего подключения команда за четырнадцать месяцев выросла с 8 до 20 человек, а топология runner'ов почти не менялась: релизные недели с красными PR, сборки, застревающие в Queued, TestFlight с двухдневным опозданием. Ниже — почему CI замедляется при росте команды и на каком участке pipeline узкое место.
Если нужен ответ «сколько Mac на 500 сборок в день», смотрите сестринскую статью 3 Cloud Mac на 500 iOS-сборок в день — это внедрение после нашего входа. Здесь — почему всё рухнуло до этого.
Обзор: как CI скатывается от «нас больше» к Failure Zone
Рис. 1 — сводная схема механизма всей статьи; для postmortem или onboarding команды она удобнее деталей pipeline. Обвал редко одиночная точка — это наложение цепочки ниже.
Рис. 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 · Pipeline iOS CI/CD: слои L0 / L1 / L2 и типичные узкие места (клиент 2024–2025)
L0: lint / лёгкие проверки; L1: интеграционная сборка PR и тесты Simulator; L2: Archive, нотаризация, upload. Клиент рухнул в Queue и смешанном пуле runner'ов, а не в компиляции L1 — раздувание matrix часто путают с «Xcode тормозит».
Исходная точка: почему CI «хватало» на 8 человек, но не масштабировалась
Середина 2024, типичная конфигурация:
- 1 Mac mini M2 в офисе, попутно Self-hosted Mac Runner (см. доку GitHub о self-hosted runner'ах)
- Остальные job'ы — hosted macOS runner'ы GitHub
- Monorepo с 2 приложениями, один YAML workflow на всё
- Около 80–120 macOS job'ов в день, P95 очереди < 5 минут
CTO сказал дословно: «CI не узкое место, не тратьте на это время.» — На 8 человек это верно; после удвоения продуктивности CI стала невидимым налогом: никто не планировал ёмкость, feature-команды только наращивали workflow.
Healthy baseline: что считать «нормой»?
Если смотреть только неделю обвала при 20 человек, легко решить: «большая команда = много Mac». Добавляем сравнение healthy baseline 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% |
| macOS job'ов на push · P99 | < 6 | 19 |
| macOS job'ов / день | 80–180 | 500+ (релизная неделя) |
| Топология runner'ов | L1/L2 по labels в разных пулах | hosted + офисный mini в одной куче |
Ключевое: run duration почти не изменился, queue time вырос ~в 10 раз — проблема в очередях и workflow, не в компиляции Xcode. Именно здесь формулировка «CI тормозит» вводит в заблуждение.
Хронология: три предупреждения, три раза списали на «разовое»
Первое (12 человек, +4 iOS): merge PR вырос с 6 до 14 в день. build queue на hosted runner'ах днём показывала 25–40 минут ожидания. Реакция: «GitHub сегодня глючит.» Никто не строил график queue_wait_seconds.
Второе (16 человек, +2 backend в том же repo): в офис добавили второй Mac mini, оба как self-hosted, без label-разделения очередей. Archive и PR Build в одном пуле — лёгкие job'ы скачком с 8 до 35 минут, тяжёлые со случайным падением нотаризации. Реакция: «купим ещё диск.»
Третье (20 человек, крупные версии двух app): релизная неделя, macOS job'ы с 120 до 280+ в день. Slack #ios-ci с дежурством 24/7. Реакция: «закупка 8 Mac mini в процессе» — тот самый сценарий из кейса 500 сборок/день; тогда ещё никто не разложил 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 в общем пуле, подпись падает sporadically |
| 20 чел. | 500+ | 47 мин | Failure Zone | Пик релизной недели, CI фактически мёртва |
Источник: логи GitHub Actions workflow (скользящее окно 14 дней); при 20 — пик релизной недели двух app. Объём job'ов растёт быстрее headcount — matrix и Archive на PR, а не просто «больше людей».
Два нюанса в таблице: между 16 и 20 объём job'ов резко растёт, доля 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-слоты с маршрутизацией по labels — офисный mini занят разработчиком, знаменатель падает мгновенно. Если одновременно верны любые два условия ниже, прекращаем «ещё один Mac mini» и рефакторим pipeline:
- queue_wait_seconds P95 > 30 мин и доля Archive job'ов > 30 % → разделить L1/L2, убрать полный Archive с PR
- macOS job'ов на push · P99 > 15 → matrix/path filter вне контроля, сначала меньше job'ов, потом железо
- L2 queue P95 > 40 мин 3 дня подряд и cache hit > 60 % → структурное масштабирование, выделенный release-пул
Неделя обвала при 20: все три условия — Failure Zone, а не Warning, который лечится пакетами минут.
GitHub Actions queue time: первый показатель, который уходит вразнос после 12 человек
Многие читают «CI тормозит» как медленную компиляцию и крутят DerivedData. В логах клиента вразнос ушёл queue time — job'ы долго в Queued. GitHub разделяет queue_wait_seconds и run duration; смотреть только второе — оптимизировать компиляцию, пока узкое место в org concurrency и пулах runner'ов.
Org macOS concurrency: GitHub Actions usage limits. После 12 человек в PR-шторм днём: runner'ы не медленные — job'ы ждут слот — Queued в UI часто списывают на сеть или Xcode. При разборе: время выполнения и ожидания job'а, разделять wait и run.
При 12 queue P95 уже 25–40 минут. Labels fast/archive (вместо двух mini в одной куче) отложили бы конфликт подписи. Топология runner'ов: матрица эластичный пул vs постоянные ноды.
Пять узких мест масштабирования (Scaling Taxonomy)
Пять категорий — наши внутренние теги mobile CI, привязаны к рис. 2, удобны для postmortem.
① Capacity bottleneck · исчерпана org macOS concurrency
При полной hosted concurrency лёгкие job'ы и Archive в одной очереди — быстрые очереди блокируются медленными, как кривая GitHub Actions queue time выше. Надёжнее, чем пакеты минут: вынести L2 из hosted-пула; иначе org concurrency — первый потолок.
② Resource contention · офисный Mac mini как «запасная dev-машина»
Runner на машине с SSH для инженеров — рано или поздно локальный xcodebuild debug. Runner и повседневная разработка делят CPU/диск — таймауты или sporadic compile error. При 16: два mini «пятница мертвы, понедельник оживают» — Remote Desktop и смена ветки.
③ Workflow design debt · полный Archive на каждом PR
При 8 — shortcut «зелёный PR = можно релизить»: Archive + upload в конце каждого pull request workflow. 6 merge/день ещё терпимо; при 20 доля Archive с 10 % до 35 %+ — медленные job'ы заливают переполненную очередь. Правильно: разделить L1 интеграцию и L2 Archive — сделали после нашего входа, см. слои job'ов в сестринской статье.
④ Scaling explosion · matrix job'ы monorepo растут экспоненциально
При 20 один push — до 22 macOS job'ов: людей в 2,5 раза больше, job'ов в 4. Самая часто пропускаемая точка обвала в ритме продукта.
⑤ Crypto / signing contention · Keychain и подпись в день релиза
Два self-hosted Mac параллельно с Archive — на 16 GB RAM sporadically ещё ок; плюс Match unlock, нотаризация, upload TestFlight — блокировки Keychain и codesign превращаются в «тот же код ошибки случайно». Релизная неделя: четыре раза «сертификат не найден», локально на Mac ок — типично параллельные job'ы в одной signing-среде, не просроченный сертификат. Док: Apple Xcode Signing and Capabilities.
Их «фиксы» — и почему стало хуже
Докупить минуты GitHub: разгружает только hosted-очередь, доля Archive та же — деньги потрачены, P95 > 60 мин.
Третий Mac mini в офисе: по-прежнему без labels fast/archive, три машины в одной куче — чаще падения подписи.
Запрет merge по пятницам: частота PR искусственно ниже, пик релиза ещё выше.
timeout-minutes: 180 на job: время Queued не входит в timeout — больше job'ов долго держат слоты.
Единственная частично рабочая попытка не доведена: Release на отдельной машине — всё ещё офисный mini, ночью некому unlock Keychain, очередь L2 к понедельнику.
Три цифры для мониторинга — которых не было до обвала
До обвала в Grafana были только success rate и средняя длительность. Первая неделя после входа — три метрики локализовали 80 % проблемы (разбивка queue_wait_seconds: официальный мониторинг GitHub):
- queue_wait_seconds P95 (по L0/L1/L2) — «медленная компиляция» vs «в очереди»
- Доля Archive job'ов (еженедельно) — workflow design вне контроля?
- macOS job'ов на push · P99 — взрыв matrix?
Неделя обвала при 20: L1 queue_wait P95 47 мин, Archive 38 %, job'ов/push P99 19. Два порога превышены — менять топологию, не закупку в первую очередь.
Порядок остановки кровотечения: топология, потом число Mac
Первые две недели после входа — без новых машин, четыре шага:
- Убрать Archive из PR workflow, L2 только nightly + release branches
- Ужать path filter — README/backend не триггерят iOS matrix
- Офисный mini снят с runner'ов, без конкуренции с dev
- Hosted runner'ы только на пики L1, L2 на выделенные ноды
Только это: queue_wait P95 релизной недели с 47 до 22 мин — ещё мало, но доказало: главная причина топология и workflow, не «таинственная формула Mac». Потом пулы fast/release на Cloud Mac и проверка ёмкости; эластичный vs постоянный пул: GitHub Actions self-hosted macOS runner: матрица решений.
Три сигнала для растущей команды
Если идёте с 12 к 20 и видите хотя бы одно — на этой неделе 30 минут на CI, не ждать столкновения релизов:
- Разработчики спрашивают «можно merge без зелёной CI?»
- На офисном Mac стикер «не перезагружать, runner работает»
- Сборка TestFlight ok, но «ждём окно upload» — норма
За этими сигналами обычно уже минимум два из пяти точек обвала выше.
Postmortem Summary (для цитирования)
Root cause
При росте команды топология CI не масштабировалась: частота PR и ширина matrix росли, runner'ы остались «hosted + офисный mini в одной куче», PR workflow с L2 Archive — двойное насыщение queue и signing.
Contributing factors
- Нет мониторинга
queue_wait_seconds, Queued принят за медленную компиляцию - Второй Mac mini без labels fast/archive, Archive и PR в одном пуле
- Слишком широкий path filter monorepo, правка README триггерит iOS matrix
- Пики job'ов релизной недели не связаны с закупкой/сменой топологии
Fixes tried (неэффективно или ухудшило)
- Больше минут GitHub — только hosted-очередь, доля Archive та же
- Третий офисный mini в общей куче — чаще падения подписи
- Запрет merge по пятницам — пик переехал на релизную неделю
timeout-minutes: 180— Queued не в timeout
What worked (первые две недели после входа)
- Archive убран с PR; L2 только nightly + release branches
- Path filter ужат; офисный mini без runner'а
- L2 на выделенные release-ноды — queue P95 47min → 22min
FAQ
GitHub Actions долго в Queued — не медленная компиляция?
Сначала queue_wait_seconds и run duration. Высокая доля Queued — узкое место очередь или пул runner'ов, не компиляция. Узел Queue на рис. 2 и healthy baseline на рис. 3.
При каком размере iOS-команды CI чаще всего ломается?
В этом кейсе: 12 Warning / 16 Structural Debt / 20 Failure Zone. Надёжнее метрики: queue P95 > 30 мин и Archive > 30 % — сначала рефактор pipeline.
При росте: сначала железо или pipeline?
Слои job'ов, убрать Archive с PR, ужать matrix. Иначе железо только откладывает обвал — пики queue вернутся на следующей релизной неделе.
Это та же статья, что «500 сборок/день — сколько Mac»?
Нет. Здесь почему обвал (Failure Model); сестринская — ёмкость и число машин (Sizing). Сначала эта + диагностика, потом Sizing.
Рецепт Failure Zone: сначала изоляция L2, потом проверка пула runner'ов
Все три условия выполнены → Failure Zone (как низ рис. 1)
- queue_wait_seconds P95 > 30 мин
- Доля Archive job'ов > 30 %
- macOS job'ов на push > 15
Инженерные шаги первыми (до закупки)
1. L2 isolation — убрать Archive с PR; release/nightly только на label macos-archive.
2. Dedicated Mac pool — L2 вне офисной dev-среды и смешанного hosted-пула; L1 остаётся hosted или эластичный self-hosted на пики.
3. Проверка — release workflow на изолированной ноде, queue P95 vs healthy baseline рис. 3 (< 8 мин).
Число машин и алгоритм пулов fast/release: 3 Cloud Mac на 500 iOS-сборок в день; эластичный vs постоянный: матрица решений GitHub Actions self-hosted macOS runner.
Для дневного PoC выделенной release-ноды проверки L2 isolation: Mac cloud hosting или главная VPSSpark — изолированная очередь Archive на Cloud Mac. Проверка топологии, а не покупка всего кластера сразу.