сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · CI/CD

Пайплайны CI: раннеры, кэш, артефакты

Пайплайн: параллельность, кэш, воспроизводимость

Пайплайн - это набор шагов, которые запускаются на каждое изменение кода: собрать, проверить, выложить. Разбирают его через симптомы: сборка идёт двадцать минут, у соседа зелено, у тебя красно, а перезапуск того же коммита даёт другой результат.

Стержень: параллельность помогает не всегда, кэш ускоряет не то, что кажется, а красное у одного при зелёном у другого почти всегда означает разницу в окружении.

// Формулировки: «как ускорить сборку?», «чем кэш отличается от артефакта?», «тест падает через раз, что делать?»

Параллельность помогает не всем шагам

Разложить шаги параллельно - первое, что предлагают для ускорения. Замерил на этой машине, где два ядра. Три шага, которые считают (нагружают процессор): последовательно 7054 миллисекунды, параллельно 4125, ускорение всего в 1,71 раза. Три шага, которые в основном ЖДУТ ответа по сети: последовательно 3187, параллельно 1090, ускорение в 2,92 раза.

Разница объясняется просто. Считающие шаги делят между собой те же два ядра, поэтому третий шаг всё равно ждёт очереди. Ждущие шаги процессор почти не занимают, поэтому спокойно идут втроём. Отсюда практический вывод: параллелить в первую очередь то, что ждёт, а для считающих шагов сначала посмотреть, сколько ядер у машины, на которой всё это крутится.

// И перед тем как параллелить, стоит посмотреть, что вообще занимает время. Обычно это не тесты, а установка зависимостей и сборка образа - и лечится это кэшем, а не раскладыванием на потоки.

пайплайн
набор шагов, запускаемых на каждое изменение кода
шаг, который считает
занимает процессор; параллелится ровно до числа ядер
шаг, который ждёт
стоит на сети или диске; параллелится почти без потерь

Кэш ускоряет не то, что кажется

Кэш в пайплайне - это папка, которую сохраняют после запуска и подкладывают в следующий. Обычно туда кладут скачанные пакеты. Замерил честно: установка пары тяжёлых библиотек заняла 13060 миллисекунд без кэша и 12334 с прогретым кэшем. Экономия около 6%, при том что сам кэш весит 30 мегабайт и его ещё надо сохранить и восстановить.

Почему так мало: скачивание - меньшая часть работы, основное время уходит на распаковку и установку, а её кэш пакетов не отменяет. Настоящий выигрыш даёт кэш СЛОЁВ образа. Слой - это неизменяемый набор изменений файлов, который добавляет одна строка файла сборки; если вход слоя не менялся, шаг не выполняется вовсе, а значит, пропускается и вся установка. На соседнем замере это выглядело так: правка одной строки кода при неудачном порядке строк в файле сборки давала 6869 миллисекунд, а при правильном - 1594.

// Отсюда же разница между кэшем и артефактом, которую любят спрашивать. Кэш - ускорение: если он потерялся, всё соберётся заново, просто медленнее. Артефакт - результат работы шага, который передаётся дальше по пайплайну и хранится: собранный образ, отчёт, дистрибутив. Потеря артефакта означает, что следующий шаг делать не из чего. Поэтому кэш можно чистить смело, а артефакты - по политике хранения.

кэш пайплайна
папка, переживающая запуски; ускоряет, но не обязательна
артефакт
результат шага, который передают дальше и хранят

«У соседа зелено, у меня красно»

Почти всегда это разница в окружении, а не в коде. Показываю на двух замерах. Первый: заказ, созданный в 22:30 по всемирному времени, в контейнере без настройки часового пояса помечается 11 августа, с поясом Москвы - уже 12 августа 01:30, а с поясом Новосибирска - 12 августа 05:30. Тест, который проверяет «заказ от 11 августа», честно зелёный у одного и красный у другого. Переменная TZ (timezone) задаёт этот пояс, и в сборочной машине он часто не такой, как на ноутбуке.

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

// Отсюда лечение: фиксировать всё, что влияет на результат. Версии зависимостей и базового образа (того, с которого начинается сборка) - точными номерами, часовой пояс и язык - явно, случайные значения - заданным зерном. Проверил и обратную сторону: пересборка того же файла сборки с кэшем даёт ТОТ ЖЕ образ, а пересборка без кэша - уже другой, хотя в файле не поменялось ни строки. Полная побайтовая воспроизводимость - отдельная большая работа, и обещать её на собесе не надо; надо назвать те входы, которые действительно фиксируются.

окружение
всё вокруг кода: версии, переменные, пояс, язык, права
фиксация версий
точные номера зависимостей и базового образа вместо плавающих
зерно случайности
заданное число, от которого зависит «случайный» порядок

Как отвечать: «Сборка идёт двадцать минут. Как ускорить?»

Сначала смотрю, на что уходит время по шагам, а не ускоряю наугад. Обычно основное время съедает не прогон тестов, а установка зависимостей и сборка образа. Дальше по порядку. Первое - кэш слоёв образа и правильный порядок строк в файле сборки: тяжёлое и редко меняющееся выше, код ниже. На моём замере правка одной строки кода при неудачном порядке давала около семи секунд, при правильном - полторы. Второе - параллельность, но с оговоркой: считающие шаги делят те же ядра, у меня три таких шага на двух ядрах дали ускорение всего в 1,7 раза, а вот шаги, которые ждут сети, ускорились почти втрое. Третье - не тащить в пайплайн лишнее: служебные каталоги проекта, тяжёлые артефакты, полные прогоны там, где хватит быстрой проверки на каждый коммит и полной перед вливанием в общую ветку.

