Кеш сборки в CI под нагрузкой: что ломается первым
Кеш способен ускорить сборку, но при параллельных CI-задачах он сам становится узким местом: растут задержки, ключи конфликтуют, а полезные слои вытесняются. Разбираем GitHub Actions Cache и BuildKit: что измерять и как найти предел.

Когда десятки CI-задач одновременно загружают и сохраняют кеш, сборка может замедлиться сильнее, чем без кеша: время уходит на передачу больших слоёв, конкуренцию за хранилище и повторные загрузки после промаха. Ниже — разбор связки GitHub Actions Cache и BuildKit с backend `gha`: где возникает очередь, какие данные теряются первыми и как измерить эффект без выдуманных процентов.
Что именно кешируется
В этой связке есть два разных механизма. GitHub Actions Cache сохраняет файлы, которые шаги workflow явно помещают в кеш. BuildKit строит граф операций и хранит результаты отдельных шагов Dockerfile; backend `gha` позволяет ему использовать кеш GitHub Actions. Это не один общий кеш: у механизмов разные ключи, правила доступа и жизненный цикл.
Для сборки контейнера важно, чтобы BuildKit мог повторно использовать результат операции, а не только скачанные исходники. Если изменился слой с ранним `COPY`, все последующие операции могут стать недействительными. Если же меняется только код, расположенный после установки зависимостей, слой с зависимостями обычно остаётся пригодным. Это зависит от порядка инструкций и входных файлов конкретного Dockerfile.
Промах кеша — ситуация, когда нужного сохранённого результата нет или его нельзя применить, поэтому сборщик выполняет операцию заново.1
Под нагрузкой первым тормозит не CPU
Обычная проверка на одном запуске легко показывает ускорение: кеш уже прогрет, конкурентов почти нет, объём данных небольшой. В CI с параллельными ветками картина меняется. Сборка должна получить кеш из удалённого хранилища, распаковать применимые данные и, возможно, отправить новый снимок. Для крупных слоёв сетевой обмен и обработка архива могут съесть выигрыш от повторного использования.
Особенно рискован режим, при котором каждая задача публикует полный кеш, а множество задач одновременно его обновляет. Даже если сборка использовала кеш, сохранение может завершиться ошибкой или занять заметное время. Тогда следующий запуск не получает ожидаемого результата. Поведение зависит от версии BuildKit, настроек экспорта и ограничений GitHub Actions Cache; детали стоит сверять с документацией используемой версии.
Кеш окупается не количеством попаданий, а сокращённым временем CI с учётом загрузки и сохранения.
Что ломается первым
Слишком широкие ключи
Если разные ветки и конфигурации делят один ключ, обновления начинают мешать друг другу: задача может получить не тот набор данных для ожидаемого сценария или постоянно перезаписывать общий кеш. С другой стороны, ключ, содержащий каждый коммит, создаёт много почти одинаковых записей и снижает шанс повторного использования.
Для BuildKit важна не только строка ключа, но и область кеша, которую задаёт используемая интеграция. Не копируйте ключи из чужого workflow вслепую: проверьте, что параллельные сборки платформ, веток и образов не публикуют взаимозаменяемые данные под одним именем.
Большой экспорт и очереди
Полный экспорт удобен: следующая сборка может получить больше результатов. Цена — передача и сохранение большего объёма. При большом числе матричных задач несколько крупных экспортов одновременно создают конкуренцию за сеть и сервис кеширования. Если основная задержка приходится на сохранение, расширение кеша может ухудшить время окончания workflow, даже когда собственно сборка стала короче.
Нестабильные входы
Кеш не исправляет сборку, которая зависит от плавающих версий пакетов, времени, внешнего состояния или неучтённых файлов. Такие входы делают результат непредсказуемым: одна и та же операция может пересобираться, а при ошибочной инвалидации — использовать не тот результат. В первую очередь проверьте, какие файлы и параметры реально влияют на каждый дорогой шаг.
Сравнение подходов
| Подход | Что выигрываете | Чем платите | Когда уместен |
|---|---|---|---|
| Кеш зависимостей через Actions Cache | Повторное использование скачанных пакетов между запусками | Нужно точно задавать ключи и учитывать менеджер пакетов; кеш не сохраняет автоматически результаты всех слоёв образа | Зависимости скачиваются долго, а сборка не требует сложного кеширования слоёв |
| BuildKit с backend `gha` | Повторное использование результатов операций сборки контейнера | Экспорт и импорт добавляют сетевую работу; при массовом параллелизме кеш-сервис может стать узким местом | Контейнер собирается часто, а дорогие слои стабильны |
| Локальный кеш на постоянном runner | Нет необходимости каждый раз передавать весь кеш удалённо | Нужны постоянные runner, дисковое пространство и контроль изоляции задач | Есть собственная инфраструктура и сборки часто повторяются на одном пуле |
Таблица описывает классы решений, а не гарантированное преимущество: фактический результат зависит от размера слоёв, пропускной способности сети, числа параллельных jobs и настроек runner.
Как измерить предел
Сравнивайте не только длительность команды сборки. Для одинаковых коммитов и одинаковой конфигурации runner запишите время всей job, время сборки, загрузки кеша и его экспорта, объём переданных данных, долю повторно использованных операций и число ошибок кеширования. Отдельно измерьте холодный запуск и прогретый.
- Зафиксируйте исходный workflow и соберите контрольную серию без изменения конфигурации. Сравните одинаковые ветки и типы runner.
- Запустите серии с возрастающим числом одновременных сборок. Не меняйте одновременно число jobs и структуру Dockerfile.
- Сопоставьте p50 и p95 времени job, а также длительность экспорта и импорта. Медиана покажет типичный запуск, p95 — проблемы, заметные наиболее медленным задачам.
- Повторите серию с кешем зависимостей и с BuildKit отдельно. Так станет видно, какой именно слой оптимизации помогает.
Не называйте разницу «ускорением кеша», если в тестах менялись runner, сеть или набор сборок. Для осмысленного сравнения зафиксируйте версию Buildx/BuildKit, runner image, архитектуру, Dockerfile и параметры параллелизма. GitHub-hosted runner image меняется независимо от вашего репозитория, поэтому записывайте фактический образ и версии инструментов из логов каждого запуска.
Где подход ломается
Удалённый кеш может оказаться невыгоден для небольших сборок: импорт и экспорт стоят дороже, чем повторное выполнение пары быстрых операций. На другом краю — огромные слои, которые долго передавать и распаковывать. В обоих случаях смотреть только на факт cache hit недостаточно: важна экономия времени всей job.
Ещё один граничный случай — параллельные платформенные сборки. У них могут быть общие промежуточные слои, но разные целевые архитектуры и параметры. Убедитесь, что конфигурация кеша сохраняет полезное разделение, а не превращает публикацию общего кеша в гонку. Изоляция между доверенными и недоверенными запусками тоже важна: не давайте непроверенным pull request влиять на кеш, от которого зависят сборки с доступом к секретам.
Инвалидация кеша из-за изменений Dockerfile — отдельная тема; здесь рассматриваем только поведение при параллельных сборках и стоимость обмена с хранилищем.
Что делать дальше
Начните с одной дорогой сборки и снимите её холодный и прогретый профиль. Затем добавьте контролируемую параллельность и отдельно измерьте импорт, сборку и экспорт. Если очередь появляется на передаче кеша, попробуйте уменьшить объём сохраняемых данных или сократить число одновременных публикаций; если много повторно выполняемых операций — проверьте порядок инструкций Dockerfile и входы, влияющие на кеш.
Порог полезности выбирайте по своим данным: оставляйте настройку, только если она снижает время полного workflow при ожидаемом уровне параллелизма и не повышает долю ошибок. Следите за p95, объёмом экспорта и сообщениями о неудачном сохранении. Именно эти показатели раньше среднего времени обнаружат, что кеш из ускорителя превратился в очередь.
1 В BuildKit кеш связан с результатами операций сборки и их входами; это не то же самое, что кеш пакетов менеджера зависимостей.
Источники: GitHub Actions: Dependency caching, Docker: Cache backends — GitHub Actions cache
Частые вопросы
Кеш GitHub Actions и кеш BuildKit — это одно и то же?
Почему сборка стала медленнее после включения кеша?
Стоит ли сохранять кеш после каждой параллельной сборки?
Как понять, что проблема именно в ключах кеша?
Можно ли доверять кешу сборки из pull request?
Какие версии нужно зафиксировать при замерах?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.