8 lines
17 KiB
JSON
8 lines
17 KiB
JSON
{
|
||
"index": 311,
|
||
"slug": "editorial-2019-05-mechanism-http-caching",
|
||
"title": "HTTP-кэш: почему правильный TTL всё равно отдаёт неправильный ответ",
|
||
"excerpt": "Кэш проверяет не только срок хранения. Разбираем ключ варианта, Vary, ETag и границы browser cache, proxy и CDN на наблюдаемом сценарии.",
|
||
"contentHtml": "<p>После релиза пользователь открывает знакомый адрес и видит старый JavaScript или вчерашний язык страницы. В ответе есть <code>Cache-Control: max-age=60</code>, поэтому команда ждёт обновления через минуту. Но один клиент получает новый файл, другой — старый, а CDN продолжает отвечать из своего кэша. Ошибка стоит дороже, чем лишний запрос: пользователь видит неверное состояние, релиз выглядит сломанным, а команда начинает очищать кэш вслепую.</p>\n<p>Причина часто не в числе 60. Кэш хранит представление ответа для конкретного запроса. На это представление влияют URL, query-параметры, заголовки, cookies и правила посредника. <code>Cache-Control</code> описывает свежесть и допустимое хранение. Он не исправляет неполный ключ, не различает личные данные и не заставляет каждый слой сети забыть старую запись.</p>\n<h2>Тезис: срок не заменяет ключ</h2>\n<p>У каждого кэшируемого ответа должны быть три понятных свойства: какой запрос выбирает ответ, как долго его можно использовать без проверки и как подтвердить, что сохранённое представление ещё подходит. В HTTP этим ролям обычно соответствуют ключ варианта, директивы <code>Cache-Control</code> и валидатор вроде <code>ETag</code>. Заголовок <code>Vary</code> сообщает, какие поля запроса участвовали в выборе представления.</p>\n<p>Представьте публичную страницу <code>/catalog</code>. Origin формирует русский или английский HTML по <code>Accept-Language</code>. Для русского запроса кэш сохранил ответ. Следующий английский запрос получит тот же ответ, если посредник использует только URL и не учитывает язык. Этот ответ может быть свежим по <code>max-age</code>, но он всё равно неверен для запроса. Увеличение или уменьшение TTL не меняет ошибочный ключ.</p>\n<p>Другая ситуация возникает у файла <code>/assets/app.js</code>. Если содержимое меняется, а имя остаётся тем же, длинный TTL закрепляет старый файл. Безопасный долгий срок требует нового URL при каждом изменении, например <code>app.4f91.js</code>. Здесь кэш не обязан угадывать выпуск: новый адрес отделяет новое представление от старого.</p>\n<h2>Как кэш принимает решение</h2>\n<p>Клиент отправляет запрос. Кэш ищет сохранённый ответ, который подходит этому запросу. Сначала он проверяет соответствие варианта. Затем оценивает свежесть. Свежий ответ можно вернуть без origin. Несвежий ответ не обязательно нужно загружать заново: кэш может отправить условный запрос с <code>If-None-Match</code>. Origin ответит <code>304 Not Modified</code>, если представление не изменилось, или <code>200</code> с новым телом, если изменилось.</p>\n<p><code>ETag</code> не очищает запись и не делает ответ свежим. Он позволяет сравнить сохранённое представление с текущим. <code>no-cache</code> тоже не означает «не хранить»: директива требует повторной проверки перед использованием. <code>no-store</code> запрещает хранение ответа в соответствии с правилами кэша. <code>private</code> ограничивает использование частным кэшем и подходит для ответа, который нельзя отдавать общему кэшу.</p>\n<p>У ответа может быть несколько точек наблюдения: приложение, reverse proxy, CDN и браузер. Заголовок, снятый на origin, не доказывает, что тот же заголовок дошёл до браузера. Посредник может изменить TTL, ключ или правило обхода. Поэтому проверка должна сравнивать один сценарий на каждой доступной границе.</p>\n<h2>Учебный пример: два варианта одного URL</h2>\n<p>Ниже приведены команды для безопасного тестового домена. Это учебный сценарий, а не результат измерения production. Он меняет только один вход — язык — и сохраняет тело и заголовки отдельно. В реальной команде замените адрес на тестовый URL без личных данных.</p>\n<pre><code># Запросы к origin: меняется только Accept-Language.\ncurl -sS -D /tmp/catalog-ru.headers -o /tmp/catalog-ru.html \\\n -H 'Accept-Language: ru' https://origin.example.test/catalog\n\ncurl -sS -D /tmp/catalog-en.headers -o /tmp/catalog-en.html \\\n -H 'Accept-Language: en' https://origin.example.test/catalog\n\n# Сначала сравниваем договор, затем тело.\ngrep -Ei '^(cache-control|vary|etag|age):' /tmp/catalog-ru.headers\ngrep -Ei '^(cache-control|vary|etag|age):' /tmp/catalog-en.headers\nsha256sum /tmp/catalog-ru.html /tmp/catalog-en.html\n\n# Учебная проверка сохранённого варианта.\ncurl -sS -D - -o /dev/null \\\n -H 'Accept-Language: ru' \\\n -H 'If-None-Match: "catalog-ru-v18"' \\\n https://origin.example.test/catalog</code></pre>\n<p>Если тела различаются по языку, в ответе ожидается <code>Vary: Accept-Language</code> либо эквивалентное явно настроенное правило cache key. Если origin возвращает один язык для обоих запросов, CDN пока не проверяем: источник уже нарушает ожидаемый контракт. Если origin корректен, а публичный адрес склеивает ответы, ищем расхождение в CDN или reverse proxy.</p>\n<p>Значение <code>ETag</code> в команде условное. Его нужно взять из предыдущего ответа для того же языка. Нельзя использовать русский валидатор для английского варианта. При неизменном русском представлении ожидаем <code>304</code> после повторной проверки. После изменения русского HTML ожидаем новый <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>Язык не входит в вариант кэша</td><td>Сделать два запроса с разным <code>Accept-Language</code> и сравнить <code>Vary</code></td><td>Исправить ключ варианта или отказаться от общего кэша</td></tr><tr><td>Всегда приходит полный ответ после истечения TTL</td><td>Нет валидатора или условный запрос не доходит до origin</td><td>Повторить запрос с прежним <code>ETag</code> и проверить путь через proxy</td><td>Настроить устойчивый валидатор и проверить каждый слой</td></tr><tr><td>Публичный ответ отличается от origin</td><td>CDN переписывает заголовки или использует другой ключ</td><td>Снять одинаковые заголовки через origin и публичный адрес</td><td>Сверить правило CDN с HTTP-контрактом origin</td></tr><tr><td>Данные одного пользователя видит другой</td><td>Персональный ответ попал в общий кэш</td><td>Проверить <code>Cookie</code>, авторизацию и <code>Cache-Control</code> на тестовых сессиях</td><td>Выбрать <code>private</code>, <code>no-store</code> или разделить публичную и личную части</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 приводит к 304 или к новому 200\" loading=\"lazy\" /><figcaption>Кэш сначала выбирает подходящее представление, затем проверяет его свежесть. Только после этого срабатывает повторная проверка через валидатор.</figcaption></figure>\n<h2>Порядок проверки</h2>\n<ol><li>Выберите один тестовый URL и запишите допустимую давность ответа. Для цены, HTML, asset и личных данных это могут быть разные правила.</li><li>Выпишите все входы, которые меняют тело: query, язык, кодирование, cookie, авторизацию, эксперимент или географию.</li><li>Снимите ответ origin и публичный ответ одним методом. Сохраните статус, тело или его digest, <code>Cache-Control</code>, <code>ETag</code>, <code>Vary</code> и <code>Age</code>, если поле есть.</li><li>Проверьте два безопасных варианта, меняя только один вход. Разные тела требуют разных вариантов; одинаковые тела не требуют добавлять <code>Vary</code> по предположению.</li><li>Дождитесь истечения выбранного срока на тестовом стенде или используйте короткий TTL. Повторите запрос с прежним <code>ETag</code> и зафиксируйте ожидаемый <code>304</code> либо новый <code>200</code>.</li><li>После изменения правила повторите тот же сценарий через origin, proxy, CDN и браузер, если эти границы доступны. Не очищайте кэш до первого снимка: иначе исчезнет доказательство исходного поведения.</li><li>Проверьте отрицательный путь: персональный ответ, неизвестный язык, изменение asset и запрос без валидатора. Для каждого случая заранее запишите безопасный результат.</li></ol>\n<h2>Ограничения</h2>\n<p>Эта модель объясняет HTTP-контракт, но не знает настройки конкретного CDN. Поставщик может иметь собственный cache key, отдельный TTL и правила обхода. Браузер может показать результат навигационной истории или service worker, а не обычного HTTP-кэша. Заголовок <code>Age</code> может отсутствовать и не доказывает отсутствие хранения. Поэтому один ответ <code>curl</code> не описывает весь маршрут пользователя.</p>\n<p><code>Vary</code> не заменяет явную проверку персонализации. Если тело зависит от cookie, эксперимента или пользователя, перечисление большого набора полей дробит кэш и может оставить риск утечки. Для личного HTML чаще безопаснее не использовать общий кэш. Для публичной локализации нужен ограниченный список вариантов и тест на каждый поддерживаемый язык.</p>\n<p>Учебный пример не обещает конкретное ускорение и не доказывает работу вашей инфраструктуры. Проверяемый результат должен появиться только после запуска команд на контролируемом домене и сравнения ответов. Не публикуйте в диагностике токены, cookie, персональные query-параметры и внутренние адреса.</p>\n<h2>Критерий готовности</h2>\n<p>Работу можно считать завершённой, когда для каждого выбранного URL есть короткая карточка: входы, допустимая давность, ожидаемые директивы, валидатор и граница, на которой это проверяется. Два запроса с разными входами не смешивают тела. После истечения срока неизменённое представление даёт ожидаемую условную проверку, а изменённое — новый ответ. Новый asset получает новый URL. Персональный ответ не попадает в общий кэш. Если хотя бы один пункт нельзя показать заголовками и телом ответа, контракт ещё не доказан.</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> — актуальный стандарт для хранения, свежести, повторной проверки и директив управления кэшем.</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> — официальное практическое объяснение свежести, валидаторов и различий между <code>no-cache</code> и <code>no-store</code>.</li></ul>"
|
||
}
|