1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6

# теория · шаг 2 из 6

Ключ и что кешировать нельзя

Коротко

Кеш включили, доля HIT выросла, все довольны. Через день на второй странице каталога показывается первая, а в личном кабинете - чужое имя. Обе истории про ключ.

  • Ключ по умолчанию - $scheme$proxy_host$request_uri, то есть вместе со строкой параметров.
  • Замена $request_uri на $uri схлопывает ?page=1 и ?page=2 в одну запись. Замер показывает это прямо.
  • Set-Cookie, no-store и private nginx отсеивает сам - и это защита, а не помеха.
  • proxy_cache_bypass и proxy_no_cache пишут парой: иначе личный ответ уедет следующему анониму.

Если помнишь, что в ключе по умолчанию есть строка запроса, и знаешь пару bypass/no_cache - листай до «Теперь сам».

Сначала ответь сам

Популярная «оптимизация» из чужих конфигов - убрать из ключа строку запроса, «чтобы метки рекламных кампаний не плодили записи»:

/etc/nginx/conf.d/shop.local.confhttp server location /
proxy_cache_key $scheme$proxy_host$uri;

Что увидит человек, открывший вторую страницу каталога?

↳  выводдва запроса подряд (ключ $scheme$proxy_host$uri, без строки запроса)
/cat?page=1  X-Cache-Status: MISS  бэкенд вернул path=/cat?page=1
/cat?page=2  X-Cache-Status: HIT   бэкенд вернул path=/cat?page=1

Первую страницу. $uri - это путь без параметров, поэтому обе страницы получили один ключ. Пользователь листает каталог и видит одно и то же; в логе nginx при этом честный HIT, а бэкенд молчит - жаловаться некому.

На что это похоже

Гардероб, где номерки выдают по фамилии, а не по человеку. Пока однофамильцев нет, схема работает. Первый же второй Иванов уносит чужое пальто - и никакой ошибки при этом не происходит: система отработала ровно так, как её попросили.

Ключ кеша - это номерок. Всё, что не попало в ключ, для кеша не существует.

Механизм: что входит в ключ

умолчание $scheme $proxy_host $request_uri = /cat?page=2 заменили на $uri $uri = /cat параметры выпали всё, чего нет в ключе, кеш считает одинаковым
Сокращать ключ можно только тем, что точно не влияет на содержимое ответа.

Правило

Персональные ответы не кешируются. Не «кешируются с ключом по cookie», а не кешируются вовсе - редкий сайт может позволить себе хранить персональные страницы всех пользователей.

/etc/nginx/conf.d/shop.local.confhttp server location /
proxy_cache_bypass $http_authorization $cookie_session;   # не читать из кеша
proxy_no_cache     $http_authorization $cookie_session;   # не писать в кеш

Обе директивы принимают список переменных и срабатывают, если хотя бы одна не пуста и не равна нулю. Разница принципиальная: bypass не берёт ответ из кеша, no_cache не кладёт его туда. Пишут их парой - иначе авторизованный пользователь обойдёт кеш при чтении, а его личный ответ прекрасно туда запишется и уедет следующему анониму.

Замер, как это выглядит в статусах:

↳  выводproxy_cache_bypass $arg_nocache (запись /mu уже прогрета)
/mu?nocache=1   X-Cache-Status: BYPASS
/mu             X-Cache-Status: HIT      ← прежняя запись цела

Разбор: что nginx не кеширует сам

Кое-что он отсеивает без всяких настроек - по заголовкам ответа бэкенда. Проверено на стенде, два запроса подряд к каждому адресу:

ответ бэкенда статусы
Set-Cookie: sid=abc MISS, MISS - обращений к бэкенду 2
Cache-Control: no-store MISS, MISS - обращений к бэкенду 2
обычный MISS, HIT - обращений к бэкенду 1

Тот же список полностью: Set-Cookie, Cache-Control со значением no-cache, no-store или private, Expires в прошлом, Vary: *.

Это разумная защита. Отключать её можно, только когда точно знаешь, почему бэкенд шлёт эти заголовки:

