Files

8 lines
18 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"index": 311,
"slug": "editorial-2019-05-mechanism-http-caching",
"title": "HTTP-кэш: почему правильный TTL не спасает ответ",
"excerpt": "Кэш проверяет не только срок свежести. Разбираем cache key, Vary, ETag и границы браузера, proxy и CDN на воспроизводимом сценарии.",
"contentHtml": "<p>После релиза пользователь открывает знакомый адрес и видит старый JavaScript или HTML не на том языке. В ответе есть <code>Cache-Control: max-age=60</code>, поэтому команда ждёт обновления через минуту. Но один клиент получает новый файл, другой — старый, а CDN продолжает отвечать из своей записи. Цена ошибки — неверное состояние страницы, лишняя нагрузка на origin и попытки очищать кэш вслепую.</p>\n<p>Причина часто не в числе 60. Кэш сначала решает, подходит ли сохранённый ответ этому запросу, и только потом проверяет его свежесть. Если ключ варианта неполон, свежий ответ всё равно будет неправильным. Если ключ верен, но ответ устарел, помогает валидатор вроде <code>ETag</code>. Эти две задачи нельзя заменять одним большим TTL.</p>\n<h2>Модель: ключ, свежесть и проверка</h2>\n<p>В HTTP cache key как минимум включает метод и target URI запроса. Для обычных GET-кэшей на практике главным компонентом остаётся URI. Но origin может выбирать представление по заголовкам запроса, например по <code>Accept-Language</code> или <code>Accept-Encoding</code>. Тогда ответ должен сообщить об этом через <code>Vary</code>: кэш сравнивает названные поля текущего запроса с полями запроса, который породил сохранённый ответ.</p>\n<p>Управляемый CDN может добавить к ключу собственные признаки: cookies, географию, эксперимент или правило из панели. Это не отменяет HTTP-контракт. Заголовок, снятый на origin, не доказывает поведение публичного адреса: proxy или CDN может выбрать другую запись, изменить свежесть или отправить запрос дальше. Название настройки вроде «cache key» тоже не доказывает, какие поля реально участвуют в выборе.</p>\n<p><code>max-age</code> задаёт срок, после которого ответ считают несвежим. Это не срок жизни записи и не команда удалить её. Несвежая запись может быть повторно проверена у origin. При совпавшем валидаторе origin для GET или HEAD обычно возвращает <code>304 Not Modified</code> без нового тела; при изменении представления — <code>200</code> с новым телом и новым валидатором.</p>\n<h2>Что именно делают директивы</h2>\n<p><code>ETag</code> сравнивает сохранённое представление с текущим. Он не очищает запись и сам по себе не делает её свежей. <code>no-cache</code> в ответе тоже не означает «не хранить»: перед повторным использованием кэш должен обратиться к origin для успешной проверки. <code>no-store</code> запрещает кэшу намеренно хранить ответ и использовать его для другого запроса, но не является полноценной защитой от скомпрометированного посредника.</p>\n<p><code>private</code> запрещает хранение ответа в shared cache, но разрешает private cache пользователя при остальных условиях. Это подходит для персонализированного ответа, если его всё равно допустимо хранить в браузере. Для личного HTML часто безопаснее выбрать <code>no-store</code>. Для shared cache отдельный <code>s-maxage</code> задаёт максимальный возраст и переопределяет <code>max-age</code> для общего кэша; после истечения срок всё равно требует корректного правила повторной проверки.</p>\n<p>Из этого следует порядок решения: сначала совпадение URI, метода и варианта; затем допустимость повторного использования; затем свежесть; затем, если нужно, условный запрос к origin. Нельзя выводить причину по одному заголовку. <code>Age</code> полезен как признак того, что ответ был сгенерирован или проверен не для этого запроса, но отсутствие <code>Age</code> не доказывает обращение к origin.</p>\n<h2>Учебный пример: язык меняет ответ</h2>\n<p>Ниже приведён учебный сценарий для контролируемого домена. Он меняет только <code>Accept-Language</code>, сохраняет заголовки и тело отдельно и не выдаёт результат за измерение production. Подставьте URL тестового origin и не используйте в команде настоящие cookies или токены.</p>\n<pre><code># Origin должен вернуть разные представления для двух языков.\ncurl -sS -D /tmp/catalog-ru.headers -o /tmp/catalog-ru.html -H 'Accept-Language: ru' https://origin.example.test/catalog\ncurl -sS -D /tmp/catalog-en.headers -o /tmp/catalog-en.html -H 'Accept-Language: en' https://origin.example.test/catalog\n\n# Сначала сравниваем контракт, затем тела.\ngrep -Ei '^(cache-control|vary|etag|age|date):' /tmp/catalog-ru.headers\ngrep -Ei '^(cache-control|vary|etag|age|date):' /tmp/catalog-en.headers\nsha256sum /tmp/catalog-ru.html /tmp/catalog-en.html\n\n# Валидатор берём из ответа того же варианта.\ncurl -sS -D - -o /dev/null -H 'Accept-Language: ru' -H 'If-None-Match: &quot;catalog-ru-v18&quot;' https://origin.example.test/catalog</code></pre>\n<p>Если origin действительно выбирает HTML по языку, для общего HTTP-кэша ожидается <code>Vary: Accept-Language</code>. Отдельное vendor-specific правило cache key может быть нужно CDN, но оно не заменяет заголовок для других посредников. Если origin отдал одинаковые тела, сначала исправьте выбор представления у источника: проверка CDN только замаскирует проблему.</p>\n<p>Значение <code>ETag</code> в команде условное. В рабочем запуске его нужно скопировать из предыдущего ответа для русского варианта, сохранив кавычки. Нельзя использовать русский валидатор для английского. Если представление не менялось и запрос дошёл до origin, ожидаем <code>304</code>; при изменении тела ожидаем новый <code>200</code>. Статус нужно зафиксировать на вашем стенде, а не обещать заранее.</p>\n<h2>Симптомы и действия</h2>\n<div class=\"table-scroll\"><table><caption>Диагностика HTTP-кэша по наблюдаемому симптому</caption><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Гипотеза</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>После релиза старый JavaScript</td><td>Постоянный URL и длинный TTL</td><td>Сравнить URL из нового HTML и digest файла</td><td>Добавить fingerprint или изменить контракт свежести</td></tr><tr><td>Английский запрос получает русский HTML</td><td>Нет <code>Vary</code> или CDN не учитывает вариант</td><td>Сделать два запроса, сравнить тела и заголовки на origin и public URL</td><td>Согласовать выбор представления, <code>Vary</code> и cache key</td></tr><tr><td>После TTL всегда приходит полный ответ</td><td>Нет валидатора или условный запрос не доходит до origin</td><td>Повторить запрос с прежним <code>ETag</code> и проверить маршрут</td><td>Настроить устойчивый валидатор и проверить каждый слой</td></tr><tr><td>Публичный ответ отличается от origin</td><td>Proxy или CDN меняет ключ, TTL или заголовки</td><td>Снять одинаковые поля и digest через обе точки</td><td>Сверить конфигурацию посредника с HTTP-контрактом</td></tr><tr><td>Данные одного пользователя видит другой</td><td>Персональный ответ попал в shared cache</td><td>Проверить две тестовые сессии, <code>Authorization</code>, cookies и директивы</td><td>Выбрать <code>private</code>, <code>no-store</code> или разделить публичную и личную части</td></tr><tr><td>CDN обслуживает запись дольше браузера</td><td>Используется <code>s-maxage</code>, отличный от <code>max-age</code></td><td>Сравнить <code>Age</code>, <code>Cache-Control</code> и vendor headers</td><td>Зафиксировать отдельную политику shared cache и критерий purge</td></tr></tbody></table></div>\n<h2>Иллюстрация ключа и свежести</h2>\n<figure><img src=\"/assets/editorial/2019/http-cache-key-2019.svg\" alt=\"Схема выбора HTTP-кэша: метод и URL задают базовый ключ, Vary добавляет поля запроса, затем кэш проверяет свежесть и при необходимости валидирует ETag у origin\" loading=\"lazy\" /><figcaption>Кэш сначала выбирает подходящее представление по ключу, затем проверяет свежесть. Только после этого для несвежего ответа начинается повторная проверка через валидатор.</figcaption></figure>\n<h2>Порядок проверки</h2>\n<ol><li>Выберите один тестовый URL и опишите, какой ответ считается публичным, а какой персональным. Для HTML, asset и API правила могут различаться.</li><li>Запишите метод и все входы, которые могут менять тело: query, язык, кодирование, cookie, авторизацию, эксперимент или географию. Отдельно отметьте, какие из них реально входят в cache key.</li><li>Снимите ответ origin и публичный ответ одним методом. Сохраните статус, digest тела, <code>Cache-Control</code>, <code>ETag</code>, <code>Vary</code>, <code>Age</code> и <code>Date</code>, если поля есть.</li><li>Проверьте два варианта, меняя только один вход. Если тела различаются, найдите правило, которое разделяет их; если одинаковы, не добавляйте <code>Vary</code> по предположению.</li><li>Сопоставьте <code>max-age</code> и, если он используется, <code>s-maxage</code>. Не называйте истечение TTL удалением записи: оно может привести к условному запросу, а не к загрузке нового тела.</li><li>Повторите запрос после истечения срока с валидатором именно того варианта. Зафиксируйте <code>304</code>, новый <code>200</code> или иной фактический ответ и объясните его по заголовкам.</li><li>Сравните origin, reverse proxy, CDN и браузер, если эти границы доступны. Для браузера отдельно исключите service worker и историю навигации. Не очищайте кэш до первого снимка.</li><li>Проверьте отрицательный путь: персональный ответ, неизвестный язык, изменение asset и запрос без валидатора. Для каждого заранее запишите безопасный ожидаемый результат.</li></ol>\n<h2>Ограничения</h2>\n<p>HTTP задаёт правила кэша, но не описывает конфигурацию каждого CDN. Посредник может иметь отдельный cache key, TTL, stale policy и операцию purge. Браузер может показать данные из service worker или навигационной истории, а не из обычного HTTP-кэша. Поэтому один ответ <code>curl</code> не описывает весь путь пользователя.</p>\n<p><code>Vary</code> не заменяет проверку персонализации. Если тело зависит от cookie, эксперимента или пользователя, попытка перечислить все поля дробит кэш и может оставить риск утечки. Для личного HTML безопаснее начать с <code>private</code> или <code>no-store</code>, а публичную локализацию ограничить известным списком вариантов.</p>\n<p>Учебный домен, хэш файла и значение <code>ETag</code> не являются результатами измерения. Стандарт не гарантирует конкретный hit ratio, ускорение или поведение vendor-specific purge. В диагностике нельзя публиковать токены, cookies, персональные query-параметры и внутренние адреса.</p>\n<h2>Критерий готовности</h2>\n<p>Проверку можно считать завершённой, когда для выбранного URL записаны входы, допустимая давность, владелец правила cache key, ожидаемые директивы, валидатор и граница, на которой каждый пункт проверяется. Два запроса с разными входами не смешивают тела. После истечения срока неизменённое представление даёт подтверждённую условную проверку, а изменённое — новый ответ. Новый asset получает новый URL. Персональный ответ не попадает в shared cache.</p>\n<p>Если это нельзя показать заголовками, телом и снимками на доступных границах, контракт ещё не доказан. Исправление начинается с уровня, где появилось расхождение: origin, proxy, CDN или браузер. Очистка кэша может убрать симптом, но не доказывает, что ключ и срок теперь согласованы.</p>\n<h2>Проверяемые источники</h2><ul><li><a href=\"https://www.rfc-editor.org/rfc/rfc9111.html\" target=\"_blank\" rel=\"noopener noreferrer\">RFC 9111: HTTP Caching</a> — стандарт для cache key, <code>Vary</code>, свежести, валидации, <code>Age</code> и директив <code>Cache-Control</code>.</li><li><a href=\"https://www.rfc-editor.org/rfc/rfc9110.html\" target=\"_blank\" rel=\"noopener noreferrer\">RFC 9110: HTTP Semantics</a> — семантика представлений, валидаторов, <code>ETag</code>, <code>Vary</code> и условных ответов.</li><li><a href=\"https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Caching\" target=\"_blank\" rel=\"noopener noreferrer\">MDN: HTTP caching</a> — практическое объяснение private/shared cache, <code>no-cache</code>, <code>no-store</code>, proxy и managed cache.</li></ul>"
}