Параллельные джобы в CI под нагрузкой: где узкое место
Параллельный CI ускоряет pipeline только до тех пор, пока хватает runner’ов, CPU, диска и внешних сервисов. Разбираем, почему очередь растёт, какие лимиты проявляются первыми и как измерять реальный выигрыш.

Параллельные джобы не гарантируют быстрый CI: при нехватке runner’ов pipeline просто превращается в очередь, а при избытке воркеров начинает конкурировать за CPU, диск, Docker daemon и лимиты внешних сервисов. Ниже — разбор для GitLab Runner 17.x и GitHub Actions hosted runners: как найти первое узкое место, измерить эффект и не принять рост числа машин за ускорение.
Что именно замедляется под нагрузкой
У CI есть несколько последовательных этапов. Сначала job ждёт свободный runner, затем получает рабочее окружение, скачивает исходники и зависимости, выполняет команды, сохраняет артефакты и освобождает ресурсы. Параллельность ускоряет только те части, которые действительно можно выполнять одновременно.
Если в очереди стоят 40 задач, а доступно 10 runner’ов, максимум 10 задач выполняются одновременно. Добавление ещё 30 параллельных job в конфигурацию не изменит этот предел. Если же runner’ов достаточно, узким местом может стать общий ресурс: один кэш, registry, база данных для интеграционных тестов или лимит API.
Параллельность — это не число job в YAML. Это число независимых рабочих мест, которым хватает CPU, памяти, диска и пропускной способности общих сервисов.
Три времени, которые нельзя смешивать
- Queue time — сколько job ждёт назначения runner’а.
- Execution time — сколько длится выполнение на runner’е.
- Pipeline time — сколько проходит от запуска pipeline до завершения последней обязательной job.
Например, выполнение тестов может занимать 8 минут, но pipeline длится 22 минуты: 10 минут ушли на очередь и ещё 4 — на получение зависимостей и публикацию отчётов. Увеличение CPU в runner’е не исправит очередь, а разбиение тестов на новые job не поможет, если все они скачивают один и тот же большой образ.
Runner[1] — агент, который получает job от CI-сервера и выполняет её в виртуальной машине, контейнере или на физическом сервере.
Как выглядит нагрузка на практике
Сценарий 1. Очередь runner’ов
Это самый очевидный случай. Pipeline разбит на 20 независимых job, но в пуле только четыре runner’а. Первые четыре запускаются, остальные ждут. Среднее время выполнения одной job почти не меняется, зато растёт время до старта.
Проверять нужно не только среднее значение. Важнее p95 queue time: время ожидания, быстрее которого стартуют 95% задач. Среднее может выглядеть приемлемо, пока редкие пики не начинают задерживать релизы на десятки минут.
Сценарий 2. Конкуренция внутри runner’а
На виртуальной машине с четырьмя vCPU можно запустить восемь job, но это не означает двукратное ускорение. Компиляторы, тестовые процессы и контейнеры начнут делить CPU. При нехватке памяти появится reclaim или swap, а дисковая подсистема будет обслуживать несколько параллельных распаковок и сборок.
Особенно плохо ведут себя job, которые создают много мелких файлов. На локальном SSD они могут быть быстрыми, а на сетевом или временном диске — упереться в IOPS. В результате загрузка CPU снизится, но время выполнения вырастет.
Сценарий 3. Общий внешний ресурс
Параллельные тесты часто обращаются к одному PostgreSQL, Redis, registry или облачному API. CI становится быстрее только до момента, когда ресурс исчерпывает соединения или пропускную способность. После этого появляются тайм-ауты, взаимные блокировки и повторные попытки. Повторы могут дополнительно маскировать проблему: pipeline формально проходит, но занимает дольше.
Отдельный риск — общий каталог кэша. Если несколько job одновременно обновляют один и тот же кэш без корректного разделения ключей, результатом становятся повреждённые архивы, непредсказуемые промахи кэша и редкие ошибки, которые трудно воспроизвести.
Четыре стратегии распараллеливания
| Подход | Что ускоряет | Цена | Главный риск |
|---|---|---|---|
| Больше job в одном pipeline | Независимые тесты и проверки | Больше стартовых операций, логов и артефактов | Очередь или конкуренция за общий ресурс |
| Больше runner’ов | Ожидание свободного исполнителя | Оплата, обслуживание и прогрев окружений | Внешний сервис становится новым узким местом |
| Матричный запуск | Одинаковую проверку для версий, платформ или баз | Число job растёт произведением измерений | Взрыв количества комбинаций |
| Шардинг тестов | Большой набор тестов, разделённый на группы | Нужен стабильный баланс групп | Одна медленная группа задерживает весь pipeline |
Добавлять runner’ы стоит, когда растёт именно queue time и у текущих машин есть свободные ресурсы. Если runner уже загружен CPU или упирается в память, масштабирование пула поможет только очереди, но не длительности job.
Матричный запуск удобен для проверки нескольких версий Python, Node.js или баз данных, но его стоимость растёт быстро. При пяти версиях языка и трёх базах вы получаете до 15 комбинаций ещё до добавления операционной системы. Для pull request обычно оставляют короткий набор, а полный матричный прогон переносят в ночной или релизный pipeline.
Как измерять эффект, а не угадывать
Перед изменением конфигурации зафиксируйте базовую линию минимум для 20–30 одинаковых pipeline. Записывайте commit, размер набора тестов, тип runner’а и состояние кэша. Сравнение одного удачного и одного медленного запуска почти всегда вводит в заблуждение.
- Разделите очередь и выполнение. Снимите queue time, execution time и pipeline time отдельно. Если очередь занимает больше 20–30% общего времени, сначала проверяйте пул runner’ов, а не тестовый код.
- Посмотрите загрузку runner’а. Нужны CPU, память, swap, место на диске, IOPS и сетевой трафик. Измеряйте их на протяжении всей job, а не только в момент завершения.
- Проверьте дисбаланс шардинга. Сравните длительность групп. Если самая долгая группа вдвое превышает медианную, добавление новых групп не даст линейного ускорения.
- Сосчитайте внешние запросы. Для registry, package repository, базы и API фиксируйте число соединений, тайм-ауты, ответы 429 и 5xx. Ошибки должны быть видны отдельно от ошибок тестов.
- Изменяйте один параметр. Сначала увеличьте число runner’ов или измените размер группы тестов, но не делайте оба изменения сразу. После этого сравните p50 и p95, а также стоимость pipeline.
Если наблюдаемости нет, добавьте как минимум метрики очереди, длительности job, доли повторных попыток и времени загрузки зависимостей. Для self-hosted runner’ов пригодятся системные метрики хоста; для GitHub-hosted runner’ов часть характеристик окружения задаётся платформой и зависит от типа runner’а.
Что ломается первым
Нестабильные интеграционные тесты
Параллельный запуск выявляет скрытую зависимость тестов от общего состояния. Два теста могут использовать одну схему базы, одинаковый порт, фиксированное имя временного файла или один тестовый аккаунт. Последовательный прогон проходит, параллельный — случайно падает.
Изолируйте данные по job, выдавайте уникальные имена ресурсов и явно удаляйте их после завершения. Если общая тестовая база неизбежна, измеряйте блокировки и ограничивайте число конкурентных групп. Простое увеличение тайм-аута лишь растянет сбой.
Кэш и зависимости
Общий кэш должен иметь ключ, зависящий как минимум от lock-файла, версии инструмента и платформы, если бинарные файлы между платформами несовместимы. Не записывайте несколько разных форматов в один ключ. Промах кэша увеличивает сетевую нагрузку именно в момент массового запуска.
Артефакты и логи
Двадцать job могут одновременно загрузить гигабайты отчётов. Это увеличит время завершения и нагрузку на хранилище даже после окончания тестов. Оставляйте артефакты, которые нужны для диагностики, а временные файлы удаляйте. Срок хранения и лимиты платформы проверяйте в актуальной документации, потому что они зависят от тарифа и типа хранилища.
Ограничения внешних API
Пакетный менеджер или registry может ответить ошибкой 429, когда десятки job начинают скачивать одинаковые зависимости. Настройте локальное зеркало или прокси для пакетов, но не отключайте проверку целостности. Для API используйте ограничение параллелизма и контролируемые повторы с задержкой.
Где параллельность перестаёт окупаться
Ускорение заканчивается там, где появляется последовательная часть pipeline. Если сборка одного общего образа занимает 12 минут, а после неё запускаются параллельные тесты, их число не сократит эти 12 минут. То же относится к ручным approval, миграциям базы и публикации единственного релизного артефакта.
Другой предел — стоимость. Больше runner’ов означает больше потребления минут или инфраструктуры, а не обязательно меньший lead time. Если pipeline сократился с 20 до 15 минут, но потребление compute выросло втрое, решение может быть оправдано только для частых релизов или строгого SLA.
Не стоит параллелить всё подряд. Короткие job с длительной инициализацией могут стать медленнее из-за повторной загрузки исходников, образов и зависимостей. Сначала объедините слишком мелкие задачи или вынесите общую подготовку в слой, который действительно переиспользуется.
Что делать дальше
Начните с графика p50 и p95 для queue time и pipeline time за неделю. Затем постройте матрицу: число одновременно работающих job, загрузка CPU и памяти, длительность, стоимость и число ошибок. По ней видно, где находится точка насыщения.
После нахождения точки насыщения закрепите лимит параллельности для тяжёлых тестов, разделите быстрые проверки и полный прогон, а затем добавьте алерты на очередь, 429, нехватку места и рост повторных попыток. Следующий показатель для продакшена — не только среднее время pipeline, но и p95 времени от push до готового артефакта.
[1] В GitHub Actions используется термин runner, в GitLab — runner; в обоих случаях речь идёт об исполнителе job, но лимиты, политика очереди и характеристики окружения зависят от платформы и тарифа.
Тест и проверочная: Быстрые ответы с объяснением, почему остальные варианты неверны
Спросить за 10 ₽Источники: GitHub Actions: Usage limits, billing, and administration, GitLab Runner: Advanced configuration
Частые вопросы
Почему после добавления runner’ов pipeline не стал быстрее?
Сколько параллельных job можно запускать одновременно?
Почему тесты проходят по одному, но падают параллельно?
Стоит ли увеличивать число runner’ов при длинной очереди?
Что будет, если не успеть завершить все параллельные job до релиза?
Как учитывать стоимость параллельного CI?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.