Стадии, порядок и 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 точечно по результатам профилирования; следите за отсутствием циклов.
Проверьте себя
1. Что даёт ключ needs по сравнению с обычными стадиями?
AШифрует переменные
BПозволяет джобе стартовать сразу после её зависимостей, не дожидаясь всей предыдущей стадии
CОтключает раннеры
DУдаляет артефакты
2. Какие артефакты по умолчанию получает джоба с needs?
AВсех джоб всех предыдущих стадий
BТолько перечисленных в needs джоб
CНикаких
DТолько своих собственных