Оптимизация скорости и сравнение с альтернативами
Учимся делать пайплайны быстрыми и сравниваем GitLab с GitHub Actions и Jenkins.
Fail fast — принцип: упасть как можно раньше, чтобы не тратить минуты на дальнейшие шаги заведомо обречённого пайплайна.
Скорость пайплайна — это не косметика, а экономика команды. Если обратная связь приходит за две минуты, разработчик остаётся в контексте задачи и спокойно итерирует; если за двадцать — он переключается на другое, теряет нить, а возврат стоит дороже самой паузы. Помножьте это на десятки коммитов в день и на всю команду — и медленный CI превращается в постоянный налог на скорость продукта. Поэтому оптимизацию пайплайна стоит воспринимать как полноценную инженерную задачу с измеримой отдачей, а не как «приятный бонус, когда будет время».
Прежде чем что-то ускорять, полезно понять, из чего складывается время пайплайна: полезная работа (компиляция, тесты, сборка), накладные расходы (скачивание образа, клонирование, восстановление кеша) и ожидание (очередь к раннеру). Каждый рычаг бьёт по своей части: параллелизм режет полезную работу на критическом пути, лёгкие образы и кеш — накладные расходы, число раннеров — ожидание. Бессмысленно шардировать тесты, если все джобы стоят в очереди за единственным раннером: сначала найдите узкое место, потом бейте по нему.
Почему скорость важна
Медленный пайплайн убивает поток разработки: разработчик ждёт двадцать минут обратной связи и теряет контекст. Цель — быстрый, плотный конвейер, который даёт зелёный/красный сигнал за единицы минут. Рассмотрим главные рычаги.
Параллелизм и needs
Самый мощный рычаг — не делать последовательно то, что можно параллельно. Независимые джобы держите в одной стадии (идут параллельно), а через needs стройте DAG, чтобы быстрые ветки не ждали медленных. Большие тест-наборы шардируйте через parallel: N.
Ключевая разница между классическими стадиями и needs — в модели ожидания. Стадии — это синхронные барьеры: ни одна джоба следующей стадии не начнётся, пока все джобы текущей не завершатся, и быстрый линт будет зря ждать медленный интеграционный тест из той же стадии. needs переводит пайплайн в направленный ациклический граф (DAG), где джоба стартует, как только готовы её конкретные зависимости, а не вся предыдущая стадия. На практике переход со стадий на DAG нередко срезает общее время на треть, не добавляя ни одного раннера.
Шардирование через parallel: N — отдельный приём для тяжёлых тест-наборов. GitLab запускает джобу в N копий и через CI_NODE_INDEX и CI_NODE_TOTAL подсказывает каждой, какую часть тестов брать; набор, идущий 12 минут в один поток, проходит за 3 минуты в четыре. Важно лишь, чтобы тесты были независимы и не дрались за общие ресурсы вроде одной тестовой БД.
Кеш и лёгкие образы
Кешируйте зависимости с правильным ключом по lock-файлу — это убирает повторные минуты на установку. Берите минимальные образы (-alpine, -slim): тяжёлый базовый образ дольше скачивается на каждую джобу. Если собираете Docker — используйте слои и кеш сборки.
Размер образа недооценивают, а он умножается на число джоб. Если базовый образ весит 1 ГБ, его придётся тянуть на каждую джобу (контейнер ведь чистый), и эти секунды складываются в минуты на ровном месте. Alpine- или slim-вариант может весить десятки мегабайт вместо сотен. Здесь же стоит держать образы во внутреннем реестре GitLab рядом с раннерами: тянуть слои из соседнего сервиса быстрее, чем из внешнего хаба, и без риска упереться в его лимиты на анонимные pull-ы.
У кеша есть и тонкость про инвалидацию, которую важно отличать от отладочной. С точки зрения скорости нам нужен ключ, который не меняется без причины: если ключ завязан, скажем, на CI_COMMIT_SHA, кеш будет промахиваться на каждом коммите и пользы не принесёт. Идеальный ключ для зависимостей — хеш lock-файла: он стабилен, пока зависимости не менялись (попадание в кеш на каждом коммите), и сам собой меняется при их обновлении (корректная инвалидация). Один правильный ключ закрывает обе задачи разом.
Fail fast
Ставьте быстрые и часто падающие проверки (линт, юнит-тесты) раньше медленных (e2e, сборка образа). Тогда типичная ошибка остановит пайплайн за секунды, а не после десяти минут сборки. Дорогие джобы ограничивайте rules, чтобы они не запускались зря (например, e2e — только в MR).
lint:
stage: test
script: ["npm run lint"] # быстро, падает первым
e2e:
stage: test
needs: ["lint"] # не тратим время, если линт красный
script: ["npm run e2e"]
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'Принцип fail fast — это оптимизация по ожидаемому времени, а не по худшему. Большинство падений ловятся дешёвыми проверками: опечатка, провал линтера, упавший юнит-тест. Если поставить их в начало и связать дорогие джобы через needs с дешёвыми, типичный красный пайплайн умрёт за секунды и не сожжёт минуты на сборку образа, которая всё равно никому не понадобится. По сути, вы расставляете проверки в порядке «дешевизна делить на вероятность поймать баг».
interruptible: отменять устаревшее
Если в ветку прилетел новый коммит, старый пайплайн обычно уже не нужен. Флаг interruptible: true позволяет GitLab автоматически отменять устаревшие пайплайны при появлении нового — экономит раннеры.
Чтобы автоотмена работала, нужны флаг interruptible: true на джобах и включённая опция Auto-cancel redundant pipelines. Тонкость — что считать «безопасным к отмене». Линт и тесты прерывать можно без последствий. А вот джобу деплоя помечать interruptible опасно: оборванная на середине, она оставит окружение в полусломанном состоянии. Поэтому быстрые проверки — interruptible, необратимые операции деплоя — нет.
Сравнение с альтернативами
| Свойство | GitLab CI/CD | GitHub Actions | Jenkins |
| Конфигурация | .gitlab-ci.yml | workflow YAML | Jenkinsfile (Groovy) |
| Интеграция с платформой | максимальная (единый продукт) | высокая (с GitHub) | внешняя, через плагины |
| Порядок джоб | стадии + needs | needs | стадии/скрипт |
| Экосистема | встроенные фичи | Marketplace actions | огромный мир плагинов |
| Хостинг раннеров | SaaS + свои | SaaS + self-hosted | только свой сервер |
| Модель администрирования | минимум, всё встроено | минимум на GitHub | вы админите всё |
GitLab выигрывает интеграцией «всё в одном» и встроенными фичами (реестр, окружения, сканеры). GitHub Actions силён огромным Marketplace переиспользуемых actions. Jenkins — самый гибкий и старый, но требует ручного администрирования и плагинов. Выбор зависит от того, где живёт код и сколько вы готовы администрировать сами.
Стоит вглядеться в философские различия, потому что они объясняют сильные и слабые стороны каждого. GitLab построен как единый продукт: репозиторий, CI, реестр образов, окружения и сканеры живут в одном месте и знают друг о друге — это даёт бесшовность ценой привязки к экосистеме. GitHub Actions делает ставку на композицию из готовых блоков: тысячи actions из Marketplace позволяют собрать пайплайн почти без скриптов, но вы зависите от качества и безопасности чужих action-ов. Jenkins воплощает максимальную гибкость: Groovy-пайплайн может делать что угодно — но это сервер, который вы сами ставите, обновляете и чините, и «свобода» оборачивается операционной нагрузкой.
Практический вывод: выбор диктуется контекстом. Код уже в GitLab — встроенный CI почти всегда верный выбор: меньше движущихся частей. Команда живёт в GitHub — Actions органичнее. Jenkins оправдан там, где нужна экзотическая интеграция или есть наработанная инфраструктура. Концептуально же все три решают одну задачу — «на событие в репозитории запустить набор шагов в изолированной среде», — поэтому понимание GitLab CI переносится на остальные почти один в один.
Как работает под капотом
Время пайплайна — это критический путь по графу джоб плюс накладные расходы (скачивание образов, восстановление кеша, клонирование). Параллелизм и needs сокращают критический путь; кеш и лёгкие образы — накладные расходы; interruptible и rules — убирают лишнюю работу вовсе. Оптимизация — это сведение всех трёх к минимуму.
Полезно держать в голове, что критический путь — это не сумма всех джоб, а самая длинная цепочка зависимостей в DAG. Можно добавить сколько угодно параллельных джоб, и общее время не вырастет, пока они короче критической цепочки. Поэтому оптимизация начинается с поиска именно этой цепочки (её видно в визуализаторе пайплайна) и сокращения её медленного звена — ускорять джобы вне критического пути бессмысленно.
Частые ошибки
- Гнаться за параллелизмом, упираясь в лимит раннеров — джобы просто встанут в очередь.
- Ставить медленную сборку образа раньше быстрого линта — терять время на заведомо красных пайплайнах.
- Помечать
interruptibleджобу деплоя — обрыв на середине оставит окружение сломанным. - Оптимизировать джобу вне критического пути и не замечать узкое место.
Итоги
- Ускоряют: параллелизм +
needs, кеш по lock-файлу, лёгкие образы, fail fast,interruptible. - Ограничивайте дорогие джобы через
rulesи оптимизируйте критический путь DAG, а не отдельные быстрые джобы. - GitLab — интеграция «всё в одном»; Actions — Marketplace; Jenkins — гибкость ценой администрирования.