TOPZNANIE

Цифровая практика без сложностей

Выберите город
MET Norway · CC BY 4.0
← На главную

· Общий обзор

Почему ваш CI/CD медленный: профилирование пайплайнов и устранение узких мест

Тесты могут выполняться быстро, а проверка коммита всё равно занимать полчаса. Runner ждёт свободного слота, получает исходники, поднимает окружение, распаковывает зависимости. После тестов остаются отчёты и артефакты. Ускорение самой тестовой команды затронет только часть этого времени.

Для разбора нужен профиль всего запуска. Ниже используется GitLab CI с собственными runner и приложение на Node.js. Примеры конфигурации рассчитаны на существующий проект; минуты в схеме и расчёте кэша условные. Реальный выигрыш определяется по его логам.

Почему ваш CI/CD медленный: профилирование пайплайнов и устранение узких мест

Сначала собрать временную шкалу

У разработчика и CI разные точки отсчёта. Человек начинает ждать после отправки коммита, исполнитель позже. Конечная точка тоже зависит от задачи: первая ошибка в merge request или законченный релиз. Ручное одобрение выпуска выделяется отдельным интервалом, иначе ожидание согласования легко принять за медленную инфраструктуру.

В Jobs API GitLab есть queued_duration, duration, started_at и finished_at. К выгрузке стоит добавить SHA, тип запуска, runner, имя задания и статус каждой попытки. Ответ приходит постранично, а повторные задания включаются через include_retried=true. Без них успешный повтор будет выглядеть лучше, чем всё ожидание с первой неудачной попыткой.

Для начала достаточно истории сопоставимых запусков. Полный ночной прогон и проверка маленького merge request решают разные задачи. Даже на одной машине их нельзя сравнить как результат одной настройки. Внутри выбранной группы отмечаются холодный кэш, версии образов и повторные попытки.

Далее в trace ищутся границы операций. GitLab Runner получает исходники, восстанавливает кэш и скачивает входящие артефакты до пользовательских команд. После них идут дополнительные шаги, включая after_script и отправку результатов. Поэтому зелёная строка тестового фреймворка вполне может появиться задолго до окончания задания.

Если время пропало до старта, проверяются доступные runner и теги. Свободная машина с неподходящим тегом не разгрузит очередь этого задания. Если задержка внутри загрузки зависимостей, нужны сеть и registry. Если после тестов, следующий объект разбора уже архив или отчёт. Эти различия должны появиться в профиле до выбора исправления.

Измерить команду отдельно от задания

В Linux‑образе с GNU time установку можно обернуть следующим образом. Строки добавляются в существующий script:

/usr/bin/time -f ‘elapsed=%e user=%U sys=%S maxrss_kb=%M’
  -o npm-ci.time npm ci —cache .npm —prefer-offline
cat npm-ci.time

Показатели определены в описании GNU time: elapsed означает время по часам, user и sys время CPU,%M максимальный RSS в килобайтах. Встроенный time оболочки может не поддерживать этот формат. Максимальный RSS также не является суммарной памятью всех параллельных контейнеров.

При ошибке установки оболочка может остановиться до cat. Файл измерения поэтому забирается через artifacts с when: always. Для сборки и тестов нужны отдельные файлы. Полная трассировка set ‑x для замера не требуется и может показать секреты из аргументов команд.

Большой elapsed при малом времени CPU указывает на время вне вычислений, но ещё не называет его причину. Команда могла ждать сеть, диск, другой процесс или доступ к CPU при ограничении контейнера. Нужны метрики хоста за тот же интервал и ограничения контейнера. У однопоточной программы иной предел: дополнительные ядра не ускорят участок, который она выполняет последовательно.

Найти цепочку, которая задерживает результат

Длительности четырёх заданий сами по себе не определяют время пайплайна. Пусть build занимает 6 минут, unit‑тесты 4, интеграционные 8, lint 1. По очереди получится 19 минут. Если обеим группам тестов нужен только build, а lint независим, при достаточных ресурсах получается 6 + max(4, 8) = 14 минут. Подготовка, очереди, передачи файлов и выпуск здесь не учтены.

Почему ваш CI/CD медленный: профилирование пайплайнов и устранение узких мест

Даже сокращение unit‑тестов с четырёх минут до двух не изменит эти четырнадцать: позже заканчиваются интеграционные. Минута экономии на build, напротив, сдвигает обе группы. Приоритет определяется критическим путём, то есть цепочкой зависимостей, от которой зависит окончание нужной работы.

В GitLab такие связи задаёт needs. Для существующего build и скрипта интеграционных тестов:

integration:
  stage: test
  needs:
    — job: build
      artifacts: true
  script:
    — ./ci/integration.sh

Интеграционная джоба сможет стартовать после build, не дожидаясь остальных заданий предыдущей стадии. Нужные файлы сам build должен публиковать в artifacts: paths. Перечень needs определяет и источники входящих артефактов, поэтому после изменения графа проверяется происхождение пакета.

Для ветки, merge request и тега графы могут отличаться из‑за rules. Если обязательный производитель исключён из графа, необязательная зависимость не создаст отсутствующий пакет. Независимый lint можно запускать раньше ради первого полезного ответа, даже когда срок релиза почти не меняется.

Посчитать полную цену кэша

