Эффективные стратегии кэширования сборок Jenkins для ускорения CI/CD

Ускорьте свои конвейеры CI/CD Jenkins, освоив стратегии кэширования сборок. В этом руководстве подробно описаны практические методы повторного использования зависимостей, результатов компиляции и слоев Docker между сборками. Узнайте, как использовать сохранение рабочего пространства, параметры сборки Docker и методы общего кэширования, чтобы минимизировать повторяющиеся задачи и значительно ускорить процессы интеграции и развертывания.

Эффективные стратегии кэширования сборок Jenkins для ускорения CI/CD

Кэширование сборок Jenkins — это не одна функция. Это набор решений о том, что можно безопасно повторно использовать между сборками. Хороший кэш экономит загрузку зависимостей, слои Docker, работу компилятора и метаданные менеджера пакетов. Плохой кэш скрывает сломанные сборки, заполняет диски или заставляет конвейер проходить на одном агенте и падать на другом.

Начните с поиска самого медленного повторяющегося шага в журнале задания. Если каждая сборка тратит две минуты на загрузку артефактов Maven, кэшируйте Maven. Если Docker перестраивает одни и те же базовые слои каждый раз, исправьте кэширование Docker. Если тесты медленные, потому что компиляция начинается с нуля при каждом запуске, используйте собственный кэш инструмента сборки, прежде чем изобретать архив на уровне Jenkins.

Сохраняйте рабочие пространства, когда они полезны, очищайте их, когда они вредят

Простейший кэш — это существующее рабочее пространство Jenkins на постоянном агенте. Если одно и то же задание выполняется на одном узле, файлы, оставшиеся от предыдущей сборки, можно повторно использовать.

Это может помочь с такими инструментами, как Maven, Gradle, npm, pnpm, Cargo и Go. Это также может вызывать странные сбои, когда в рабочем пространстве остаются сгенерированные файлы, старые отчеты о тестах или устаревшие результаты сборки.

Распространенный компромисс — очищать только дерево исходных кодов и хранить выделенные каталоги кэша за его пределами:

pipeline {
  agent { label 'linux-build' }
  environment {
    MAVEN_OPTS = '-Dmaven.repo.local=/var/cache/jenkins/maven'
    npm_config_cache = '/var/cache/jenkins/npm'
  }
  stages {
    stage('Checkout') {
      steps {
        deleteDir()
        checkout scm
      }
    }
    stage('Build') {
      steps {
        sh 'mvn -B test'
      }
    }
  }
}

Это сохраняет воспроизводимость рабочего пространства, одновременно повторно используя загруженные зависимости. Убедитесь, что права доступа к каталогу кэша соответствуют пользователю, запускающему агента.

Кэшируйте зависимости по правилам инструмента

Кэши зависимостей работают лучше всего, когда ими управляет менеджер пакетов. Не архивируйте и не восстанавливайте node_modules на несвязанных агентах, если у вас нет веской причины. Обычно безопаснее кэшировать хранилище загрузок менеджера пакетов.

Для npm:

environment {
  npm_config_cache = "${WORKSPACE}/.npm-cache"
}
steps {
  sh 'npm ci'
}

Для pnpm:

environment {
  PNPM_STORE_PATH = "${WORKSPACE}/.pnpm-store"
}
steps {
  sh 'pnpm install --frozen-lockfile'
}

Для Maven используйте стабильный путь к локальному репозиторию:

sh 'mvn -B -Dmaven.repo.local=/var/cache/jenkins/m2 test'

Для Gradle сохраняйте GRADLE_USER_HOME стабильным и включайте кэш сборки Gradle в проекте, когда это уместно:

environment {
  GRADLE_USER_HOME = '/var/cache/jenkins/gradle'
}
steps {
  sh './gradlew test --build-cache'
}

Файл блокировки — ваша страховка. Если package-lock.json, pnpm-lock.yaml, pom.xml или build.gradle изменяется, инструмент должен загрузить правильные новые зависимости. Если кэш продолжает обслуживать старые или поврежденные пакеты, удалите его и позвольте инструменту перестроить его.

Кэширование слоев Docker

Кэширование Docker зависит от того, где демон Docker хранит слои. На долгоживущем агенте VM обычный docker build может автоматически повторно использовать слои. На эфемерных агентах Kubernetes следующая сборка часто попадает на новый pod без истории слоев.

Для постоянных агентов Docker размещайте медленно изменяющиеся строки Dockerfile в начале:

FROM node:22-bookworm
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm test

Если вы скопируете весь репозиторий до npm ci, каждое изменение исходного кода аннулирует слой зависимостей.

Для эфемерных агентов используйте кэширование реестра BuildKit:

docker buildx build \
  --cache-from type=registry,ref=registry.example.com/my-app:buildcache \
  --cache-to type=registry,ref=registry.example.com/my-app:buildcache,mode=max \
  -t registry.example.com/my-app:${GIT_COMMIT} \
  --push .

Этот кэш используется совместно через реестр, а не через локальный демон. Обычно он лучше подходит для агентов Jenkins на основе Kubernetes, чем попытка смонтировать доступный для записи каталог слоев Docker во множество pod'ов.

