open atlas
↑ К треку
CI/CD-пайплайны CICD · 08 · 04

Артефакты и кэш: передача файлов между джобами и кэш, который не попадает

Артефакты двигают файлы между джобами и наружу из запуска (дефолтное хранение 90 дней); кэш ускоряет повторные запуски через restore-ключ. Классический провал: ключ кэша без хэша lockfile, поэтому он не инвалидируется, или не попадает через окно вытеснения 10 ГБ / 7 дней.

CICD Senior ◷ 16 min
Уровень
ОсновыJuniorMiddleSenior

«Кэш сломан — он никогда не попадает». Каждый запуск CI всё ещё занимал восемь минут на переустановку зависимостей, хотя команда добавила actions/cache неделями ранее. Логи говорили Cache not found for input keys почти на каждом запуске. Проблема была в ключе: кто-то написал key: node-modules — константную строку. Запись кэша пишется под своим ключом, только если записи с этим точным ключом ещё нет; раз node-modules сохранён на самом первом запуске, он стал неизменяемым, поэтому каждый последующий запуск его находил… но команда также недоумевала, почему апгрейды зависимостей никогда не вступали в силу в CI. Оба симптома — один баг с двух сторон: ключ без хэша содержимого никогда не инвалидируется (так устаревший кэш служит вечно) и, из-за несвязанной опечатки в пути, сохранённая запись была пуста, так что restore был no-op. Фиксом был канонический паттерн — key: node-${{ hashFiles('**/package-lock.json') }} плюс префикс restore-keys: — который привязывает идентичность кэша к lockfile, так что он инвалидируется ровно тогда, когда меняются зависимости, и переиспользует близкое совпадение в остальных случаях.

Артефакты против кэша: две разные задачи

Они выглядят похоже — оба хранят файлы — но решают противоположные проблемы:

  • Артефакты выносят файлы из workflow: выходы сборки, тест-отчёты, coverage, логи — то, что человек или поздний джоб скачивает. Загружай через actions/upload-artifact, забирай через actions/download-artifact. Это единственный способ передать файлы между джобами, ведь каждый джоб выполняется на свежем раннере. Дефолтное хранение — 90 дней (настраивается per-upload вниз до 1 дня или задаётся на весь репозиторий), после чего они удаляются.
  • Кэш ускоряет будущие запуски той же работы: каталоги зависимостей (~/.npm, node_modules, ~/.cargo), промежуточные результаты сборки. Используй actions/cache. Запись кэша ключуется и неизменяема после записи — и она best-effort (по возможности, без гарантии), не гарантирована: по политике вытеснения GitHub удаляет кэши, не запрашивавшиеся 7 дней, и когда кэши репозитория превышают 10 ГБ суммарно, он вытесняет least-recently-used записи, чтобы остаться под лимитом.

Ментальное правило: артефакт = «мне нужен этот файл позже или человеку», кэш = «пересчёт этого медленный, а входы редко меняются». Никогда не используй кэш для выходов сборки, которые нельзя терять (он может быть вытеснен в любой момент); никогда не используй артефакты как кэш скорости (нет restore по ключу, а 90-дневное хранение стоит места).

- uses: actions/upload-artifact@v4
  with:
    name: dist
    path: build/
    retention-days: 7      # сократи с дефолта 90 дней для эфемерных сборок

- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
      npm-${{ runner.os }}-

Ключ кэша — это весь дизайн

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

  • key должен включать хэш содержимого входовhashFiles('**/package-lock.json'). Когда lockfile меняется, хэш меняется, ключ меняется, и старый кэш правильно не используется. Ключ без хэша (key: npm из Hook) никогда не меняется, поэтому либо служит устаревшим кэшем вечно, либо — раз записи неизменяемы — фиксирует то, что сохранено первым.
  • restore-keys — упорядоченный список префиксов, пробуемых при промахе точного key. При изменении lockfile точный ключ ещё не существует, но restore-keys: npm-${{ runner.os }}- совпадает с предыдущим кэшем как частичное попадание — так ты стартуешь от node_modules прошлой сборки и устанавливаешь только дельту, затем сохраняешь под новым точным ключом. Это и превращает холодный кэш в тёплый сквозь подъёмы зависимостей.

Семантика сохранения ловит людей: actions/cache сохраняет автоматически в конце джоба только если точный key ещё не существовал (попадание по точному ключу означает отсутствие сохранения). Неизменяемость означает, что нельзя перезаписать запись — чтобы «обновить» кэш, надо изменить ключ. А из-за вытеснения по 7-дневному простою и 10 ГБ-LRU кэш редко запускаемого workflow может просто исчезнуть, что правильное поведение, а не баг — кэш это оптимизация, обязанная терпеть промахи.

