DarkRiDDeR12 мин

CDN отдаёт старый язык: как проверить cache key, Vary и ETag

HTTPCDNДиагностика

На тестовом домене карточка по адресу /catalog должна отвечать на русском и английском в зависимости от Accept-Language. После включения общего кэша часть английских запросов получает русский HTML, хотя origin формирует правильный язык. Цена ошибки — не косметика: пользователь видит чужой интерфейс, а команда может ошибочно списать проблему на перевод или браузер вместо того, чтобы проверить ключ сохранённого ответа.

Ниже полевой сценарий для одной ветки: один URL, два языка, доступ к безопасному тестовому origin и к публичному адресу через CDN. Он не предполагает, что любой CDN автоматически уважает каждый Vary. Сначала докажем, что origin различает варианты и объявляет это в HTTP, затем проверим тот же договор на публичном пути. Никаких личных cookies и настоящих пользовательских страниц в команду не подставляем.

Определяем, что считается разным представлением

Один URL может иметь несколько представлений. В нашем примере тело меняется из-за Accept-Language: меняются заголовок, подписи и ссылка на локализованный asset. Это не вопрос вкуса, а вход генерации ответа. RFC описывает Vary как список полей запроса, которые повлияли на выбранное представление. Поэтому origin обязан не только вернуть русский текст, но и объявить кэшу, что язык участвовал в выборе.

Не добавляйте Vary: Cookie по инерции. Если cookie содержит идентификатор сессии, такой ключ может раздробить общий кэш на множество значений и всё равно не решить персональные данные. Сначала ответьте, почему тело меняется. Для личной страницы правильный маршрут часто начинается с private, а для публичной локализации — с ограниченного и проверяемого Vary: Accept-Language.

СценарийОжидаемый заголовокЧто считаем ошибкойСледующее действие
Публичный русский и английский HTMLVary: Accept-LanguageТела различаются, а Vary отсутствует или не совпадаетИсправить origin и повторить два запроса
Одинаковый ответ независимо от языкаVary не требуется только ради предположенияДобавлен лишний Vary без отличий телаУбрать лишний вариант и измерить ключ заново
Страница с пользователемprivate или более строгий запрет храненияОбщий кэш способен использовать ответ другой сессииОтделить публичный shell от личных данных
Вариант устарел после срокаETag и условный запрос при выбранном договореСервер всегда отдаёт тело, хотя представление не менялосьПроверить генерацию валидатора и путь до origin

Собираем контрольный запрос к origin

Начинаем не с интерфейса, а с заголовков. Два запроса должны отличаться ровно одним входом — языком. Сохраняем заголовки и тело раздельно, чтобы не перепутать вывод curl с данными. В боевом окружении тестовый origin должен быть доступен безопасным способом: отдельный host, allowlist или стенд. Не обходите аутентификацию и не добавляйте служебные адреса в публичные примеры.

# Русский вариант.
curl -sS -D /tmp/catalog-ru.headers -o /tmp/catalog-ru.html -H "Accept-Language: ru" https://origin.example.test/catalog

# Английский вариант: меняется только один вход.
curl -sS -D /tmp/catalog-en.headers -o /tmp/catalog-en.html -H "Accept-Language: en" https://origin.example.test/catalog

grep -Ei "^(cache-control|vary|etag|last-modified):" /tmp/catalog-ru.headers
grep -Ei "^(cache-control|vary|etag|last-modified):" /tmp/catalog-en.headers

Ожидаем не конкретный текст ETag, а форму договора. В обоих ответах должно быть одинаковое правило кэширования, если срок свежести одинаков. Vary должен перечислять Accept-Language, если тела различаются по этому полю. ETag может различаться, потому что представления различаются. Если origin уже возвращает неверные заголовки, CDN пока не трогаем: сначала исправляем источник, иначе edge-диагностика будет смешивать две ошибки.

Вертикальная схема проверки двух языковых вариантов: origin формирует русский и английский ответы с Vary, CDN хранит отдельные ключи, затем условный запрос с ETag подтверждает или обновляет вариант.
Один URL не равен одному телу. В сценарии решающим входом является язык, поэтому его надо увидеть в Vary и в поведении реального кэша.

Повторяем сценарий через публичный путь

Когда origin прошёл проверку, повторяем те же два запроса через публичный адрес. Менять одновременно URL, язык, User-Agent и cookie нельзя: тогда результат невозможно объяснить. Сначала сравниваем headers с origin: они могут дополняться, но Cache-Control, Vary и валидатор не должны потерять смысл. Затем сравниваем тела или их безопасные хеши. Если публичный путь отдаёт один вариант на два языка, фиксируем это как расхождение cache key, а не как «кэш иногда глючит».

for lang in ru en; do
  curl -sS -D "/tmp/edge-$lang.headers" -o "/tmp/edge-$lang.html" -H "Accept-Language: $lang" https://www.example.test/catalog
done

sha256sum /tmp/edge-ru.html /tmp/edge-en.html
grep -Ei "^(cache-control|vary|etag|age):" /tmp/edge-ru.headers
grep -Ei "^(cache-control|vary|etag|age):" /tmp/edge-en.headers

