Найти тему
Библиотека программиста

😺🐙🗄️ Кэширование в GitHub Actions: основные нюансы, проблемы и решения

Оглавление

Рассмотрим тонкости работы с кэшем в GitHub Actions, обсудим распространенные проблемы и предложим эффективные решения, основанные на реальном опыте разработки крупного проекта.

GitHub Actions – это инструмент непрерывной интеграции и доставки (CI/CD), встроенный в платформу GitHub. Он позволяет автоматизировать различные рабочие процессы – сборку, тестирование и деплой. Среди основных возможностей GitHub Actions:

  • Автоматизация сборки, тестирования и развертывания кода.
  • Запуск задач по расписанию или в ответ на определенные события в репозитории.
  • Настройка сложных рабочих процессов с помощью YAML-файлов.
  • Использование готовых действий из Marketplace или создание собственных.
  • Интеграция с другими сервисами и API (для создания сложных и многоэтапных рабочих процессов, взаимодействующих с внешними системами).
  • Ускорение рабочих процессов с помощью кэширования.

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

♾️ Библиотека devops’a

Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека devops’a»

Зачем используют кэширование в GitHub Actions

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

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

В теории настройка кэширования выглядит очень просто:

  • Используем действие actions/cache.
  • Определяем кэш с помощью ключа и пути к файлам/директориям.
  • При совпадении ключа кэш восстанавливается.
  • Если ключ не найден – создается новый кэш.

На практике есть нюансы.

Нюансы кэширования в GitHub Actions

Кейс: настройка кэширования для монорепозитория с 30+ пакетами.

Один из разработчиков Prosopo (открытого проекта, нацеленного на создание децентрализованной сети обнаружения ботов) недавно пришел к выводу, что пора заняться настройкой кэширования: время сборки достигло 20 минут. До этого разработчики использовали GitHub Actions для автоматизации всех аспектов CI/CD, но кэширование никогда не настраивали. Рабочий процесс выглядел стандартно:

  • Весь код проекта расположен в монорепозитории.
  • Основная ветка main содержит стабильную версию кода. Когда нужно добавить новую функцию, разработчик создает новую ветку от главной.
  • Когда функция готова, разработчик создает запрос на слияние. Перед тем, как изменения попадут в главную ветку, запрос на слияние должен пройти ряд автоматических проверок и тестов.
  • Если все проверки пройдены успешно, изменения из новой ветки добавляются в главную ветку.

Со временем количество пакетов достигло 30 (некоторые их них написаны на Rust). При каждом запуске рабочего процесса все эти пакеты собирались заново с нуля, даже если была изменена всего одна строка кода в одном из них. В результате продолжительность сборки приблизилась к 20 минутам, разработчики начали переключаться на другие задачи во время ожидания. Стало ясно, что пора читать документацию GitHub Actions по кэшированию. В документации все выглядит просто и понятно: кажется, что настройка займет не более 5 минут. На деле работа заняла несколько дней, а в процессе выявились нюансы, которым не уделяется никакого особого внимания в документации, но при этом их необходимо учитывать для реализации успешной стратегии кэширования.

Статья по теме

📜👆 Руки прочь: автоматизация ручных задач с помощью GitHub Actions

1. Кэшируемые директории должны существовать до восстановления кэша

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

Решение: нужно использовать команду mkdir -p для создания всех кэшируемых директорий перед восстановлением кэша:

   Кэшируемые директории должны существовать до восстановления кэша
Кэшируемые директории должны существовать до восстановления кэша

2. Разные ветки не могут иметь общий кэш

Из-за этого ограничения кэширование помогает только при инкрементальных сборках в рамках одной ветки. То есть:

  • Вы делаете сборку проекта.
  • Вносите небольшие изменения.
  • Делаете новую сборку, используя результаты предыдущей для ускорения процесса.

Но если у вас есть две очень похожие ветки (X и Y), то ветка Y не может использовать результаты сборки ветки X для ускорения своей сборки, даже если изменения минимальны:

   Разные ветки не могут иметь общий кэш
Разные ветки не могут иметь общий кэш

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

3. Дочерние ветки могут наследовать кэш от родительской ветви

Это очень полезная фича, которая позволяет обойти ограничение на прямой обмен кэшами между ветками. Вот как это работает:

  • Предположим, у вас есть ветка C (родительская).
  • От нее создаются ветки A и B (дочерние).
  • Если вы соберете проект в ветке C и закэшируете результат, то ветки A и B получат доступ к этому кэшу:
   Дочерние ветки могут наследовать кэш от родительской ветви
Дочерние ветки могут наследовать кэш от родительской ветви

Эту особенность можно использовать с максимальной пользой, если все ветки в репозитории наследуются от главной:

  • Проект собирается на главной ветке, результат кэшируется.
  • Теперь все ветки могут использовать общий кэш – в результате процесс сборки для каждой из них значительно ускоряется.

