VPSSpark Блог
← К дневнику

Где ломается iOS CI/CD, когда в команде уже 20 разработчиков

Заметки с сервера · 2026.06.04 · ~16 мин чтения

Команда разбирает очередь iOS CI и сбой масштабирования
Обзор CI: queue time vs время компиляции

Эта статья — модель обвала iOS CI/CD при масштабировании (Scaling Failure Model), а не калькулятор ёмкости. До нашего подключения команда за четырнадцать месяцев выросла с 8 до 20 человек, а топология runner'ов почти не менялась: релизные недели с красными PR, сборки, застревающие в Queued, TestFlight с двухдневным опозданием. Ниже — почему CI замедляется при росте команды и на каком участке pipeline узкое место.

Если нужен ответ «сколько Mac на 500 сборок в день», смотрите сестринскую статью 3 Cloud Mac на 500 iOS-сборок в день — это внедрение после нашего входа. Здесь — почему всё рухнуло до этого.

Кратко: Когда CI команды из 20 человек ломается, редко хватает формулировки «мало Mac». В этом кейсе пять узких мест накладывались по цепочке: исчерпана concurrency → runner'ы конкурируют с разработкой → Archive на каждом PR → взрыв matrix job'ов → борьба за подпись. При 12–16 человек уже были признаки; при 20 без разделения очередей — Failure Zone.

Обзор: как CI скатывается от «нас больше» к Failure Zone

Рис. 1 — сводная схема механизма всей статьи; для postmortem или onboarding команды она удобнее деталей pipeline. Обвал редко одиночная точка — это наложение цепочки ниже.

Рис. 1 · Механизм обвала CI (Scaling Failure Cascade)

Рост частоты merge PRteam scaling · нелинейная стартовая точка
Взрыв Matrix / job'овmonorepo · path filter не работает
Затор в Queuequeue_wait_seconds ↑
Пул runner'ов переполненhosted concurrency · mini занят
Перегруз ArchiveL2 заливает PR-очередь
Конфликт Signing / Keychaincodesign contention
Failure Zonequeue P95 > 30min · Archive > 30%

У клиента: при 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)

PR Pushpath filter · matrix
Queuequeue_wait_seconds
Runner Poolhosted + self-hosted
L0/L1 Buildинтеграция · unit-тесты
L2 Archivexcodebuild archive
SigningKeychain · Match
UploadTestFlight
↑ Bottleneck · Queue (исчерпана org macOS concurrency) ↑ Contention · Runner Pool + Signing (офисный Mac и Keychain в конфликте)

L0: lint / лёгкие проверки; L1: интеграционная сборка PR и тесты Simulator; L2: Archive, нотаризация, upload. Клиент рухнул в Queue и смешанном пуле runner'ов, а не в компиляции L1 — раздувание matrix часто путают с «Xcode тормозит».

Исходная точка: почему CI «хватало» на 8 человек, но не масштабировалась

Середина 2024, типичная конфигурация:

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 фактически мёртва
Job'ов / день 0 200 400 600 100 180 260 500+ 8 12 16 20

Источник: логи 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'ов:

Риск обвала CI ∝ (частота 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. Проверка топологии, а не покупка всего кластера сразу.

Акция

CI iOS на 20 человек? Сначала разделите очереди

build queue · изоляция Archive · Cloud Mac Runner

На главную
Акция Смотреть тарифы