Матричные джобы: parallel:matrix
Учимся размножать джобу по комбинациям переменных одной конструкцией.
parallel:matrix — механизм, порождающий из одной джобы несколько параллельных по заданным наборам значений переменных.
Зачем матрицы
Часто нужно прогнать одно и то же на разных конфигурациях: тесты на Python 3.10, 3.11, 3.12; сборка под Linux и Windows; проверка на двух базах данных. Писать четыре почти одинаковые джобы — копипаста. Матрица описывает оси, а GitLab сам разворачивает все комбинации.
За кажущейся простотой стоит важная идея: матрица отделяет что делать от на чём делать. Тело джобы — установка зависимостей, прогон тестов — пишется один раз и не знает, на какой версии Python или какой базе оно выполняется. Конкретику задают оси. Благодаря этому добавить ещё одну версию интерпретатора — это правка одной строки списка, а не копирование целой джобы со всеми её script, cache и artifacts. Меньше дублирования — меньше расхождений: невозможно случайно поправить шаг тестов для одной версии, но забыть про другую, потому что шаг ровно один.
Матрицы напрямую отвечают на вопрос «а на чём мы вообще обещаем, что работаем?». Если библиотека заявляет поддержку трёх версий рантайма и двух операционных систем, то честный CI должен проверять все шесть сочетаний на каждый коммит — иначе обещание не подкреплено. Матрица делает таблицу совместимости исполняемой: она не висит в README, а реально прогоняется и краснеет, когда совместимость ломается.
test:
image: python:${PY_VERSION}
parallel:
matrix:
- PY_VERSION: ["3.10", "3.11", "3.12"]
script:
- python --version
- pytestЭта запись породит три джобы: test: [3.10], test: [3.11], test: [3.12], каждая со своим значением PY_VERSION, подставленным в том числе в image. Они идут параллельно, заметно ускоряя проверку совместимости.
Обратите внимание на подстановку ${PY_VERSION} прямо в image. Значение оси — это обычная CI-переменная, доступная везде, где переменные вообще допустимы: в имени образа, в путях артефактов, в аргументах скрипта, в тегах раннера. Поэтому матрица может управлять не только тем, что прогоняется, но и тем, где и как: например, ось со значениями тегов раннера отправит сборку под Windows на windows-раннер, а под Linux — на linux-раннер, оставаясь одной джобой в YAML.
Несколько осей
Оси можно комбинировать — GitLab построит декартово произведение:
test:
parallel:
matrix:
- PY_VERSION: ["3.11", "3.12"]
DATABASE: ["postgres", "mysql"]
script:
- echo "Python $PY_VERSION на $DATABASE" postgres mysql
3.11 [3.11,postgres] [3.11,mysql]
3.12 [3.12,postgres] [3.12,mysql]
=> 2 x 2 = 4 параллельных джобыПолучится четыре джобы — все комбинации версии и базы. Будьте аккуратны: число джоб растёт как произведение, и матрица 4x4x3 — это уже 48 параллельных джоб.
Декартово произведение растёт быстро, и здесь кроется главный риск матриц. Сорок восемь джоб на каждый коммит — это сорок восемь раз скачать образ, поднять окружение, прогреть кэш и потратить минуты раннера; на бесплатных тарифах квота уходит за считанные пуши, а на самохостинге упирается в число одновременных раннеров. Прежде чем расширять оси, стоит спросить: действительно ли нужна каждая комбинация? Часто крайние сочетания (самая старая ОС плюс самая новая версия языка) важны, а внутренние избыточны.
GitLab даёт два инструмента укрощения матрицы. Первый — exclude, который вычитает из произведения заведомо ненужные или несовместимые комбинации (скажем, старая база на новой ОС). Второй — стратегия «полная матрица только по расписанию»: на обычный push гоняют сокращённый набор осей через переменные в rules, а полную матрицу совместимости запускают ночным scheduled-пайплайном, когда раннеры свободны. Так быстрый фидбэк на каждый коммит сочетается с честной широкой проверкой без постоянной платы за неё.
Простой parallel без матрицы
Есть и более простой parallel: N — он запускает N копий одной джобы (с переменными CI_NODE_INDEX/CI_NODE_TOTAL), что используют для шардирования тестов: разбить набор тестов на N частей и гонять параллельно.
Разница между двумя режимами тонкая, но существенная. parallel: N создаёт N одинаковых копий джобы, отличающихся лишь номером CI_NODE_INDEX; смысл им придаёт сам скрипт, который по этому номеру выбирает свою долю тестов (этим занимаются сплиттеры тестов или встроенное деление по тайм-боксам). Цель — ускорить один большой тестовый прогон, разрезав его на куски. А parallel:matrix создаёт разные джобы с разными наборами переменных; цель — проверить разные конфигурации. Первое отвечает на «как быстрее прогнать один и тот же набор», второе — на «как прогнать на множестве окружений».
Сравнение с GitHub Actions
В GitHub Actions это strategy.matrix — концепция и поведение очень близки: оси переменных, декартово произведение, параллельный запуск. Если вы знаете матрицы Actions, parallel:matrix освоится мгновенно; различается лишь синтаксис и имена контекстных переменных.
Совпадает не только идея, но и большинство возможностей; различия — в деталях управления. Ниже сопоставление ключевых механизмов:
| Возможность | GitLab CI | GitHub Actions |
|---|---|---|
| Объявление осей | parallel: matrix: | strategy: matrix: |
| Декартово произведение | да, автоматически | да, автоматически |
| Исключить комбинацию | exclude | exclude |
| Не падать всей матрицей | не падает по умолчанию | fail-fast: false |
| Шардирование одной задачи | parallel: N + CI_NODE_INDEX | матрица по индексам вручную |
Одно поведенческое отличие стоит запомнить: в Actions по умолчанию включён fail-fast — падение одной ячейки матрицы отменяет остальные. В GitLab такого «домино» нет: каждая развёрнутая джоба живёт своей жизнью, и падение версии 3.10 не остановит 3.12. Это удобно, когда нужен полный отчёт о совместимости за один прогон, но означает, что бесполезные джобы не отменятся сами — экономию приходится закладывать осознанно через exclude и rules.
Как работает под капотом
На этапе создания пайплайна GitLab разворачивает матрицу в отдельные джобы, подставляя каждой свой набор переменных. Каждая такая джоба — полноценная: со своим окружением, артефактами, статусом. Они попадают в очередь одновременно и выполняются на доступных раннерах параллельно, насколько хватает мощности.
Частые ошибки
- Создать гигантскую матрицу и исчерпать минуты раннеров или их параллелизм.
- Забыть, что переменная матрицы должна быть строкой (числа версий берут в кавычки), иначе YAML может интерпретировать
3.10как число3.1. - Путать
parallel: N(копии для шардирования) иparallel:matrix(комбинации переменных).
Итоги
parallel:matrixразворачивает одну джобу в комбинации значений переменных, выполняемые параллельно.- Несколько осей дают декартово произведение — следите за числом джоб.
parallel: N— простое размножение для шардирования тестов.