Фрагмент предназначен для shell с безопасным доменом; он не является выполненным измерением этой статьи. Важна последовательность: сначала подтверждаем два варианта, потом смотрим, что edge не склеил их в один. Поле Age, если его отдаёт слой, помогает понять, что ответ уже жил в кэше, но его отсутствие не доказывает отсутствие кэширования. Конкретные диагностические заголовки CDN не универсальны и должны быть описаны его документацией.

Проверяем повторную проверку после истечения свежести

Второй тип сбоя выглядит иначе: варианты различаются правильно, но после изменения перевода один из них долго остаётся старым. Здесь проверяем валидатор. Сохраняем ETag русского варианта, ждём или на тестовом стенде настраиваем короткий срок свежести, затем посылаем If-None-Match для того же языка. Если тело не менялось, допустим 304; если менялось — ожидаем новый 200 и новый ETag. Нельзя проверять русский валидатор английским запросом: это уже другой вариант.

# Значение взять из ответа русского варианта, не подставлять личные токены.
curl -sS -D - -o /dev/null -H "Accept-Language: ru" -H 'If-None-Match: "catalog-ru-v18"' https://www.example.test/catalog

Если ответ всегда 200, это не повод отключить кэш. Сначала выясняем, меняется ли ETag на каждом запросе из-за времени, случайного идентификатора или неустойчивого порядка данных. Валидатор должен описывать представление, а не шум вокруг него. Если сервер всегда 304 после реального изменения текста, наоборот, валидатор слишком грубый. Оба случая проверяются на маленьком контролируемом изменении, а не на общей очистке всей зоны.

Матрица решения по наблюдению

Результат сценария удобно зафиксировать как четыре короткие строки: вариант запроса, URL, digest тела и заголовки. В локализации это даёт картину, которую можно показать владельцу CDN или backend: вот два запроса, вот origin, вот edge, вот поле, исчезнувшее на переходе. Такой артефакт ценнее скриншота, потому что его можно повторить после правки правила.

Наблюдение после двух запросовГраница, где искатьБезопасная следующая проверка
Origin возвращает один и тот же языкШаблон/роутинг originПроверить, доходит ли Accept-Language до приложения
Origin различает, но не ставит VaryHTTP-ответ приложения или proxyДобавить Vary для реального входа и снова снять заголовки
Origin корректен, edge склеивает вариантыНастройка CDN/cache keyСверить правило провайдера с входом языка на тестовом URL
Варианты разделены, но новый текст не приходит после срокаETag, TTL или маршрут условного запросаПроверить один вариант с If-None-Match

Порядок внедрения

  1. Выберите публичный тестовый URL без личных данных и один вход, который точно меняет тело.
  2. Снимите два ответа origin, сохранив тело и ключевые заголовки по отдельности.
  3. Проверьте соответствие между различием тела и Vary; не расширяйте key произвольными полями.
  4. Повторите сценарий через CDN с теми же двумя запросами и зафиксируйте расхождение.
  5. Проверьте один язык условным запросом после выбранного срока свежести.
  6. После правки оставьте команды и ожидаемые признаки в репозитории или runbook, чтобы следующий релиз не начинал диагностику заново.

Ограничения и следующий шаг

Сценарий не проверяет все возможные варианты: мобильный рендер, эксперимент, гео и авторизацию нужно добавлять отдельно только если они действительно меняют тело. Он также не разрешает кешировать персональный HTML. Его задача уже: показать, что правильный перевод на origin не равен правильному cache key на edge.

После этой проверки можно переходить к настройке TTL и purge-процесса конкретного провайдера, но только с зафиксированным ключом. Если команда не может назвать, какие поля запроса создают разные представления, сначала вернитесь к шаблону и данным. Для автора 2019 года это развитие от ручного curl-диагноза к границе между HTTP-договором и инфраструктурой доставки, без притворной уверенности, что один заголовок управляет всей сетью.

Короткий вывод

  • Два языка по одному URL требуют проверяемого различия вариантов, а не только двух правильных HTML на origin.
  • Vary отражает входы, которые изменили представление; он не заменяет проверку реальной настройки CDN.
  • ETag проверяется внутри одного варианта запроса, иначе 304 и 200 нельзя интерпретировать.
  • Небольшой повторяемый сценарий с headers и телом быстрее локализует сбой, чем очистка всего кэша.

Проверяемые источники

  • RFC 7234, раздел 5.2: Cache-Control — семантика max-age, s-maxage, private, no-cache и no-store для HTTP/1.1-кэшей
  • RFC 7234, раздел 4.3: validation — условные запросы, ETag, If-None-Match и ответ 304 как повторная проверка сохранённого представления
  • RFC 7232, раздел 2.3: ETag — синтаксис entity-tag и связь валидатора с конкретным представлением ресурса
  • RFC 7234, раздел 4.1: Vary — как кэш выбирает подходящую сохранённую вариацию ответа по полям запроса
  • MDN: HTTP caching — практическое объяснение свежести, повторной проверки, ETag и разницы между no-cache и no-store
  • MDN: Vary header — Vary как перечисление полей запроса, которые повлияли на представление ресурса