8 lines
18 KiB
JSON
8 lines
18 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> и заметил странный результат: запрос с <code>Accept-Language: en</code> получил русский HTML. На origin уже была новая подпись, но публичный адрес иногда отдавал старую. Инженер сначала решил очистить CDN, затем сравнил ответы по слоям. Цена ошибки — неверный интерфейс и часы на спор между frontend, backend и CDN; если в ответе есть персональные данные, ошибка кэша становится утечкой.</p>\n<p>Этот разбор отвечает на один вопрос: как доказать, что два запроса с одним URL получают свои представления. Сначала проверяем, что именно создаёт origin (источник ответа), затем смотрим, как edge (пограничный кэш CDN) выбирает сохранённый объект, и только после этого проверяем свежесть через <code>ETag</code>. Очистка кэша может скрыть симптом, но не подтверждает правильность контракта.</p>\n<h2>Разбор ситуации: один URL, две версии</h2>\n<p>Представим публичный URL <code>https://www.example.test/catalog</code>. Тело ответа зависит от одного входа — <code>Accept-Language</code>. Для <code>ru</code> и <code>en</code> это два разных представления одного ресурса. Они могут иметь одинаковый путь, но кэш не должен считать их взаимозаменяемыми.</p>\n<p>Первое предположение дежурного — в CDN осталась старая запись. Проверка origin изменила картину: origin действительно различал языки, а edge возвращал один и тот же digest тела. В этот момент purge перестал быть диагностикой. Нужно было проверить, объявляет ли ответ зависимость через <code>Vary</code> и совпадает ли это правило с фактическим cache key провайдера.</p>\n<h2>Как кэш выбирает сохранённый ответ</h2>\n<p>Минимальный cache key включает метод и целевой URI. Для согласования содержимого кэш может хранить несколько ответов на один URI. Заголовок <code>Vary</code> сообщает, какие request headers исходного запроса участвуют в сопоставлении: если ответ содержит <code>Vary: Accept-Language</code>, значение этого поля должно совпасть с запросом, который кэш хочет обслужить без повторной проверки.</p>\n<p><code>Vary</code> — часть HTTP-контракта ответа. Настройка CDN может дополнительно включать в ключ cookie, query-параметр или нормализованное значение заголовка. Поэтому наличие <code>Vary</code> не доказывает, что конкретный edge настроен верно: нужно сравнить фактические тела и заголовки на origin и публичном адресе. Если тело не зависит от языка, добавлять <code>Vary</code> «на всякий случай» не следует — это создаст лишние варианты.</p>\n<p>Свежесть — отдельная координата. <code>max-age</code> задаёт срок свежести для обычного кэша, а <code>s-maxage</code> может задать отдельный срок для shared cache. Директива <code>private</code> ограничивает хранение ответа общим кэшем, а <code>no-store</code> запрещает его хранить. Ни одна из этих директив не выбирает язык и не исправляет неполный ключ.</p>\n<table><thead><tr><th scope='col'>Что наблюдаем</th><th scope='col'>Что это означает</th><th scope='col'>Что проверить</th></tr></thead><tbody><tr><td>Origin возвращает разные тела</td><td>Представление зависит от входа</td><td>Записать вход, digest тела и response headers</td></tr><tr><td>В ответе нет <code>Vary</code></td><td>Зависимость от request header не объявлена</td><td>Сверить фактические входы генератора</td></tr><tr><td>Origin различает, edge склеивает</td><td>Правило cache key не соответствует варианту</td><td>Сравнить холодные запросы и настройки CDN</td></tr><tr><td>Тело верное, но старое</td><td>Ответ ещё свежий или revalidation не работает</td><td>Проверить <code>Age</code>, TTL, <code>ETag</code> и <code>304</code></td></tr><tr><td>В ответе есть пользовательские данные</td><td>Shared cache может отдать объект другому пользователю</td><td>Проверить <code>Authorization</code>, cookie и <code>Cache-Control</code></td></tr></tbody></table>\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>Начните с безопасного тестового домена без персональных данных. Меняйте только язык, сохраняйте заголовки и тела отдельно. Не добавляйте одновременно cookie, User-Agent и экспериментальные параметры: иначе после этого нельзя будет сказать, какой вход изменил результат.</p>\n<pre><code>curl -sS -L -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 -L -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\ndiff -u /tmp/catalog-ru.html /tmp/catalog-en.html || true\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>Команды предназначены для стенда и здесь не выполнялись. Разные digest и осмысленный diff подтверждают, что origin сформировал разные тела. В обоих ответах ищите <code>Vary: Accept-Language</code>. Если origin уже отдаёт неверный язык, не начинайте с purge: исправьте выбор представления или передачу заголовка.</p>\n<figure><img src='/assets/editorial/2019/http-cache-variant-check-2019.svg' alt='Проверка вариантов HTTP-кэша через origin и CDN с разными языками и ETag' loading='lazy' /><figcaption>Сначала сравниваются два ответа origin, затем те же варианты на edge. Digest тела показывает склейку даже тогда, когда статус обоих запросов равен 200.</figcaption></figure>\n<h2>Поворот расследования: origin и edge расходятся</h2>\n<p>После проверки origin инженер повторяет те же запросы через публичный адрес. Статус <code>200</code> сам по себе ничего не доказывает: сервер мог вернуть HTML от другого варианта. Нужны тело, итоговый URL после redirect, <code>Content-Type</code>, <code>Cache-Control</code>, <code>Vary</code>, <code>ETag</code> и, если его добавляет shared cache, <code>Age</code>. Отсутствие <code>Age</code> не доказывает обращение к origin.</p>\n<pre><code>for lang in ru en; do\n curl -sS -L -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 отдаёт один digest, причина находится между ними: cache key, нормализация заголовка, proxy или правило провайдера. RFC описывает поведение HTTP-кэша, но конкретный CDN может иметь собственные настройки ключа; их нужно сверить с документацией провайдера и подтвердить запросами.</p>\n<p>Если публичный ответ содержит персональные данные, не пытайтесь лечить это добавлением всех cookie в ключ. Такой ключ может раздробить кэш и всё равно оставить ошибку в политике хранения. Для личного HTML безопаснее отказаться от shared cache и проверить директивы <code>private</code> или <code>no-store</code> согласно модели данных.</p>\n<h2>ETag и условный запрос</h2>\n<p><code>ETag</code> — непрозрачный валидатор выбранного представления. Он может различать версии ресурса во времени и одновременно существующие варианты content negotiation. При <code>If-None-Match</code> сервер сравнивает тег с выбранным представлением запроса; для GET при совпадении возвращается <code>304 Not Modified</code> без тела. Поэтому язык в условном запросе должен выбирать тот же вариант, для которого был снят тег.</p>\n<p>Сначала получите тег для одного языка, затем повторите запрос с тем же <code>Accept-Language</code>. После контролируемого изменения представления ожидайте новый тег и <code>200</code>. Случайно меняющийся тег ломает повторное использование, а неизменный тег после изменения позволяет отдать устаревшее представление. Сравнивать тег от <code>ru</code> с запросом <code>en</code> нельзя: это уже другая выбранная репрезентация, даже если URL совпадает.</p>\n<pre><code>curl -sS -L -D /tmp/catalog-ru-first.headers -o /tmp/catalog-ru-first.html \\\n -H \"Accept-Language: ru\" \\\n https://www.example.test/catalog\n\nETAG=$(awk 'BEGIN{IGNORECASE=1} /^etag:/ {sub(/^etag:[[:space:]]*/, \"\"); print; exit}' \\\n /tmp/catalog-ru-first.headers)\n\ncurl -sS -L -D - -o /dev/null \\\n -H \"Accept-Language: ru\" \\\n -H \"If-None-Match: $ETAG\" \\\n https://www.example.test/catalog</code></pre>\n<p>Ожидаемый <code>304</code> означает, что выбранное представление не изменилось и валидатор совпал; это не доказательство попадания в CDN, потому что условный запрос может обработать origin. Для проверки edge отдельно фиксируйте публичный URL и доступные признаки кэша. После изменения только русской версии повторите оба шага и убедитесь, что английский тег и тело не изменились случайно.</p>\n<h2>Порядок проверки</h2>\n<ol><li>Выберите тестовый URL без персональных данных и назовите единственный вход, который должен менять тело.</li><li>Снимите два ответа origin, меняя только <code>Accept-Language</code>; сохраните входы, заголовки, тела и digest.</li><li>Сравните тела. Если они различаются, проверьте, что каждый response header содержит согласованный <code>Vary</code>.</li><li>Повторите те же запросы через публичный edge и запишите итоговый URL, статус, заголовки и digest каждого тела.</li><li>Если ответы склеились, проверьте cache key, нормализацию заголовка и правило провайдера; не начинайте с полной очистки кэша.</li><li>Для одного языка снимите <code>ETag</code> и выполните условный GET с тем же языком и <code>If-None-Match</code>.</li><li>Измените только выбранное представление и повторите запрос: ожидайте новый тег и <code>200</code> с новым телом, если сервер применяет этот контракт.</li><li>Запишите команды и ожидаемые признаки в runbook или автоматическую проверку, а затем удалите тестовые персональные данные.</li></ol>\n<h2>Ограничения и критерий готовности</h2>\n<p>Один <code>Vary</code> не описывает всю конфигурацию CDN. Тело может зависеть от cookie, <code>Authorization</code>, географии, устройства, query-параметра или эксперимента. Каждый реальный вход нужно включить в модель варианта либо исключить shared caching. Redirect, CORS, service worker и серверная нормализация могут изменить наблюдаемую загрузку, поэтому проверяйте итоговый ответ.</p>\n<p>Сценарий не сообщает production-результаты: домены, языки, теги и digest выше учебные. Работа готова, когда origin отдаёт ожидаемые тела, заголовки объявляют реальные входы, edge не смешивает варианты, а условный запрос проверен для того же выбранного представления. Вернувшись к утренней сцене, инженер оставляет purge аварийным действием, а доказательством считает повторяемую пару запросов и зафиксированный результат.</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> — состав cache key, сопоставление вариантов через <code>Vary</code>, свежесть, validation и правила shared cache.</li><li><a href='https://www.rfc-editor.org/rfc/rfc9110.html' target='_blank' rel='noopener'>RFC 9110: HTTP Semantics</a> — selected representation, <code>ETag</code>, <code>If-None-Match</code>, weak comparison и ответ <code>304 Not Modified</code>.</li></ul>"
|
||
}
|