Пометка cache hit сообщает, что данные нашлись. Она ничего не говорит о затратах на их получение. В условном примере восстановление архива занимает 25 секунд, его упаковка и отправка ещё 20, а установка экономит 30. Джоба в итоге потеряла 15 секунд. Если сравнить только время установщика, результат получится противоположным.

У npm встречается ещё более прямой расход: runner распаковывает node_modules, после чего npm ci удаляет существующий каталог перед установкой. Для такой последовательности стоит проверить сохранение скачанных пакетов.npm. Следующий фрагмент предполагает подходящий Node.js и закоммиченный lock‑файл:

cache:
  key:
    files: [package-lock.json]
  paths: [.npm/]
script:
  — npm ci —cache .npm —prefer-offline
  — npm test

Флаг ‑prefer‑offline оставляет возможность загрузить недостающее из сети, поэтому registry и авторизация остаются нужны. Рассогласование package.json и lock‑файла вызовет ошибку при любом состоянии кэша. Сохраняются и настройки npm, с которыми создавался lock‑файл.

В сравнении участвуют полные джобы, отдельно с холодным и прогретым кэшем. Если получение архива дороже экономии, меняется его состав или сам механизм. У кэша GitLab нет гарантии доступности: промах должен оставлять рабочую сборку. Обязательный пакет конкретного коммита передаётся артефактом задания, которое его собрало.

В общем кэше также нужны понятные права записи. Недоверенная ветка не должна подменять содержимое для защищённого релиза. При нескольких писателях с одним ключом стоит проверить, чей архив останется последним; заданиям, которые только читают его, подходит policy: pull.

У Docker повторная установка может зависеть от порядка COPY. При раннем COPY.. изменение исходника способно инвалидировать слой зависимостей. Манифест и lock‑файл можно скопировать отдельно, вместе с файлами, которые действительно нужны install‑скриптам..dockerignore уменьшает контекст, сохраняя необходимые сборке данные.

Порядок слоёв не поможет следующему одноразовому builder найти локальный кэш, удалённый вместе с предыдущим. Для него проверяются импорт и экспорт внешнего BuildKit‑кэша, включая стоимость передачи. Эти механизмы описаны в руководстве Docker. Секреты передаются механизмом секретов сборки, без копирования в слой. Git‑метаданные исключаются из контекста только если сборка обходится без них.

Не перегрузить исполнителя параллельными заданиями

После изменения графа очередь может сократиться, а тесты стать медленнее. Несколько контейнеров с базами данных распаковывают файлы на один физический накопитель. CPU, память и сеть тоже общие. Разница между отдельным быстрым запуском и серией под обычной нагрузкой покажет, насколько выгодна новая параллельность.

В конфигурации GitLab Runner concurrent ограничивает общую параллельность экземпляра, limit отдельный runner, request_concurrency одновременные запросы заданий. Это разные ограничения. Изменение числа запросов не добавляет ресурсов машине.

Тестовые шарды делятся по истории длительностей, а не по одинаковому количеству файлов. Самая тяжёлая группа определит окончание этапа. Каждому запуску нужны собственные порты, каталоги и данные, иначе конфликт добавит повторную попытку. При сравнении учитываются и соседние проекты, которые используют тот же хост.

Выделенный сервер обоснован, когда замеры показывают конкуренцию за CPU, RAM или диск. По ним же определяются требования к машине. Если задание ждёт подходящего тега либо ответа внешнего registry, быстрый локальный NVMe не устраняет эту причину задержки.

Принять изменение по серии запусков

Для одной гипотезы меняется один механизм. Одновременная замена машины, кэша и шардов покажет общую разницу, но скроет вклад каждой правки. В отчёте остаются медиана, число наблюдений, p95, медленные trace и суммарные runner‑минуты. На небольшой выборке p95 нестабилен. Отказы и повторы flaky‑тестов включаются, поскольку на них тоже ушло время.

У production может быть намеренная очередь. Resource group последовательно допускает задания к общему ресурсу и защищает от конфликтующих выкладок. Для устаревших проверок ветки подходят auto‑cancel и interruptible, если работа безопасно прерывается. Для миграции данных это свойство требует отдельной процедуры.

Перед слиянием проверяется следующий набор условий:

· Обязательные тесты есть в merge request и релизе, их обнаруженное количество не уменьшилось.

· Артефакты относятся к нужному коммиту, пустой кэш не ломает сборку.

· Фильтры путей учитывают изменения общих библиотек и конфигурации.

· Выигрыш сохраняется при обычной нагрузке вместе с очередью и повторными попытками.

Быстрый зелёный статус после исключения обязательного теста не подтверждает ускорение проверки.

В задаче на изменение CI стоит сохранить короткую запись: какой интервал задерживает результат, какой механизм меняется и с чем будет сравниваться новая серия. Тогда сокращение установки на минуту не потеряется внутри общего графика, а более короткая очередь не скроет замедление тестов. Если опыт не подтвердил гипотезу или помешал соседнему проекту, настройка возвращается вместе с сохранёнными измерениями.

Теги:

  • linux
  • линукс
  • cd
  • ci
  • хостинг
  • сервер
  • devops
  • системное администрирование
  • непрерывная интеграция

Хабы:

  • DevOps
  • Git
  • Linux
  • Системное администрирование
  • Хостинг