В современном мире программного обеспечения высочайшая скорость сборки и тестирования кода часто становится ключевым фактором успеха проекта. Для того чтобы ускорить процесс сборки Docker-образов, многие разработчики обращаются к функциям кэширования, предоставляемым GitHub Actions, особенно используя Docker caching. Тем не менее, использование механизма кэширования Docker на self-hosted раннерах (собственных серверах) может привести к серьезным проблемам, которые негативно скажутся на производительности и стабильности сборок. Особенно это актуально для крупных и комплексных приложений, где время отклика в CI/CD-пайплайнах критично. Разберемся, почему рекомендуется избегать использования стандартного GitHub Docker caching в таких условиях и какие альтернативы могут помочь.
GitHub Docker caching представляет собой технологию, позволяющую сохранять промежуточные слои Docker-образов, чтобы при последующих сборках использовать уже готовые части, а не пересобирать с нуля. На первый взгляд, это должно значительно снижать время сборки. Эта функция особенно удобна в средах Hosted runners, где инфраструктура поддерживается GitHub, и канал связи с сервисами кэширования оптимизирован. Но в случае self-hosted раннеров ситуация меняется кардинально. Одна из основных проблем — это скорость сети и пропускная способность канала между self-hosted раннером и GitHub storage.
На практике скорость передачи данных в таких условиях ограничивается примерно 32 МБ/сек, что почти в четыре раза медленнее, чем у стандартных GitHub хостед раннеров, которые обеспечивают скорость порядка 120 МБ/сек. Это ограничение оказывает непосредственное влияние на время загрузки и выгрузки кэша, которое становится неоправданно долгим — загрузка 1Гб кэша может занимать около минуты, а выгрузка — аналогичное время. Еще хуже, если зависимости, необходимые вашему приложению, не кэшируются на самом раннере. Исходя из особенностей работы менеджеров пакетов, таких как npm или yarn, даже небольшие изменения в установке зависимостей приводят к изменению слоя "install dependencies". Поскольку Docker кэширует слои последовательно, любое изменение в одном слое автоматически инвалидирует все последующие слои.
Это приводит к частым ситуациям, когда кэш сбрасывается и пересборка занимает столько же времени, сколько и без кэширования. Важно понимать, что Docker предлагает два режима кэширования: минимальный (min) и максимальный (max). В минимальном режиме кэшируются только те слои, которые экспортируются в итоговый образ, а максимальный режим хранит все промежуточные слои. Максимальный режим повышает шанс попадания в кэш, но требует больше места и времени на передачу данных. На практике выбрать оптимальный режим помогает экспериментирование в зависимости от специфики проекта.
При работе с несколькими Docker-образами или микросервисами, возникает сложность централизованного управления кэшем. В GitHub Actions есть два основных способа реализации кэширования: используя bake-action и вручную с помощью cache action. Из-за ограничений окружения, часто приходится выбирать ручной подход. Однако ручное кэширование требует дополнительных скриптов и логики для создания, восстановления и очистки кэша, что усложняет процесс и увеличивает риск ошибок. Еще один интересный момент касается лучшей производительности локальных сборок.
Часто сборка с Docker на локальном компьютере или на неочищенном self-hosted раннере, где Docker кэш не сбрасывается, завершается в два раза быстрее. Секрет в том, что Docker по умолчанию использует локальный кэш слоев без передачи их в удаленное хранилище. Таким образом, «горячий» кэш держится в пределах машины и исключает сетевые задержки. Однако такой подход противоречит принципам безопасности: раннеры должны быть эфемерными, чтобы избежать накопления нежелательных данных и угроз безопасности. Несмотря на то, что использование GitHub Docker caching на self-hosted раннерах выглядит логичным решением для ускорения CI/CD, на практике это приводит к нерегулярным и неоптимальным результатам.
Иногда скорость сборки достигает ожиданий — около 4-5 минут, но в худших случаях все равно составляет 11-12 минут, что сравнимо с полным отсутствием кэша. Ключевым фактором здесь является то, что отсутствие консистентного хеширования зависимостей приводит к частой инвалидизации кэша. Исходя из анализа, существуют две разумные альтернативы. Первая — это повторное использование самих раннеров, то есть «разогрев» кэша внутри Docker по умолчанию, без обращения к внешним сервисам кэширования. Такой подход позволяет сократить время сборки до 2-3 минут, что существенно быстрее и стабильнее.
Однако он подразумевает компромисс в области безопасности и инфраструктурных практик, так как повышается риск несанкционированного доступа и накопления устаревших данных. Вторая альтернатива — использование специализированных решений, таких как GitHub Actions Cache Server, который позволяет создать свою инфраструктуру кэширования с лучшей производительностью и контролем на self-hosted раннерах. Такая настройка требует дополнительной конфигурации и поддержки, но в долгосрочной перспективе улучшает стабильность и эффективность сборочных процессов. Подводя итог, можно с уверенностью утверждать, что применение стандартного Docker caching в GitHub на self-hosted раннерах чаще всего не оправдывает ожиданий. Ограничения пропускной способности сети, природа зависимостей и архитектура кэширования приводят к непредсказуемым результатам и дополнительным затратам времени.