Стадии, порядок и DAG-пайплайны с needs
Учимся ломать жёсткую последовательность стадий и строить граф зависимостей через needs.
DAG (directed acyclic graph) — направленный ациклический граф зависимостей джоб, позволяющий запускать джобу сразу, как готовы её зависимости, не дожидаясь всей стадии.
Скорость пайплайна — не косметика, а деньги и нервы команды. Если каждый коммит ждёт зелёного билда двадцать минут, разработчики начинают коммитить реже, складывать изменения в большие пачки и обходить CI — и вся идея непрерывной интеграции рушится. Поэтому умение управлять порядком джоб так же важно, как умение их писать. GitLab даёт два инструмента для этого: стадии (stages) и явные зависимости (needs). Первый прост и нагляден, второй — мощнее и быстрее. Большинство реальных пайплайнов используют их вместе, и важно понимать, когда какой брать.
Ограничение чистых стадий
Стадии удобны, но строги: стадия build начнётся только после того, как закончатся все джобы стадии test. Если одна джоба тестов медленная, она задерживает весь конвейер, даже если конкретная сборка от неё не зависит. Это и есть «бутылочное горлышко» линейных пайплайнов.
Стоит проговорить, откуда вообще берётся такая строгость. Модель стадий — это барьер синхронизации: GitLab гарантирует, что ни одна джоба стадии N+1 не стартует, пока хоть одна джоба стадии N ещё бежит. Это удобно для рассуждений («сначала всё собрали, потом всё протестировали, потом всё задеплоили») и для ментальной модели новичка. Но эта же гарантия становится тормозом, как только у вас появляются независимые цепочки работы. Представьте монорепозиторий с фронтендом и бэкендом: тесты фронтенда не зависят от сборки бэкенда, но в модели чистых стадий test-frontend всё равно ждёт, пока дособерётся build-backend. Получается искусственная задержка ровно там, где логической связи нет.
needs: явные зависимости
Ключ needs разрывает оковы стадий: джоба запускается, как только завершились перечисленные в needs джобы, не дожидаясь остальной стадии. Так строится DAG.
stages:
- build
- test
- deploy
build-frontend:
stage: build
script: ["echo build FE"]
build-backend:
stage: build
script: ["echo build BE"]
test-frontend:
stage: test
needs: ["build-frontend"]
script: ["echo test FE"]
test-backend:
stage: test
needs: ["build-backend"]
script: ["echo test BE"]Здесь test-frontend стартует сразу после build-frontend, не дожидаясь build-backend. Две независимые ветки фронта и бэка едут параллельно.
build-frontend ──> test-frontend
build-backend ──> test-backend
(две независимые цепочки, идут параллельно)Чтобы прочувствовать выигрыш, прикинем числа. Допустим, build-frontend занимает 2 минуты, build-backend — 6 минут, а тесты каждой части — по 3 минуты. В модели стадий тесты не стартуют, пока не доедет самый медленный билд (6 минут), и общий путь = 6 + 3 = 9 минут. С needs цепочка фронта проходит за 2 + 3 = 5 минут, цепочка бэка — за 6 + 3 = 9 минут, но они идут параллельно, так что критический путь остаётся 9. Кажется, выигрыша нет? Он появляется, как только тесты фронта длиннее или билд бэка не блокирует деплой фронта: needs освобождает каждую цепочку ровно от тех ожиданий, которые ей не нужны. На больших пайплайнах с десятками джоб это легко превращает 25 минут в 12.
needs и артефакты
По умолчанию джоба с needs получает артефакты только от тех джоб, что в needs (а не от всех джоб предыдущих стадий). Это и быстрее, и чище. Если артефакты конкретной зависимости не нужны, можно указать artifacts: false у элемента needs.
Почему это «чище»? В классической модели стадий джоба автоматически скачивает артефакты всех джоб всех предыдущих стадий. На большом пайплайне это могут быть сотни мегабайт сборок, которые конкретной джобе не нужны — чистый трафик и время впустую. needs делает зависимости по данным явными: вы сами перечисляете, чьи артефакты вам действительно нужны. Это не только ускоряет, но и документирует пайплайн — по needs видно настоящий граф потоков данных, а не только порядок исполнения.
Сравнение с GitHub Actions
GitHub Actions изначально строится на needs между джобами — там нет понятия глобальных стадий. GitLab даёт оба подхода: простые стадии для линейных случаев и needs для графа. Хорошая практика — начинать со стадий, а needs добавлять там, где это реально ускоряет.
Разница в философии поучительна. GitHub Actions с самого начала сказал: «граф — единственная модель», и любой порядок выражается через jobs.<id>.needs. Это элегантно, но новичку приходится сразу думать графами. GitLab пошёл от простого к сложному: стадии — это «граф для тех, кто пока не хочет графа», а needs доступен, когда понадобится. Практический вывод один и тот же в обеих системах: не усложняйте раньше времени. Если ваш пайплайн линеен (собрал → протестировал → задеплоил) и быстр, стадий достаточно. needs вводите точечно, когда профилирование показало конкретное бутылочное горлышко.
Как работает под капотом
Когда вы добавляете needs, планировщик GitLab перестаёт смотреть только на номер стадии и строит граф зависимостей. Джоба переходит в готовность, как только все её needs завершились успешно. Стадии при этом сохраняются для группировки и отображения, но фактический порядок диктует граф. Цикл в зависимостях запрещён — отсюда «ациклический» в DAG.
Запрет циклов — не каприз, а необходимость: если бы джоба A ждала B, а B ждала A, ни одна из них никогда бы не стартовала. Поэтому GitLab при загрузке .gitlab-ci.yml проверяет граф на отсутствие циклов и отказывается запускать пайплайн с зацикленными зависимостями — ошибку вы увидите сразу, ещё до запуска. Есть и второе ограничение, которое часто удивляет: длина цепочки needs ограничена (исторически — около 50 джоб в одной цепочке зависимостей), так что бесконечно длинные графы построить не выйдет. На практике в это упираются редко, но знать о пределе полезно.
Частые ошибки
- Указать в
needsджобу из той же или более поздней стадии — нужно ссылаться на джобы, которые логически идут раньше. - Ожидать, что
needsподтянет артефакты всех предыдущих джоб — он тянет только от перечисленных. - Перегрузить пайплайн ненужными
needsтам, где простой последовательности стадий было достаточно. - Создать циклическую зависимость и получить отказ пайплайна ещё на этапе валидации.
- Оптимизировать «на глаз» без замеров: добавлять
needsтуда, где узкого места на самом деле нет.
Итоги
- Стадии последовательны: следующая ждёт завершения всей предыдущей — это барьер синхронизации.
needsстроит DAG — джоба стартует, как только готовы её зависимости, что снимает лишние ожидания.- С
needsджоба получает артефакты только от указанных зависимостей, что ускоряет и упрощает пайплайн. - Начинайте со стадий, добавляйте
needsточечно по результатам профилирования; следите за отсутствием циклов.