/etc/nginx/conf.d/shop.local.confhttp server location /
proxy_ignore_headers Cache-Control Set-Cookie Expires;
proxy_hide_header    Set-Cookie;
↳  выводс этими двумя строками
/cookie   MISS, затем HIT - обращений к бэкенду 1
/nocache  MISS, затем HIT - обращений к бэкенду 1

Обрати внимание на вторую строку конфига. Без proxy_hide_header ты закешируешь ответ вместе с чужой сессионной cookie и раздашь её всем - это хуже, чем отсутствие кеша. Классический повод так делать один: фреймворк выдаёт сессионную cookie каждому анониму на публичной странице.

Разбор: не кешировать сразу

/etc/nginx/conf.d/shop.local.confhttp server location /
proxy_cache_min_uses 3;
↳  выводчетыре запроса подряд при proxy_cache_min_uses 3 (X-Cache-Status)
запрос 1  MISS   бэкенд: 1
запрос 2  MISS   бэкенд: 2
запрос 3  MISS   бэкенд: 3
запрос 4  HIT    бэкенд: 3

Ответ сохраняется только на третьем обращении к тому же ключу. Приём против засорения зоны «хвостом»: миллион уникальных адресов, каждый из которых запрошен однажды, вытеснит действительно горячие записи. Плата видна в замере - до третьего раза бэкенд работает как без кеша.

Рядом стоит proxy_cache_methods (по умолчанию GET HEAD): кешировать POST можно, но почти никогда не нужно.

Что ломается без этого

Сокращённый ключ ломает не только страницы. Всё, что влияет на содержимое, но не попало в ключ, склеивается в одну запись: язык из Accept-Language, версия API из заголовка, мобильная и десктопная вёрстка, сжатая и несжатая версия ответа.

Последнее особенно коварно и решается не ключом, а заголовком: Vary: Accept-Encoding (первая глава раздела) говорит кешам на пути, что версий несколько. Свой кеш nginx это понимает сам, а вот CDN и корпоративный прокси - только по заголовку.

Зачем это в работе

Первое, что стоит сделать после включения кеша, - проверить его на чужом пользователе. Открой страницу под авторизацией, потом ту же страницу в приватном окне и сравни: если во втором окне видно имя первого пользователя, у тебя ровно та ошибка, ради которой написана эта глава.

Второе - посмотреть в лог на число разных ключей. Если у страницы каталога сотни тысяч ключей из-за меток рекламных кампаний, правильный ход не «убрать параметры из ключа», а вырезать конкретные метки:

/etc/nginx/nginx.confhttp
map $request_uri $cache_uri {
    ~^(?<clean>[^?]*)\?(.*&)?utm_[^&]*(&(?<rest>.*))?$  $clean?$rest;
    default                                             $request_uri;
}

Разница с $uri в том, что здесь ты убираешь известные бесполезные параметры, а не все подряд.

Теперь сам

В кеш попал ответ с чужой сессией: аноним видит имя другого пользователя. В конфиге стоит proxy_cache_bypass $cookie_session;, а proxy_no_cache нет. Как это произошло?

Авторизованный пользователь пришёл с cookie - bypass честно отправил его мимо кеша на бэкенд. Но запретить запись было нечем: персональный ответ уехал в кеш и стал общим. Пара директив пишется всегда вместе; проверяется это тем же опытом с приватным окном.

Главное

Ключ по умолчанию содержит $request_uri вместе с параметрами; замена его на $uri схлопывает ?page=1 и ?page=2 в одну запись, и замер показывает честный HIT с чужим содержимым. Персональные ответы не кешируются вовсе: Set-Cookie, no-store и private nginx отсеивает сам, а для авторизованных ставят парой proxy_cache_bypass и proxy_no_cache - без второй директивы личный ответ попадёт в общий кеш. Отключая защиту через proxy_ignore_headers, обязательно снимай Set-Cookie через proxy_hide_header. proxy_cache_min_uses бережёт зону от одноразовых адресов ценой первых обращений.

Комментарии

Пока нет комментариев. Будь первым!

Оставить комментарий

Комментарий появится после проверки. Email не публикуется. Войти, чтобы не вводить имя каждый раз.