4. Неизменность кэша

Как ни странно, после создания кэша его нельзя обновить. Это создает нелепую проблему:

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

Обычное решение этой проблемы – удалить старый кэш и создать новый. Но это создает новую проблему:

  • Между удалением старого кэша и созданием нового всегда есть некий промежуток времени.
  • Для больших кэшей (например, 1,3 ГБ) этот промежуток может составлять около 2 минут.
  • В это время рабочие процессы не имеют доступа к кэшу, что значительно увеличивает время их выполнения (до 20 минут).

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

  • При каждом слиянии запроса на вытягивание.
  • При каждом коммите в открытом запросе на вытягивание, который запускает процессы CI/CD.

При наличии множества активных запросов на вытягивание (например, 10 запросов одновременно – обычное дело для проекта) это превращается в очень серьезную проблему:

-4

Решить проблему с неизменяемыми кэшами можно с помощью символов подстановки в именах. Если имя кэша заканчивается на -, оно рассматривается как регулярное выражение с подстановочным знаком в конце. Например, abc-def- будет соответствовать abc-def-1, abc-def-2 и т.д. GitHub использует самый последний кэш.

Чтобы обеспечить специфичность кэша для ОС и архитектуры, а также отличать кэши разных запусков, стоит давать им названия в таком формате:

Таким образом, кэши будут сохраняться с известным префиксом и неизвестным суффиксом (в который входит id и попытка запуска). Если при восстановлении кэша указано имя project-cache-${{ runner.os }}-${{ runner.arch }}-– GitHub будет использовать самый последний подходящий кэш.

Статья по теме

🏃 Работаем нон-стоп: непрерывная интеграция и непрерывное развертывание кода (CI/CD)

5. Ограничение размера кэша

Лимит кэша в GitHub – 10 Гб. При достижении лимита GitHub автоматически удаляет самые старые кэши. Ограничивать объем кэша, конечно, нужно – серверы не резиновые. Но если у проекта есть, например, два кэша с разной частотой обновления, это может привести к проблемам: тот кэш, что обновляется часто (и в итоге приводит к превышению лимита), GitHub не тронет, а удалит второй, который обновляется реже (и может быть гораздо важнее, чем первый).

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

  • Сначала сохраняем новый кэш. Он включает несколько директорий и использует уникальный ключ, содержащий информацию о ОС, архитектуре, ID запуска и попытке.
  • После сохранения у нас будет ≥ 2 кэша (включая предыдущий). Напишем действие, которое удалит все кэши, кроме только что созданного:

Этот код:

  • Устанавливает расширение gh для работы с кэшами GitHub Actions.
  • Получает список ключей кэшей, соответствующих шаблону project-cache-${{ runner.os }}-${{ runner.arch }}-.
  • Удаляет все кэши, кроме самого последнего.

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

Как выглядит процесс кэширования

Процесс запускается после слияния запроса в ветку main (или вручную через GitHub). Проект собирается, результат сохраняется в кэше:

Особенности:

  • Создаются четыре директории: cargo-cache, target, node_modules, Cypress cache. Это делается для безопасности, чтобы избежать сбоя механизма кэширования, если директории не существуют.
  • Ключ кэша project-cache-${{ runner.os }}-${{ runner.arch }} позволяет иметь разные кэши для разных ОС и архитектур.
  • Кэш не восстанавливается из предыдущих сборок, чтобы избежать петли кэширования: это предотвращает накопление артефактов от предыдущих сборок и разрастание кэша, с которым GitHub расправляется беспощадно :(.

При создании запроса на вытягивание автоматически запускаются CI/CD-процессы. Так, например, выглядит использование кэша в процессе тестирования:

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

Что еще стоит сделать

Важно настроить рабочие процессы так, чтобы их можно было отменить при необходимости:

  • Если в проект вносятся новые изменения (например, новый коммит), старые проверки уже не актуальны.
  • Отмена устаревших процессов позволяет сразу начать проверку самой свежей версии кода.
  • И самое главное – отменяемость предотвращает одновременное создание нескольких кэшей. Хотя (теоретически) несколько одновременных процессов создания кэша не должны конфликтовать, есть небольшой шанс, что в итоге вы получите кэш от более старого процесса вместо нового.

Отменяемость процессов в GitHub Actions это обычно реализуют с помощью настройки concurrency – она позволяет автоматически отменять предыдущие запущенные процессы при появлении новых.

Подведем итог

Кэширование в GitHub Actions – мощный инструмент оптимизации, способный значительно ускорить процессы CI/CD. Однако, как мы увидели, его эффективное использование требует знания нюансов работы платформы и преодоления ряда ограничений. Применяя описанные в статье подходы – от правильного именования кэшей до стратегий их обновления и очистки – можно существенно сократить время выполнения рабочих процессов, особенно в крупных проектах.