Архивируйте артефакты, а не кэши, если вы не имеете это в виду

archiveArtifacts полезен для результатов сборки, которые вы хотите сохранить: JAR-файлы, отчеты о тестах, файлы покрытия, сгенерированные пакеты. Это не очень хорошее универсальное хранилище кэша. Большие архивы зависимостей замедляют контроллер, увеличивают нагрузку на хранилище и усложняют очистку.

Если вам нужен кэш для нескольких агентов, предпочтите реальное внешнее расположение кэша: репозиторий артефактов, корзину объектного хранилища, прокси пакетов или кэш реестра. Для сборок Java менеджер репозиториев, такой как Nexus или Artifactory, часто дает лучшие результаты, чем копирование .m2 между агентами Jenkins. Для Docker кэш BuildKit на основе реестра более предсказуем, чем архивирование каталогов слоев.

Сделайте ключи кэша видимыми

У кэша должна быть причина для валидности. Хорошие ключи кэша включают операционную систему, архитектуру, основную версию языка, версию менеджера пакетов и хэш файла блокировки.

Например, ключ кэша Node может быть основан на:

linux-amd64-node22-npm10-sha256(package-lock.json)

Вам не нужен модный плагин кэша, чтобы применить эту идею. Даже имя каталога может нести ключ:

sh '''
LOCK_HASH=$(sha256sum package-lock.json | awk '{print $1}')
export npm_config_cache="/var/cache/jenkins/npm/node22-${LOCK_HASH}"
npm ci
'''

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

Следите за режимами сбоев

У кэширования есть несколько знакомых шаблонов сбоев:

  • Сборки проходят только на одном агенте, потому что у этого агента есть скрытый файл в рабочем пространстве.
  • Диск заполняется, потому что старые кэши никогда не очищаются.
  • Образы Docker перестраиваются с нуля, потому что Dockerfile копирует изменчивые файлы слишком рано.
  • Общий кэш повреждается, когда несколько сборок записывают в него одновременно.
  • Восстановление огромного кэша занимает больше времени, чем загрузка зависимостей из ближайшего прокси пакетов.

Журналы сборки должны показывать, используется ли кэш. Maven сообщает, когда загружает зависимости. Docker показывает CACHED или промахи кэша с выводом BuildKit. Gradle имеет сканирование сборки и вывод консоли для поведения кэша. Если кэш невидим, добавьте логирование вокруг его пути, размера и ключа.

Практический план развертывания

Выберите один конвейер и одно узкое место. Добавьте кэш только для этого шага. Запустите один и тот же коммит дважды и сравните журналы. Затем запустите после изменения файла блокировки зависимостей и подтвердите, что кэш аннулируется правильно. Наконец, добавьте очистку.

Для постоянных агентов очистка может быть задачей cron или задачей обслуживания Jenkins:

find /var/cache/jenkins/npm -mindepth 1 -maxdepth 1 -type d -mtime +30 -exec rm -rf {} +
find /var/cache/jenkins/m2 -type f -name '*.lastUpdated' -delete
docker system prune -af --filter 'until=168h'

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

Лучшая стратегия кэширования сборок Jenkins — скучная: кэшируйте дорогую повторяющуюся работу, позволяйте инструменту сборки проверять корректность, делайте пути кэша явными и удаляйте старые данные до того, как диск агента станет следующим узким местом.

Сопоставьте кэш с моделью агента

Постоянные агенты VM и одноразовые облачные агенты требуют разных конструкций кэша. На постоянной VM локальные каталоги дешевы и быстры. Кэш Maven в /var/cache/jenkins/m2 или кэш слоев Docker на демоне могут сохраняться неделями. Риск — рост диска и устаревшее состояние.

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

Для многих команд лучшая первая инвестиция — это не плагин Jenkins. Это ближайший прокси зависимостей:

  • Maven или Gradle через Nexus или Artifactory.
  • npm через частный реестр или прокси.
  • Docker через зеркало реестра.
  • Python через зеркало пакетов или внутренний индекс.

Это улучшает каждого агента без восстановления огромных архивов в начале каждого задания.

Знайте, когда не кэшировать

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

Также скептически относитесь к кэшированию, когда шаг восстановления больше, чем работа, которую он экономит. Архив размером 2 ГБ, загрузка и распаковка которого занимает 90 секунд, не полезен, если менеджер пакетов может чисто установиться за 45 секунд из локального прокси.

Измеряйте холодные и теплые сборки:

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

Эти четыре запуска показывают, помогает ли кэш реальному рабочему процессу или только делает одну искусственную пересборку хорошей.

Сделайте очистку кэша частью дизайна

Каждый кэш нуждается в истории истечения срока до того, как он будет запущен. На статических агентах планируйте очистку вне пиковых часов сборки. В общих реестрах или объектных хранилищах используйте политики жизненного цикла. В Jenkins отслеживайте размер кэша как операционный показатель, а не как запоздалую мысль.

Нормально удалять кэши во время инцидента. Хороший конвейер должен стать медленнее после удаления кэша, а не сломаться. Если удаление кэша ломает сборку, кэш скрывает отсутствующее объявление зависимости или предположение об окружении.