Викторина

Workflow использует key: deps-${{ hashFiles('package-lock.json') }} с restore-keys: deps-. PR поднимает одну зависимость, меняя lockfile. Что происходит на том запуске и что сохраняется?

Почему это работает

Почему записи кэша неизменяемы, а не перезаписываются при сохранении? Потому что иначе параллельные джобы гонялись бы за запись одного ключа, и читатель мог бы получить полузаписанный кэш, или две ветки могли бы затереть состояние зависимостей друг друга под общим ключом. Неизменяемость делает ключ стабильной идентичностью содержимого: один ключ всегда означает те же байты, поэтому попаданию можно доверять без перепроверки. Цена в том, что «обновить» кэш невозможно — ты делаешь новый ключ (что хэш содержимого делает за тебя автоматически при изменении входов) и даёшь LRU-вытеснению удалить старый. Вот почему хэш содержимого в ключе — не опциональный лоск; это то, что делает неизменяемую модель корректной, а не вечно устаревшей.

Размеры и гигиена

Кэши и артефакты оба стоят места и оба имеют лимиты, кусающие на масштабе. Одна запись кэша должна быть каталогом зависимостей, а не всем workspace — кэширование node_modules плюс ~/.npm плюс выхода сборки утраивает размер и толкает к 10 ГБ-LRU-обрыву быстрее, вытесняя другие полезные кэши. Артефакты по умолчанию хранятся 90 дней, что для per-PR выходов сборки расточительно; сократи retention-days до недели или меньше для эфемерных данных, а долгое хранение оставь релизам и аудит-логам. Сжимай перед загрузкой крупных артефактов и вообще не загружай node_modules как артефакт — для этого есть кэш. Дисциплина — сопоставлять каждое хранилище его задаче: долговечная передача → артефакт с намеренным хранением; оптимизация скорости с терпимыми промахами → кэш с ключом, хэширующим содержимое.

Викторина

Команда кэширует выход сборки (финальный скомпилированный бинарь) вместо загрузки его как артефакт, чтобы «сэкономить время на скачивании». Почему это неправильное хранилище и что ломается?

Вспомните перед уходом
  1. 01
    Почему ключ кэша должен включать хэш содержимого и что restore-keys добавляет поверх него?
  2. 02
    Противопоставь артефакты и кэш по долговечности и применению и назови числа вытеснения и хранения.
Итог

Артефакты и кэш оба сохраняют файлы, но отвечают на противоположные нужды. Артефакты — долговечный канал: они выносят выходы сборки, отчёты, coverage и логи из workflow человеку или нижестоящему джобу, и поскольку каждый джоб выполняется на свежем раннере, они единственный способ двигать файлы между джобами — хранятся 90 дней по умолчанию, что стоит сокращать через retention-days для эфемерных per-PR данных, а долгое оставлять релизам и аудит-следам. Кэш — best-effort ускорение повторной работы вроде каталогов зависимостей и промежуточных результатов сборки: записи ключуются и неизменяемы после записи, и они явно вытесняемы — GitHub удаляет кэши, нетронутые 7 дней, и, за 10 ГБ суммарного кэша репозитория, вытесняет least-recently-used записи, так что промах — корректное поведение, которое дизайн обязан терпеть. Ключ кэша несёт всё бремя корректности: включи хэш содержимого вроде hashFiles('**/package-lock.json'), чтобы ключ менялся ровно при изменении входов — бесхэшевый константный ключ либо служит устаревшим кэшем вечно, либо, при неизменяемости, замораживает то, что сохранено первым — и добавь префиксы restore-keys, чтобы свежий промах точного ключа всё равно восстановил предыдущий кэш как тёплое частичное попадание, ставя только дельту перед сохранением под новым ключом. Сопоставляй каждое хранилище его задаче: долговечную передачу, которую нельзя терять, — в артефакт с намеренным хранением; медленно-пересчитываемые, редко-меняющиеся входы — в кэш по хэшу содержимого, для которого промахи бесплатны. Кэшируй каталог зависимостей, а не весь workspace, чтобы держаться вдали от 10 ГБ-LRU-обрыва, вытесняющего твои другие полезные кэши. Теперь, когда встретишь запуск CI, переустанавливающий всё каждый раз, — первым делом проверяй ключ: константная строка или хэш не того файла виноваты почти всегда, а фикс — один вызов hashFiles().

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 5 завершено

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.