Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Привет, меня зовут Владимир. Я работаю в компании GitLab Архитектором Решений и время от времени я отвечаю на вопросы, которые, как мне кажется, могли бы быть интересны широкому сообществу. Сегодня я поделюсь рекомендациями о некоторых способах ускорения выполнения CI/CD задач в конвейерах GitLab.
Согласитесь, всем нам хочется, чтобы сборка, тестирование, сканирование и развертывание приложений проходило как можно быстрее. Неважно как крепко мы верим в асинхронность рабочего процесса, скорость выполнения автоматизированных задач остается одним из ключевых показателей эффективности процесса разработки. Необходимость быстро выкатить приложение в заданный момент для многих является критическим требованиям. Кроме того, быстрые пайплайны — это ещё и счастливые разработчики.
Исходная ситуация: один из моих клиентов во время миграции в облачную версию GitLab обратился за помощью. Входные данные: GitLab CI пайплайн, который при помощи pulumi проверяет, верифицируют и создает инфраструктурный стек в облаке AWS (как мы увидим далее содержание CI задач для предлагаемых здесь оптимизаций не имеют большого значения). В данном случае задачи выполняются на публичных (shared) раннерах GitLab.com, но в сегодняшней статье мы также проработаем более общие случаи использования своих собственных раннеров. Желаемый результат: минимизировать общую продолжительность выполнения всех задач.
Приступим!
Предустановка зависимостей в образ Docker
Избегайте скачивания и установки требуемых инструментов и зависимостей внутри CI задач. Пользуйтесь заранее подготовленным образом, заточенным для конкретной задачи и содержащим в себе минимальный набор требуемых зависимостей и библиотек. У многих разработчиков велик соблазн использовать стандартный slim образ, в который по мере надобности устанавливаются нужные инструменты во время выполнения пайплайнов. Чаще всего это ведет к тому, что одни и те же компоненты скачиваются и устанавливаются несколько раз. Сам по себе процесс выкачивания и установки зависимостей занимает больше времени, чем скачивание подгрузка подготовленного образа.
Если вы не можете, найти уже готовый образ (в частности, в моем примере с pulumi, можно было использовать официальный Docker образ), создайте свой! В этом нет ничего сложного. Тем более собрать его можно тут же, средствами GitLab CI и разместить в GitLab Container Registry. У этого подхода есть еще и дополнительные преимущества: вы сможете заранее проверять используемые разработчиками образы, так как в GitLab включены также средства сканирований контейнеров. Больше контроля, больше безопасности, меньше рисков. Не забывайте, правда, о рекомендациях из следующего параграфа.
Если вы запомните одно правило из этой статьи, то запомните, пожалуйста, это!
Оптимизация образов Docker
Другая часто встречаемая крайность — это создание так называемых мега образов. В один и тот же образ вшиваются все возможные инструменты, которые могут потребоваться для выполнения задач (как на практике, так и в теории). Это в свою очередь приводит к разрастанию образа до гигантских размеров.
Такое решение, несомненно, упрощает процесс написания пайплайнов (автору не требуется выбирать и подготавливать образ), но неминуемо ведет к снижению эффективности их выполнения. По возможности избегайте этого! Чем меньше ваш образ, тем быстрее будет инициализирована конкретная CI задача. Попробуйте создать индивидуальные образы минимального размера, заточенные для выполнения конкретных задач. Приведу ниже несколько методов оптимизации размера вашего Docker образа, но также порекомендую внимательно изучить лучшие практики написания Dockerfile.
Используйте базовые slim образы (например, debian-slim)
Избегайте установки пользовательских инструментов (vim, curl, и т.д.)
Отключайте установку man-страниц и прочей документации
Минимизируйте количество слоев RUN (комбинируя команды в один слой)
Используйте multi staged build
Если используете apt, то отключайте установку ненужных зависимостей при помощи ключа
--no-install-recommends
Не забывайте почистить кэш (например,
rm -rf /var/lib/apt/lists/*
для Debian)Такие инструменты как dive или DockerSlim могут помочь с дальнейшей оптимизацией
Используйте Docker кэш при сборке образов
Кстати, о сборке образов. Если в одной из задач вашего пайплайна вы собираете новый образ (например, для реализации предыдущей рекомендации), то не забывайте о возможности использования кэширования для ускорения этого процесса.
Дело в том, что при выполнении команды docker build
, сборка слоев производятся с нуля. Использование ключа --cache-from
с указание образов, которые послужат источником кэша, может значительно ускорить сборку. Не забывайте, что вы можете передать процессу несколько аргументов --cache-from
задействовав таким образом несколько образов.
#.gitlab-ci.yml
build:
stage: build
script:
- docker pull $CI_REGISTRY_IMAGE:latest || true
- docker build --cache-from $CI_REGISTRY_IMAGE:latest --tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA --tag $CI_REGISTRY_IMAGE:latest .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
- docker push $CI_REGISTRY_IMAGE:latest
Локальное кэширование Docker образов
Дело в том, что GitLab помимо прочего содержит в себе Container Registry Dependency Proxy, который умеет проксировать и кэшировать образы из Docker Hub. Таким образом GitLab может использоваться как pull-through сервис для минимизации задержки сети при работе с Docker Hub. В зависимости от вашей сети и того, где располагаются GitLab раннеры, подобное кэширование может значительно ускорить процесс запуска CI задач. Кроме того, использование Dependency Proxy позволит обойти ограничения количества запросов на Docker Hub (я уже писал об этом ранее).
Для использование этой возможности вам необходимо будет:
Включить функционал на уровне группы (Settings > Packages & Registries > Dependency Proxy > Enable Proxy)
Добавьте префикс
${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}
к имени образа в.gitlab-ci.yml
определении
# .gitlab-ci.yml
image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/alpine:latest
Если вы уже пользуетесь другим регистром контейнеров, то рекомендую проверить его на наличие подобного функционала.
Оптимизация политики скачивания Docker образов
Данная рекомендация, к сожалению, недоступна пользователям публичных (shared) раннеров на GitLab.com. Однако если вы настраиваете свои GitLab раннеры, она может принести значительные улучшения скорости запуска CI/CD задач.
При настройке собственных раннеров вы можете указать политику скачивания при помощи параметра pull_policy
(делается это в конфигурационном файле config.toml). Этот параметр определяет то, как раннер будет подгружать требуемые образы из регистра.
Возможные значения:
always (дефолтное значение): образы каждый раз подгружаются из удаленного регистра
never: образы вообще не выгружаются из удаленного регистра, а должны быть вручную закэшированы на Docker хосте
if-not-present: раннер сначала проверит локальный кэш и лишь при отсутствии искомого образа скачает его из внешнего регистра
Как легко догадаться использование значения if-not-present может сократить задержки на скачивание и анализ слоев за счет использования локального кэша, а, значит, ускорить запуск и выполнение задач. Однако будьте осторожны при использовании этой конфигурации с часто меняющимися образами. Быстрорастущий кэш и необходимость регулярно его чистить могут свести все выигрыши по времени на нет.
# config.toml
[runners.docker]
pull_policy = "if-not-present"
Кэширование CI/CD
Кэш в GitLab CI - мощный и гибкий инструмент для оптимизации работы пайплайнов. Наверно, одним из самых часто встречаемых и популярных примеров его использования является кэширования зависимостей (.npm/
, node_modules
, .cache/pip
, .go/pkg/mod/
, и т.д.).
Давайте предположим, что мы используем pip для подгрузки требуемых библиотек Python. Без использования механизма кэширования подгрузка библиотек будет выполняться с нуля для каждого нового пайплайна, для каждой индивидуальной задачи. Кэширование позволяет решить эту проблему:
# .gitlab-ci.yml
flake8-install:
before_script:
- pip install virtualenv
- virtualenv venv
- source venv/bin/activate
cache:
paths:
- .cache/pip
- venv/
script:
- python setup.py test
- pip install flake8
Важной особенностью является то, что CI/CD кэш может быть как локальным (файлы остаются на хосте, на котором запушен раннер), так и распределенным (кэш в виде архива сохраняется в S3 хранилище). Это позволяет оптимизировать работу пайплайнов даже в том случае, если у вас нет выделенных раннеров или если они создаются динамически, а значит является эффективным решением как для собственных, так и для публичных (shared) раннеров на GitLab.com.
Политика кэширования CI/CD
Большинство пользователей знают и успешно пользуются механизмом кэширования, описанным выше. Однако многие забывают о дополнительных возможностях оптимизации за счёт использования правильной политики. Дело в том, что при стандартной конфигурации кэш скачивается в начале выполнения CI задачи и загружается обратно в конце. При большом размере кэша и медленных сетях это может стать проблемой. Конфигурировать этот процесс можно за счет параметра cache:policy
.
push-pull (стандартное поведение): кэш скачивается в начале и загружается обратно в конце выполнения задачи
pull: кэш скачивается в начале выполнения задачи, но не загружается в конце
push: кэш не скачивается, но загружается в конце выполнения задачи.
Таким образом мы можем оптимизировать продолжительность выполнения задачи за счет использования политики pull для некоторых задач
# .gitlab-ci.yml
flake8-test:
cache:
paths:
- .cache/pip
- venv/
policy: pull
script:
- flake8 .
Изменение уровня компрессии
Все артефакты и кэш, требуемые для выполнения задач, передаются в сжатом виде. Это означает, что архивы должны разжиматься в начале выполнения задачи и сжиматься в её конце. GitLab Позволяет выбирать желаемый уровень компрессии для этого процесса (fastest, fast, default, slow, slowest)
При выборе желаемого уровня компрессии необходимо руководствоваться. вашей индивидуальной конфигурацией: скорость сети, доступные раннеру CPU ресурсы, размер и содержание архивов. Здесь, скорее всего, придется поэкспериментировать и выбрать наиболее подходящий для вас баланс. Конфигурация производится за счёт использования переменных окружения и может выполняться как для всего пайплайна, так и для индивидуальных задач. Обратите внимание, что необходимо также включить feature flag FF_USE_FASTZIP
.
# .gitlab-ci.yml
variables:
# Enable feature flag
FF_USE_FASTZIP: "true"
# These can be specified per job or per pipeline
ARTIFACT_COMPRESSION_LEVEL: "fast"
CACHE_COMPRESSION_LEVEL: "fast"
Стратегия git клонирования
Ещё один шаг, который выполняется в начале выполнения любой задачи — это клонирование git репозитория. К сожалению, это часто делается даже для тех задач, где клонирование репозитории не требуется вовсе. Например, я нередко наблюдаю такую конфигурацию для ручных задач, которые используются для согласования перехода пайплайна на следующую стадию (например, развертывание в PROD). Решить это можно за счет конфигурирования стратегии git клонирования при помощи переменной окружения (GIT_STRATEGY).
# .gitlab-ci.yml
approve:
variables:
GIT_STRATEGY: clone
stage: approve
script:
- echo Approved !!
allow_failure: false
when: manual
Доступные значение GIT_STRATEGY:
none: репозиторий не клонируется вообще
fetch: с использованием локальной рабочей копии (обычно быстрее, особенно на выделенных раннера)
clone: без использования локальной рабочей копии
Иные ключевые возможности GitLab CI
Не стоит также забывать и об остальных ключевых возможностях GitLab CI, которые позволяют контролировать как, когда и при каких условиях должны выполняться задачи. Я пройдусь по ним довольно кратко (так как написание эффективных пайплайнов само по себе является слишком обширной темой для этой статьи):
rules: рекомендую углубленно ознакомиться с их возможностями, так как они не только помогают запускать задачи только тогда, когда они нужны, но и модифицировать их поведение
needs: так называемые направленные ацикличные графы позволяют построить пайплайны таким образом, что задачи не будут ожидать завершения предыдущей стадии, а запустятся в тот момент, когда все зависимости будут удовлетворены (оптимизируя таким образом общее время выполнения)
interruptible: задачи, помеченные как прерываемые, могут быть автоматически отменены при запуске нового пайплайна на той же ветке кода. Позволяет свести на нет выполнение ненужных задач
Используйте свои собственные раннеры
Возвращаясь к теме публичных раннеров на GitLab.com, отмечу, что несмотря на то, что они подходят для большинства задач, иногда их стандартной конфигурации (3.75 GB RAM, 1vCPU, 25GB Storage) является недостаточно. Публичные раннеры являются простым, эффективным и дешевым (а часто и бесплатным) решением. Однако для решения более сложных задач, требующих глубокой оптимизации, имеет смысл подключить собственные ресурсы (например, с большим количеством памяти или доступом к выделенной сети). Напомню, что собственные раннеры могут быть использованы не только с собственной инсталляцией GitLab, но и с облачным сервисом GitLab.com.
А какими способами оптимизации ваших пайплайнов пользуетесь вы? Делитесь в комментариях!