Кэширование зависимостей CI/CD: где сборка теряет минуты
Каждый запуск CI/CD-конвейера начинается с чистого окружения, и первое, что оно делает, — заново скачивает и собирает те же зависимости. Повторная загрузка пакетов занимает от половины до двух третей времени сборки конвейера: это минуты ожидания правки после каждого коммита вместо работы. А когда внешний реестр отвечает медленно или ограничивает частоту запросов, конвейер падает не из-за кода.
Кэширование зависимостей CI/CD снимает именно этот участок, но работает только при точном ключе. Ключ, привязанный к lock-файлу, инвалидирует кэш ровно тогда, когда изменился набор версий, и даёт попадание почти на каждом запуске. Ключ по ветке или номеру сборки даёт обратное: либо регулярные промахи, либо тихое залипание на устаревших версиях, которое всплывает уже на стенде.
Второе узкое место сборки — путь до внешних реестров, а не вычисления. Локальный прокси-реестр и артефакт-хранилище переводят загрузку во внутреннюю сеть: пакеты приходят с диска рядом с раннерами и перестают зависеть от доступности публичного источника. Сюда же ложится разогрев кэша по расписанию, чтобы первый запуск после обновления не тянул всё заново.
На выходе время сборки конвейера сокращается в разы, а сама сборка перестаёт зависеть от внешней инфраструктуры. Разбираем, как это закрывается на практике: ключи и слои кэша, общий кэш для параллельных задач и политика очистки, которая не даёт ему разрастаться.
Что меняется в скорости сборки и надёжности доставки
Кэш зависимостей меняет профиль сборки: шаг установки перестаёт быть самым дорогим. Когда хранилище прогрето, время определяется компиляцией и тестами, а не скачиванием — цикл проверки изменений сокращается в 2-4 раза, и правка проверяется, пока контекст задачи ещё в голове.
Ключ кэша считается по контрольной сумме файла зависимостей, поэтому шаги, которые раньше ходили в реестр, забирают готовые артефакты из локального хранилища. Сеть выпадает из 80-90% сборок: внешние обращения остаются только там, где зависимости действительно менялись.
Нагрузка на реестр пакетов падает кратно — уходят очереди, ограничения по частоте запросов и таймауты на пиках. Заодно реестр перестаёт быть единой точкой отказа: при его недоступности конвейер доходит до конца на прогретом кэше, падает только шаг обновления зависимостей.
Отдельный эффект — классы отказов, которые исчезают совсем. Больше не воспроизводятся обрывы загрузки на середине, расхождение версий косвенных зависимостей между запусками и сборки, зелёные со второго прогона. Стабильность конвейера становится свойством конфигурации, а не удачи.
Предсказуемое время сборки — прямое следствие: разброс между быстрым и медленным прогоном сжимается до 10-15% вместо двукратного. По такой длительности уже можно планировать выпуск и держать окно доставки фиксированным.
Способы хранения артефактов сборки: сравнение вариантов
От того, где лежит кэш, зависит, сколько шаг установки зависимостей добавляет к каждой сборке. Локальный кэш на раннере берёт файлы прямо с диска и работает быстрее всех, но привязан к конкретному узлу: задача уехала на другой раннер или машина пересоздана — кэш пустой, и установка идёт с нуля. Общий кэш в объектном хранилище убирает привязку к узлу, но требует продуманного ключа и ограничений при одновременной записи из нескольких сборок.
| Вариант | Область видимости | Скорость | Сопровождение | Параллельные сборки |
|---|---|---|---|---|
| Локальный кэш на раннере | Один узел и его рабочий каталог | Максимальная, файлы на диске | Минимальное: периодическая очистка | Неустойчив: узлы не делят данные, растёт дублирование |
| Общий кэш в объектном хранилище | Все раннеры, ветки и окружения — по ключу | Высокая при тёплом кэше, зависит от сети | Среднее: правила инвалидации, срок жизни объектов | Устойчив при блокировке записи по ключу |
| Кэш в реестре образов | Проект целиком, слои доступны всем задачам | Высокая, слои подтягиваются параллельно | Низкое: жизненный цикл задаётся политикой реестра | Устойчив: слои неизменяемы, конфликтов записи нет |
| Разделяемая сетевая файловая система | Все узлы, где смонтирован том | Средняя, упирается в задержки ввода-вывода | Высокое: права, квоты, согласованность данных | Ограниченно: запись становится узким местом |
Область видимости задаётся ключом: если в него входит хеш файла зависимостей и версия окружения, устаревшие данные не переиспользуются, а ветки не вытесняют друг друга. Правила инвалидации важнее объёма — кэш без ограничения по ключу и сроку жизни растёт быстрее, чем приносит ускорение. Поэтому вариант выбирают по паре «скорость + сопровождение», а не по максимальной скорости в отрыве от остального.
Порядок внедрения: от аудита до передачи команде
Кэш не настраивают на глаз: сначала замеряем, какую долю времени цикла занимает установка зависимостей и как часто повторные запуски переиспользуют уже скачанные пакеты. Снимаем базовый срез — время по шагам, доля попаданий, объём загружаемых архивов — и только потом трогаем конфигурацию.
- Аудит конвейера. Разбираем задания: где ставятся зависимости, читается ли локальный кэш между запусками, какие ветки и окружения используют один и тот же набор. Фиксируем текущее поведение и причины промахов.
- Схема ключей кэша. Ключ собираем из неизменяемых признаков: файл блокировки зависимостей, версия рантайма, тип задания, базовый образ. Он должен меняться при обновлении набора пакетов и оставаться стабильным при правке кода приложения.
- Разделение областей. Отдельный кэш на сборку, тесты и развёртывание — смешение делает ключ слишком широким, и задания начинают получать чужие наборы пакетов.
- Проверка попаданий. Серия из холодной и повторных сборок: сверяем долю попаданий и время шага установки. Отдельно прогоняем ветку с обновлением зависимостей — ключ обязан смениться.
- Инвалидация. Правило хранения: срок жизни и лимит объёма, чтобы кэш не разрастался и не отдавал устаревшие версии пакетов.
- Передача настроек. Конфигурация лежит в репозитории рядом с кодом; схему ключей, правило очистки и порядок действий при промахах отдаём команде короткой документацией.
Комплект поставки: конфигурации, политики, регламенты
Комплект поставки — это то, что остаётся в репозитории и в системе после того, как кэширование заработало. Без него ускорение сборок держится на одном инженере: при смене команды или переезде хранилища конвейер тихо возвращается к полной загрузке зависимостей, а причина видна только по выросшему времени сборки.
- Конфигурация конвейера с явными шагами восстановления и сохранения кэша и порядком их выполнения относительно установки зависимостей.
- Схема ключей: из чего формируется ключ (файл блокировок, ветка, тип задачи), какие слои общие, а какие привязаны к конкретной сборке.
- Политика хранения: срок жизни записей, предельный объём, правило вытеснения старых слоёв и частота полной инвалидации.
- Правила доступа и разделения: кто может перезаписывать общий кэш, что принципиально не попадает в него — прежде всего секреты и приватные реестры.
- Схема отказоустойчивости: поведение сборки при недоступном хранилище — конвейер деградирует до обычной загрузки, но не падает.
- Документация по сборке: как воспроизвести окружение локально, что делать при несовпадении ключа и как проверить, что зависимости взяты из кэша, а не скачаны заново.
- Регламент сопровождения: периодичность ревизии записей, порядок инвалидации при смене базового образа и ответственный за изменения схемы ключей.
- Инструкция по диагностике типовых сбоев: ложные попадания после обновления зависимостей, рост объёма хранилища, замедление на этапе распаковки.
Отдельно фиксируем точку входа: как добавить кэш для нового сервиса, не ломая уже настроенные. Это тот минимум, после которого ускорение сборок воспроизводимо, а не разово.
Кейс: сборка монорепозитория ускорилась в разы
Монорепозиторий на 40+ пакетов собирался 35–40 минут, и почти всё это время уходило на повторную загрузку одних и тех же зависимостей из внешних источников. Кэш формально был включён, но ключ строился по номеру ветки — при каждом коммите он менялся, и хранилище not попадало в цель. Хуже того, при сетевых сбоях на стороне реестра падал весь конвейер: 5–7 упавших сборок в неделю, каждая — ручной перезапуск и потерянные часы команды.
Что изменили
Пересобрали схему ключей: базовый слой с системными пакетами и инструментами фиксированной ветки обновляется по расписанию, слой зависимостей привязан к lock-файлу, слой артефактов — к содержимому самих пакетов. Так изменение кода в одном модуле не инвалидирует кэш соседних. Рядом подняли локальное зеркало реестра с прогревом по самым частым пакетам — конвейер перестал зависеть от доступности внешней сети. На GitLab CI это дало ещё и параллельный прогрев: ветка графа, затронутая правкой, собирается заново, остальное подтягивается из хранилища.
Метрики до и после
| Показатель | До | После |
|---|---|---|
| Медианная длительность сборки | 38 мин | 9 мин |
| Доля попаданий в кэш | 12% | 78% |
| Сбои из-за недоступности источников пакетов | 5–7 в неделю | 0 |
| Доля сборок без пересборки зависимостей | 8% | 84% |
Отдельно проверили инвалидацию: при обновлении lock-файла пересобирается только затронутая часть графа, откат на предыдущий набор зависимостей остаётся возможным. Правки в кэш-конфигурации проходят ревью как обычный код — регрессии ловятся на первом же прогоне.
Чем кэширование зависимостей отличается от послойного хранения?
Это разные уровни ускорения сборки, и путают их часто. Послойное хранение образов кэширует результат сборки самого образа: каждый слой — набор изменений файловой системы, привязанный к инструкции, которая его создала. Кэш зависимостей живёт отдельно от образа и хранит артефакты менеджера пакетов, скачанные или собранные в ходе работы конвейера.
Инвалидация у них разная. Инвалидация слоёв идёт цепочкой: изменение любой инструкции выше по описанию сбрасывает все последующие слои, поэтому манифест зависимостей копируют до исходного кода — тогда правка кода перестаёт пересобирать установку пакетов. Кэш зависимостей инвалидируется по ключу: хэш манифеста с зафиксированными версиями. При промахе берётся ближайший предыдущий ключ и докачивается только недостающее.
В этом и смысл разделения уровней: образ отвечает за среду исполнения — системные библиотеки, исполняющую среду, собранное приложение. Кэш зависимостей ускоряет шаги, которые в образ вообще не попадают: модульные тесты, проверки стиля, статический анализ, сборку артефактов. Если кэшировать только образы, эти шаги на каждом запуске заново тянут одни и те же пакеты.
Эффект виден на контрасте горячего и холодного прогона: холодный измеряется минутами, горячий — десятками секунд, а узкое место смещается с установки зависимостей на сам прогон тестов. На практике кэш держат общим для ветки, чтобы параллельные сборки не перетирали записи друг друга.
Проверяется это по логам шагов: попадание в кэш и причина промаха печатаются явно. Если промах повторяется на каждом запуске — в ключ затесалось изменяющееся значение вроде номера сборки, либо слой с зависимостями стоит после копирования исходного кода.
Ограничения хранения и типичные ошибки настройки
Кэш зависимостей ведёт себя предсказуемо ровно до момента, когда меняется то, что лежит за его пределами: версия библиотеки, состав окружения или источник пакетов. Ниже — четыре ситуации, которые чаще всего всплывают уже в рабочем процессе.
Разберём их по одной, с признаком сбоя и способом настройки.
Кэш отдаёт устаревшие зависимости — как это поймать?
Признак: сборка проходит, но в артефакте остаётся прошлая версия библиотеки. Ключ записи должен строиться на хеше файла блокировки зависимостей, а не на имени ветки — тогда любое обновление версий автоматически промахивается мимо старой записи. Плюс срок жизни: данные, не запрошенные дольше заданного числа циклов, вычищаются.
Два окружения перетирают кэш друг друга
Это конфликт ключей: ключ собран из имени ветки, и одинаковые имена в разных репозиториях или окружениях попадают в одну запись. Ключ делаем составным — идентификатор репозитория, окружение, платформа сборки. Цена ошибки невелика: при промахе зависимости собираются заново, чужой артефакт в сборку не попадёт.
Что делать с закрытым реестром пакетов?
Внутренний реестр не отдаёт пакеты наружу и требует авторизации на каждый запрос. Кэш ставим на стороне агента сборки, внутри периметра: наружу уходят только проверенные артефакты. Учётные данные храним в секретах, а не в конфигурации кэша, — иначе они утекают вместе с дампом.
Хранилище растёт бесконечно — как ограничить?
Без политики очистки кэш растёт быстрее кода: каждая сборка добавляет слои, старые не удаляет никто. Правило удержания — последние несколько записей на ключ плюс удаление всего, что не запрашивалось за заданный период. Объём выходит на плато, доля промахов остаётся в допустимых пределах.
Обсудим задачу: что уточнить перед стартом
Чтобы вернуться с оценкой и планом работ, а не с общими обещаниями, нам нужны вводные по вашему конвейеру. Ниже — что уточняем на первом разговоре: этого набора хватает, чтобы посчитать объём, порядок этапов и риски.
- Как устроен конвейер: одна сборка на репозиторий или матрица целей, какие стадии обязательны, что уже кэшируется.
- Менеджеры пакетов и файлы фиксации версий: сколько их и как часто меняются — от этого зависит ключ кэша и поведение при обновлении зависимостей.
- Где хранится кэш: локальный диск сборочного агента, общий сетевой том или объектное хранилище — влияет на скорость и на очистку устаревшего.
- Топология агентов: свои машины или облачные, сколько одновременных сборок, есть ли ограничения по сети до внешних реестров.
- Структура репозиториев и общих библиотек: держать кэш общим или разделять по проектам.
- Доступы и периметр: кто владеет настройками агентов, закрытая сеть, разрешён ли прокси.
- Целевое время сборки: сколько занимает типичная сборка сейчас и какая цель по времени — по этому считаем эффект.
С этими вводными мы возвращаемся с оценкой сроков, порядком этапов и перечнем изменений в конфигурации агентов. Опишите задачу текстом или ссылкой на текущую конфигурацию — предложим план работ.







