8 lines
16 KiB
JSON
8 lines
16 KiB
JSON
{
|
||
"index": 310,
|
||
"slug": "editorial-2019-05-field-http-caching",
|
||
"title": "Один URL, два ответа: как не склеить варианты в HTTP-кэше",
|
||
"excerpt": "После включения CDN пользователь получает не тот язык или старую версию страницы. Разбираем cache key, Vary и ETag на контролируемом примере и проверяем результат через origin и edge.",
|
||
"contentHtml": "<p>После деплоя <code>/catalog</code> должен отдавать английский HTML по запросу с <code>Accept-Language: en</code>. Но часть пользователей видит русский текст. Иногда проблема выглядит мягче: новая подпись уже есть на origin, а публичный адрес ещё отдаёт старую. Цена ошибки — неверный интерфейс, потерянное доверие и часы на спор между frontend, backend и CDN. Если в ответе есть персональные данные, ошибка кэша становится утечкой.</p>\n<p>Тезис простой: кэш хранит не «URL вообще», а представление ответа для конкретного запроса. Чтобы использовать сохранённый ответ безопасно, нужно согласовать четыре вещи: какие входы меняют тело, как они попадают в cache key, сколько ответ считается свежим и как сервер подтверждает его версию. Один правильный заголовок не исправляет расхождение между приложением и edge.</p>\n<h2>Механизм: представление, вариант и свежесть</h2>\n<p>В учебном примере тело <code>/catalog</code> зависит только от <code>Accept-Language</code>. Русский и английский ответы имеют один URL, но являются разными представлениями. Origin должен сообщить это через <code>Vary: Accept-Language</code>. Кэш использует указанные поля запроса, чтобы отличить сохранённые варианты. Если поле не указано, edge может считать русский и английский ответы одним объектом.</p>\n<p><code>Cache-Control</code> задаёт правила хранения и повторного использования. <code>max-age</code> ограничивает свежесть ответа для обычного кэша. <code>s-maxage</code> позволяет отдельно задать срок для shared cache, если это нужно архитектуре. <code>private</code> запрещает общий кэш для ответа, который нельзя отдавать другой сессии. <code>no-store</code> запрещает хранение, но не превращает любой публичный ответ в персональный.</p>\n<p>После истечения свежести кэш может не получать всё тело заново. Он отправляет условный запрос с валидатором. Для этого сервер возвращает <code>ETag</code>, а клиент или кэш повторяет его в <code>If-None-Match</code>. Если представление не изменилось, сервер отвечает <code>304 Not Modified</code>. Если изменилось, он возвращает новое тело и новый валидатор. Проверяйте ETag внутри одного варианта: русский ETag нельзя интерпретировать запросом без того же языка.</p>\n<pre><code>GET /catalog HTTP/1.1\nHost: www.example.test\nAccept-Language: en\n\nHTTP/1.1 200 OK\nCache-Control: public, max-age=60\nVary: Accept-Language\nETag: \"catalog-en-v18\"\n\n<h1>Catalog</h1></code></pre>\n<p>Это учебный обмен. Значение <code>ETag</code> придумано для примера и не является результатом измерения. В реальном сервисе валидатор должен меняться вместе с выбранным представлением, а не с каждым запросом из-за случайного идентификатора.</p>\n<h2>Сначала докажите различие на origin</h2>\n<p>Начните с безопасного тестового домена. Снимите два ответа, изменив только язык. Сохраните заголовки и тела отдельно. Не добавляйте cookies, User-Agent и экспериментальные параметры одновременно: иначе нельзя понять, какой вход изменил результат.</p>\n<pre><code>curl -sS -D /tmp/catalog-ru.headers \\\n -o /tmp/catalog-ru.html \\\n -H \"Accept-Language: ru\" \\\n https://origin.example.test/catalog\n\ncurl -sS -D /tmp/catalog-en.headers \\\n -o /tmp/catalog-en.html \\\n -H \"Accept-Language: en\" \\\n https://origin.example.test/catalog\n\nsha256sum /tmp/catalog-ru.html /tmp/catalog-en.html\ngrep -Ei \"^(cache-control|vary|etag|last-modified):\" /tmp/catalog-ru.headers\ngrep -Ei \"^(cache-control|vary|etag|last-modified):\" /tmp/catalog-en.headers</code></pre>\n<p>Команды предназначены для стенда и не выполнены в рамках этой статьи. На origin ожидайте разные тела, если язык действительно влияет на HTML. При этом <code>Vary</code> должен назвать <code>Accept-Language</code>. Если тела одинаковые, не добавляйте <code>Vary</code> по догадке: лишний вариант уменьшит эффективность кэша. Если origin уже отдаёт неверный язык, не начинайте с purge CDN. Исправьте выбор представления или передачу заголовка.</p>\n<figure><img src=\"/assets/editorial/2019/http-cache-variant-check-2019.svg\" alt=\"Проверка русской и английской версии через origin, CDN и условный запрос с ETag\" /><figcaption>Один URL может иметь несколько представлений. Язык должен участвовать в проверяемом договоре кэширования.</figcaption></figure>\n<h2>Потом сравните публичный путь</h2>\n<p>Повторите те же запросы через CDN. Сравните статус, тело и ключевые заголовки. Edge может добавлять служебные поля, например <code>Age</code>, но не должен терять смысл <code>Vary</code>, срока свежести и валидатора. Отсутствие <code>Age</code> само по себе не доказывает, что origin был вызван.</p>\n<pre><code>for lang in ru en; do\n curl -sS -D \"/tmp/edge-$lang.headers\" \\\n -o \"/tmp/edge-$lang.html\" \\\n -H \"Accept-Language: $lang\" \\\n https://www.example.test/catalog\ndone\n\nsha256sum /tmp/edge-ru.html /tmp/edge-en.html\ngrep -Ei \"^(cache-control|vary|etag|age):\" /tmp/edge-ru.headers\ngrep -Ei \"^(cache-control|vary|etag|age):\" /tmp/edge-en.headers</code></pre>\n<p>Если origin различает ответы, а edge отдаёт одинаковое тело, ищите расхождение в настройке cache key или в том, как провайдер обрабатывает <code>Vary</code>. Не каждый CDN одинаково строит ключ по заголовкам. Документация провайдера важна, но её нужно подтвердить двумя реальными запросами на тестовом URL.</p>\n<h2>Симптом → причина → проверка → действие</h2>\n<div class=\"table-scroll\"><table><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Причина</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>Origin отдаёт один язык</td><td>Приложение не использует вход или прокси его теряет</td><td>Два запроса к origin, тела и входящие заголовки</td><td>Исправить маршрутизацию или генерацию; CDN не менять</td></tr><tr><td>Origin различает, но <code>Vary</code> отсутствует</td><td>HTTP-договор не описывает зависимость</td><td>Сравнить тела и response headers</td><td>Добавить реальный вход в <code>Vary</code>; повторить тест</td></tr><tr><td>Origin корректен, edge склеивает варианты</td><td>Cache key не учитывает язык или правило провайдера не применилось</td><td>Одинаковые запросы через публичный URL, digest тел</td><td>Исправить правило CDN и проверить два холодных варианта</td></tr><tr><td>Новая версия приходит только после purge</td><td>Слишком длинный TTL или неработает revalidation</td><td>Тот же язык, ETag и <code>If-None-Match</code> после выбранного срока</td><td>Настроить TTL и валидатор; purge оставить аварийным инструментом</td></tr><tr><td>Личная страница попадает в общий кэш</td><td>Ответ помечен как публичный или ключ неполон</td><td>Проверить cookies, Authorization и <code>Cache-Control</code></td><td>Разделить публичный shell и данные либо использовать <code>private</code>/<code>no-store</code></td></tr></tbody></table></div>\n<h2>Проверяем ETag без ложного вывода</h2>\n<p>Выберите один язык и короткий срок на тестовом стенде. Сначала получите ETag. Затем отправьте условный запрос с тем же <code>Accept-Language</code> и этим ETag.</p>\n<pre><code>curl -sS -D - -o /dev/null \\\n -H \"Accept-Language: ru\" \\\n -H 'If-None-Match: \"catalog-ru-v18\"' \\\n https://www.example.test/catalog</code></pre>\n<p>Если представление не менялось и валидатор совпадает, ожидайте <code>304</code>. После контролируемого изменения текста ожидайте <code>200</code> с новым телом и новым ETag. Если сервер всегда возвращает <code>200</code>, проверьте, не меняется ли ETag из-за времени или случайного поля. Если сервер возвращает <code>304</code> после изменения, валидатор слишком грубый. Это учебные критерии; конкретные статусы нужно подтвердить на вашем стенде.</p>\n<h2>Порядок действий</h2>\n<ol><li>Выберите публичный тестовый URL без персональных данных и назовите единственный вход, который должен менять тело.</li><li>Снимите два ответа origin: сохраните заголовки, тела и значения входов отдельно.</li><li>Сравните тела. Если они различаются, проверьте <code>Vary</code> и не расширяйте ключ случайными полями.</li><li>Повторите те же два запроса через CDN. Зафиксируйте digest тел, <code>Cache-Control</code>, <code>Vary</code>, <code>ETag</code> и доступные служебные поля.</li><li>Для одного варианта выполните условный запрос с <code>If-None-Match</code> после выбранного срока свежести.</li><li>Исправьте слой, на котором появилось расхождение: приложение, proxy или cache key. Не начинайте с полной очистки кэша.</li><li>Повторите положительный и отрицательный сценарии: правильный язык и попытку получить другой язык из того же URL.</li><li>Оставьте команды и ожидаемые признаки в runbook или автоматической проверке.</li></ol>\n<h2>Ограничения</h2>\n<p>Один заголовок не описывает всю конфигурацию CDN. Провайдер может иметь собственные правила нормализации, дополнительные части ключа и отдельные механизмы purge. Их нужно проверять по документации и на вашем тестовом URL.</p>\n<p>Если тело зависит от cookie, Authorization, географии, устройства или эксперимента, добавьте каждый реальный вариант в модель. Не превращайте <code>Vary: Cookie</code> в универсальный ремонт: он может раздробить кэш и не решить задачу персональных данных. Для личного HTML безопаснее отказаться от shared cache.</p>\n<p>Сценарий не доказывает production-результаты и не заменяет нагрузочные тесты. Учебные домены, ETag и digest выше нужны только для воспроизводимой проверки механизма. Переход к production требует согласованного TTL, политики очистки, наблюдаемости и владельца каждого правила.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Работа готова, когда два запроса к origin дают ожидаемые представления, response headers объявляют реальные входы через <code>Vary</code>, а два запроса через CDN возвращают соответствующие тела, а не первый сохранённый вариант. Для одного языка условный запрос возвращает <code>304</code> без изменения представления и <code>200</code> с новым ETag после контролируемого изменения. Персональный ответ не доступен через общий кэш. Все четыре результата записаны в повторяемую проверку.</p>\n<h2>Проверяемые источники</h2><ul><li><a href=\"https://www.rfc-editor.org/rfc/rfc9111.html\" target=\"_blank\" rel=\"noopener\">RFC 9111: HTTP Caching</a> — правила свежести, хранения и расчёта вариантов через <code>Vary</code>.</li><li><a href=\"https://www.rfc-editor.org/rfc/rfc9110.html\" target=\"_blank\" rel=\"noopener\">RFC 9110: HTTP Semantics</a> — семантика представлений, <code>ETag</code>, условных запросов и <code>Vary</code>.</li></ul>"
|
||
}
|