Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Всем привет, меня зовут Фёдор — я руководитель фронтенд-разработки на проекте Smartbot Pro в компании KTS.
Наш проект — конструктор ботов для социальных сетей, в котором алгоритм бота представлен в виде визуального графа. Конструктор включает большое количество интеграций.
Недавно на проекте остро встал вопрос оптимизации наших ci/cd пайплайнов, потому что релиз определенной версии мог занимать до 18 минут.
Для нас очень важно сократить это время, потому что мы хотим быстрее доставлять пользователям две вещи:
Новый функционал
Исправления багов
В статье я расскажу, как мы решили эту проблему с помощью оптимизации сборки Docker-образа, оптимизации установки зависимостей и сокращения количества шагов пайплайна.
Это может быть полезно тем, кто столкнулся с проблемой долгих пайплайнов.
Содержание:
Структура проекта:
Основной монорепозиторий
Shopback монорепозиторий
Анализ пайплайна
Base-build
Test
Build
Deploy Stages
Первая попытка оптимизации
Оптимизация base-build
Оптимизация build
Итоги первой попытки
Вторая попытка оптимизации
Yarn Zero-Installs
Миграция
Шаг 1
Шаг 2
Шаг 3
Шаг 4
Шаг 5
Шаг 6
Итоги второй попытки
Дальнейшие планы
Структура проекта
Пробежимся чуть подробнее по этой схеме для понимания источников проблемы.
Основной монорепозиторий
Основной репозиторий состоит из 3 пакетов:
b2c — основная версия продукта
b2b — версия для клиента, которая дополняет версию b2c специфичными функциями, которые подключаются к b2c версии с помощью плагинов.
sa — приложение для администрирования. Которая используется утилиты/типы/модели из b2c версии.
Shopback монорепозиторий
Мы с командой часто тестируем разные продуктовые гипотезы. Shopback стал одной из таких гипотез — это конструктор магазинов внутри ботов в Telegram, который мы реализовали на базе Web App.
После успешного тестирования гипотезы было принято решение интегрировать этот конструктор в основной проект. Для интеграции потребовалось использовать UI-компоненты из shopback-репозитория для реализации превью магазина. Мы вынесли их в отдельную библиотеку @shopback/ui, которая используется внутри пакета app и внутри b2c пакета из основного репозитория.
Важно отметить, что задача заключалась в оптимизации пайплайнов именно основного репозитория. Пакет UI часто обновляется, потому что shopback сейчас активно разрабатывается. Из-за этого нам приходится часто изменять версию этого пакета в b2c. Поэтому нам важно, чтобы пайплайны длились как можно меньше именно при изменяющихся зависимостях.
Анализ пайплайна
Для начала необходимо было проанализировать наш пайплайн и понять, какие именно джобы (job) тормозят.
Base-build
В этой джобе собирается и пушится в registry Docker-образ со всеми установленными зависимостями монорепозитория. Мы приняли такое решение, чтобы не повторять установку зависимостей внутри каждой сборки наших пакетов (build stage).
Длительность base-build зависела от того, были ли изменены зависимости.
Если зависимости не изменились: ~30 секунд, благодаря Docker cache
Если изменились: ~10 минут. Этот вариант стал большой проблемой
Test
Запускаются тесты внутри Docker-образа, собранного на предыдущем шаге. Длительность тестов составляла в среднем 30 секунд и не сильно влияла на общую длительность.
Build
В зависимости от пакета длительность составляла 2-7 минут.
Для оптимизации мы будем рассматривать одну джобу: docker-b2c. Все они примерно одинаковые, проблема везде одна.
Длительность docker-b2c составляла 5-7 минут.
Deploy stages
Все джобы внутри deploy-* стейджей деплоят собранные на предыдущем шаге образы в Kubernetes-кластер. Длительность каждой из этих ~10 секунд.
Как и в случае с тестами, 10 секунд из 16-18 минут — это совсем небольшая часть. К тому же это стабильное время. Поэтому test и deploy стейджы в оптимизации не нуждаются.
Первая попытка оптимизации
После анализа стало очевидно:
Основная длительность сосредоточилась в base-build и build, эти джобы и нужно оптимизировать:
Длительность пайплайна зависит от того, есть ли измененные зависимости:
Если зависимости не изменились, длительность составляет 3-5 минут
Если изменились, длительность составляет 16-18 минут
Оптимизация base-build
В качестве пакетного менеджера мы использовали yarn v1.
Напомню, что длительность 10 минут соответствовала варианту с изменениями в зависимостях.
Сначала мы попробовали добавить кэширование. Про Docker cache и лучшие практики применения очень хорошо написано в документации.
После прочтения и анализа Docker-файла, стало понятно, что можно применить кэш для команды RUN:
RUN --mount=type=cache,target=.yarn/cache YARN_CACHE=.yarn/cache yarn install
Для использования данного кэша необходимо использовать BuildKit в качестве docker backend.
При таком вызове при следующей сборке будет использоваться yarn cache из директории .yarn/cache из предыдущего образа.
Оптимизация build
В качестве сборщика нашего проекта мы используем webpack v5.
Webpack по дефолту использует memory cache при development mode, но для production mode кэширование отключено — подробнее об этом можно прочесть в документации. Можно изменить это поведение, указав в настройках конфига cache:
{
...
cache: isProd ? {
type: 'filesystem',
buildDependencies: {
config: [__filename]
}
} : { type: 'memory' }
}
Теперь при production mode для кэширования будет использоваться файловая система.
По аналогии с добавлением кэширования при установке зависимостей добавим кэширование при сборке проекта:
RUN --mount=type=cache,target=.yarn/.cache/webpack yarn run build
Итоги первой попытки
После подключения кэширования в джобы были получены следующие цифры:
Если зависимости не изменились, длительность составляет 2,5-4,5 минуты.
Было 4-6 минутЕсли изменились, длительность составляет ~13-15 минут.
Было 16-18 минут
Стало лучше. Но как я уже говорил, зависимости изменяются часто, и для нас 13-15 минут — плохой результат.
Поэтому мы продолжили исследование.
Вторая попытка оптимизации
При повторном взгляде на base-build стало понятно, что на самом деле установка зависимостей занимает не так много времени относительно общей длительности этой джобы:
Длительность до первой оптимизации: ~140 секунд
Длительность после первой оптимизации: ~100 секунд
Напомню, что общая длительность составляет ~10 минут.
Остальное время уходило на push образа в registry. Тут же вскрылась другая проблема: большой образ с зависимостями занимает 1 Gb и негативно влияет на размеры registry, скорость пуша и пула.
Очевидный вопрос: «Можно ли вообще избавиться от этого образа?».
Первое, что пришло на ум — устанавливать зависимости не в отдельной джобе и хранить их в отдельном Docker-образе, а устанавливать в каждой docker-* джобе. Тогда при использовании multi-stage сборки размер будет включать только собранные файлы без зависимостей, но этот шаг будет повторяться в каждой из docker-* джоб. Это не лучший вариант, так как длительность установки зависимостей с ростом их числа тоже будет расти. К тому же установленные зависимости необходимы для прохождения тестов в джобе test.
Второй вариант — хранить зависимости внутри репозитория. Я предчувствую, что на этом месте половина читателей уже скроллит вниз, чтобы оставить плохой комментарий, но пожалуйста, подождите, я могу всё объяснить!