8 lines
18 KiB
JSON
8 lines
18 KiB
JSON
{
|
||
"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: "catalog-ru-v18"' 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>"
|
||
}
|