Ответ начинается с замера, а не с рецепта, и честно называет предел параллельности. Это и отличает человека, который ускорял пайплайн, от человека, который читал про ускорение.

На чём валятся

  • Параллелят считающие шаги на машине с двумя ядрами и удивляются, что стало не втрое быстрее.
  • Складывают в кэш то, что всё равно надо распаковывать, и получают экономию в проценты.
  • Путают кэш и артефакт: чистят по одной политике и теряют то, что нужно следующему шагу.
  • Ищут в коде причину «у соседа зелено», не сравнив окружение и версии.
  • Оставляют плавающие версии зависимостей и базового образа, а потом ловят сборку, которая вчера работала.

Проверьте себя

Пять вопросов из банка по этой подтеме. Всего их 6, остальные разбираются в тренажёре.

  1. #dvo_ci_pipeline1 / 5
    В пайплайне есть и кэш, и артефакты. В чём разница?
    A)Кэш живёт в реестре образов, артефакты — на раннере
    B)Кэш доступен всем проектам, артефакты — только текущей ветке
    C)Кэш хранится вечно, артефакты чистятся после каждой сборки
    D)Кэш ускоряет повторные сборки, артефакты передают результат дальше
    показать ответ и разбор
    +D)Кэш ускоряет повторные сборки, артефакты передают результат дальше

    // разбор: Кэш — расходный ускоритель: зависимости, скачанные пакеты, промежуточные объекты. Его потеря делает сборку медленной, но не ломает. Артефакт — результат работы стадии: собранный бинарь, отчёт тестов, образ; он передаётся следующим стадиям и хранится по политике. Отсюда правило: если без этого следующий шаг не соберётся, это артефакт, а не кэш.

  2. #dvo_ci_pipeline2 / 5
    Раннер настроен исполнителем shell на общей машине. Чем это грозит?
    A)Сборки тащат состояние друг друга: мусор, версии, права, гонки
    B)Такой раннер не умеет забирать артефакты между стадиями
    C)Он поддерживает лишь один язык сборки одновременно
    D)Логи сборки не попадут в интерфейс CI, только в системный журнал
    показать ответ и разбор
    +A)Сборки тащат состояние друг друга: мусор, версии, права, гонки

    // разбор: Shell-исполнитель запускает команды прямо на хосте: глобально установленные пакеты, кэши в домашнем каталоге, файлы от прошлых сборок и параллельные джобы, дерущиеся за одни и те же пути. Отсюда «зелено на одной машине, красно на другой» и утечки секретов между проектами. Изоляцию даёт исполнитель на контейнерах или в кластере: чистое окружение на каждую сборку.

  3. #dvo_ci_pipeline3 / 5
    Сборка проходит локально, но падает в CI на другой версии зависимости. Как сделать её воспроизводимой?
    A)Ставить зависимости в CI из кэша прошлой удачной сборки
    B)Зафиксировать lock-файл и базовый образ по дайджесту
    C)Разрешить сборке доступ в интернет только через прокси
    D)Собирать под одним пользователем с теми же правами
    показать ответ и разбор
    +B)Зафиксировать lock-файл и базовый образ по дайджесту

    // разбор: Воспроизводимость — это когда вход зафиксирован полностью: lock-файл с точными версиями и хэшами пакетов, базовый образ по дайджесту, а не по плавающему тегу, пины тулчейна. Кэш и сеть влияют на скорость, а не на состав. Проверка простая: две сборки одного коммита с разницей в неделю должны дать одинаковый результат.

  4. #dvo_ci_pipeline4 / 5
    Как гарантировать, что тесты и линт прошли ДО того, как ветку вольют в main?
    A)Договориться, что все запускают тесты локально перед пушем
    B)Гонять тесты уже после merge, ночным прогоном по main
    C)Обязательные проверки в branch protection
    D)Повесить пре-коммит хук — он не пустит плохой код
    показать ответ и разбор
    +C)Обязательные проверки в branch protection

    // разбор: Гарантию даёт защита ветки (branch protection / merge request checks): PR нельзя влить, пока обязательные проверки (тесты, линт, сборка) не стали зелёными, плюс требование ревью и актуальности с main. Локальные договорённости и pre-commit хуки обходятся и не заменяют серверный гейт. Пост-merge прогон ловит поломку уже после того, как она попала в main.

  5. #dvo_ci_pipeline5 / 5
    В публичном логе CI засветился секрет: его вывела в stdout одна из команд сборки. Что теперь и как впредь?
    A)Просто удалить строку из лога, и проблема решена
    B)Ничего: маскирование CI и так звёздочками его скрыло
    C)Сделать репозиторий приватным, лог станет недоступен
    D)Секрет скомпрометирован — ротировать; в CI включить маскирование
    показать ответ и разбор
    +D)Секрет скомпрометирован — ротировать; в CI включить маскирование

    // разбор: Секрет, попавший в лог (даже приватный), считается скомпрометированным — его ротируют немедленно. Логи скачивают, кэшируют, пересылают; удаление строки не откатывает утечку. Впредь: хранить секреты в защищённых переменных CI (они маскируются в выводе), не эхоить их, отключать set -x вокруг чувствительных мест, а маскирование считать подстраховкой, а не гарантией.

дальше

Теорию прочитали. Навык ставится повторением

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