Реклама
Перетяжка, 07.10, ОТП банк

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

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

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

Знакомая ситуация: вы поправили баг, задеплоили, обновили страницу — а там всё то же самое. Проверяете код в репо, смотрите логи пайплайна, коммит точно смёржен и ушёл в прод. А пользователи как писали, что у них старая версия, так и пишут. Первая мысль — «наверное, что-то не задеплоилось». На деле почти всегда виноват кэш, причём не один: сразу несколько, каждый на своём уровне и со своей логикой обновления.

Кто вообще кэширует ваш сайт

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

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

Каждый уровень обновляется по своим правилам и не знает о существовании соседних. Поэтому «обновил — не работает» может означать три совершенно разные проблемы, и решать их нужно по отдельности.

Почему браузер не видит обновление

Браузер решает, использовать ли сохранённую копию файла или запросить новую, по заголовку Cache-Control, который сервер прислал вместе с этим файлом в прошлый раз. Директив несколько, и путаница вокруг них — обычное дело: no-cache не значит «не кэшировать», это значит «кэшировать, но каждый раз ревалидировать перед показом»; за настоящее «не хранить вообще» отвечает no-store. Если сервер прислал max-age=86400, браузер имеет полное право сутки ничего не спрашивать — то есть сутки отдавать сохранённую копию, даже не обращаясь к серверу.

Ревалидация тоже работает не вслепую. Для проверки актуальности файла сервер присылает ETag, то есть идентификатор его версии. При следующем запросе браузер подставляет его в заголовок If-None-Match. Если файл не изменился, сервер отвечает 304 Not Modified без тела ответа. Браузер использует сохранённую версию, поэтому трафик на повторную загрузку не тратится.

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

Почему CDN отдаёт старую версию, даже когда ориджин уже обновлён

CDN — ещё один слой кэша: его копии лежат на edge-серверах между вашим сервером и браузером пользователя, физически ближе к тому, кто делает запрос. Edge-серверы CDN решают, отдать сохранённую версию или сходить на origin, по ключу кэша. Обычно это URL плюс набор заголовков, которые указаны в Vary, указывающем, какие варианты ответа нужно различать. Директива Cache-Control: s-maxage работает именно для таких shared-кэшей и имеет приоритет над max-age в CDN, хотя браузер её игнорирует.

Здесь и кроется вторая причина «обновил — не работает»: вы поменяли файл на origin, но edge-серверы CDN узнают об этом, только когда истечёт TTL их собственной копии — или когда вы явно скажете им сбросить кэш через purge. Purge — это команда, отличная от кэш-бастинга на стороне браузера. Purge удаляет сохранённый файл, поэтому при следующем запросе CDN обращается к origin. У крупных провайдеров эта операция быстрая: и Cloudflare, и Fastly заявляют медианное время распространения purge по всей сети менее 150 мс. Но всё равно запуск очистки остается отдельным этапом, который обычно добавляют в CI/CD-пайплайн и выполняют после успешного деплоя.

А что кэширует сам сервер

Третий уровень — кэш внутри приложения: сохранённый результат тяжёлого запроса к базе, посчитанная агрегация, готовый рендер страницы. Здесь чаще всего используют отдельное хранилище вроде Redis, которое инвалидируется вручную или по TTL — так же, как и CDN, только этим занимается уже код приложения.

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

Почему нельзя просто выключить кэш

Соблазн понятен: раз кэш создаёт столько путаницы, почему бы не выставить везде no-store и не жить спокойно? Дело в том, что кэш — основа современной веб-инфраструктуры: без него задержки и нагрузка на серверы выросли бы на порядок.

Без кэша браузера каждая загрузка страницы заново скачивает все статические файлы: шрифты, картинки, JS-бандлы — даже если они не менялись между визитами. Без кэша CDN каждый запрос долетает до origin-сервера, который физически может находиться в другом регионе: значит, для пользователя это лишние сотни миллисекунд задержки на каждый клик, а для инфраструктуры — нагрузка на origin, снимать которую как раз и должен CDN. Без кэша на уровне приложения тяжёлый запрос к базе будет выполняться заново при каждом обращении, даже если результат не изменился ни на бит.

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

Как правильно обновлять то, что закэшировано

Рабочий подход строится на двух связанных приёмах.

Для файлов, которые меняются вместе с деплоем (CSS, JS, картинки), используют версионирование имени: хэш содержимого зашивается прямо в имя файла (app.a1b2c3.js вместо app.js). Новый файл — новое имя, значит, старая закэшированная копия просто не совпадает с URL, который запрашивает обновлённая страница, и конфликта не возникает. Для таких файлов можно смело ставить долгий max-age и директиву immutable: раз имя гарантированно уникально, ревалидация браузеру не нужна вовсе.

Для остального контента, который должен обновляться без смены URL (тексты, данные API, HTML-страницы), работает сочетание короткого max-age с stale-while-revalidate: пользователь получает мгновенный ответ из кэша, а в фоне кэш обновляется, и уже следующий запрос получит свежую версию. Для больших изменений добавляют явный purge на CDN и точечную инвалидацию нужных ключей в серверном кэше вместо полного сброса всего подряд.

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

Что проверить, если «обновил, но не работает»

  • Откройте страницу в режиме инкогнито или через Ctrl+Shift+R — если там всё новое, проблема была в браузерном кэше конкретного пользователя: на сервере уже всё актуально.
  • Посмотрите заголовки ответа (Cache-Control, Age, ETag) в инструментах разработчика — заголовок Age покажет, сколько секунд назад ответ был сгенерирован и отдаётся ли он вообще из кэша.
  • Проверьте, был ли выполнен purge на CDN после деплоя — если у вас настроен автоматический purge в CI/CD-пайплайне, убедитесь по логам, что он реально отработал: одного факта запуска недостаточно.
  • Проверьте кэш на уровне приложения — не остался ли в Redis или аналогичном хранилище старый ключ, который никто не инвалидировал.
  • И только после этого возвращайтесь к вопросу «а точно ли задеплоилось» — если все слои кэша чистые, а старая версия всё равно на месте, проблема, скорее всего, действительно в деплое.
Рекомендуем