# теория · шаг 2 из 6
Ключ и что кешировать нельзя
Коротко
Кеш включили, доля HIT выросла, все довольны. Через день на второй странице каталога показывается первая, а в личном кабинете - чужое имя. Обе истории про ключ.
- Ключ по умолчанию -
$scheme$proxy_host$request_uri, то есть вместе со строкой параметров. - Замена
$request_uriна$uriсхлопывает?page=1и?page=2в одну запись. Замер показывает это прямо. Set-Cookie,no-storeиprivatenginx отсеивает сам - и это защита, а не помеха.proxy_cache_bypassиproxy_no_cacheпишут парой: иначе личный ответ уедет следующему анониму.
Если помнишь, что в ключе по умолчанию есть строка запроса, и знаешь пару bypass/no_cache - листай до «Теперь сам».
Сначала ответь сам
Популярная «оптимизация» из чужих конфигов - убрать из ключа строку запроса, «чтобы метки рекламных кампаний не плодили записи»:
proxy_cache_key $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, а бэкенд молчит - жаловаться некому.
На что это похоже
Гардероб, где номерки выдают по фамилии, а не по человеку. Пока однофамильцев нет, схема работает. Первый же второй Иванов уносит чужое пальто - и никакой ошибки при этом не происходит: система отработала ровно так, как её попросили.
Ключ кеша - это номерок. Всё, что не попало в ключ, для кеша не существует.
Механизм: что входит в ключ
Правило
Персональные ответы не кешируются. Не «кешируются с ключом по cookie», а не кешируются вовсе - редкий сайт может позволить себе хранить персональные страницы всех пользователей.
proxy_cache_bypass $http_authorization $cookie_session; # не читать из кеша
proxy_no_cache $http_authorization $cookie_session; # не писать в кеш
Обе директивы принимают список переменных и срабатывают, если хотя бы одна не
пуста и не равна нулю. Разница принципиальная: bypass не берёт ответ из кеша,
no_cache не кладёт его туда. Пишут их парой - иначе авторизованный
пользователь обойдёт кеш при чтении, а его личный ответ прекрасно туда запишется
и уедет следующему анониму.
Замер, как это выглядит в статусах:
/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: *.
Это разумная защита. Отключать её можно, только когда точно знаешь, почему бэкенд шлёт эти заголовки:
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 каждому анониму на публичной странице.
Разбор: не кешировать сразу
proxy_cache_min_uses 3;
запрос 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 и корпоративный прокси -
только по заголовку.
Зачем это в работе
Первое, что стоит сделать после включения кеша, - проверить его на чужом пользователе. Открой страницу под авторизацией, потом ту же страницу в приватном окне и сравни: если во втором окне видно имя первого пользователя, у тебя ровно та ошибка, ради которой написана эта глава.
Второе - посмотреть в лог на число разных ключей. Если у страницы каталога сотни тысяч ключей из-за меток рекламных кампаний, правильный ход не «убрать параметры из ключа», а вырезать конкретные метки:
map $arg_page$arg_sort$arg_per_page $cache_args {
"" ""; # полезных параметров нет - ключ без них
default "page=$arg_page&sort=$arg_sort&per=$arg_per_page";
}
proxy_cache_key "$scheme$proxy_host$uri?$cache_args";
Смысл: ключ собирается из ИЗВЕСТНОГО списка значимых параметров, а всё остальное - включая метки кампаний - в ключ просто не попадает. Так ошибиться нельзя: что не перечислено, то не учтено. Замер на 1.31.6:
/catalog/?page=2 → /catalog/?page=2&sort=&per=
/catalog/?page=2&utm_source=x → /catalog/?page=2&sort=&per= ← тот же ключ
/catalog/?page=2&sort=asc → /catalog/?page=2&sort=asc&per=
/catalog/?page=3&utm_campaign=a&sort=asc → /catalog/?page=3&sort=asc&per=
/catalog/?utm_source=x → /catalog/?
Метки не меняют ключ, а значимые параметры меняют - именно это и требовалось.
Соблазн вырезать метки регуляркой: не делай так
Напрашивается другое решение - оставить $request_uri и вырезать из него метки
одним map с регуляркой. Выглядит короче, работает неверно. Вот ровно такая
попытка:
map $request_uri $cache_uri {
~^(?<clean>[^?]*)\?(.*&)?utm_[^&]*(&(?<rest>.*))?$ $clean?$rest;
default $request_uri;
}
Замер на nginx 1.31.6 - что получается в $cache_uri:
/cat?utm_source=x → /cat?
/cat?page=2&utm_source=x → /cat? ← page=2 ПОТЕРЯН
/cat?utm_source=x&page=2 → /cat?page=2
/cat?page=2&utm_source=x&sort=asc → /cat?sort=asc ← page=2 ПОТЕРЯН
/cat?page=2 → /cat?page=2
Жадная группа (.*&)? съедает всё, что стоит ПЕРЕД меткой, и вторая страница
каталога получает тот же ключ, что первая. Это ровно та ошибка, от которой
предостерегает эта глава, - только спрятанная в регулярку. Для произвольной
строки параметров безопасный вариант один: либо оставить $request_uri целиком,
либо собирать ключ из перечисленных $arg_*, как в конфиге выше.
Разница с $uri в том, что здесь в ключе остаются известные значимые
параметры, а не все подряд и не «все, кроме тех, что я вспомнил».
Теперь сам
В кеш попал ответ с чужой сессией: аноним видит имя другого пользователя. В
конфиге стоит proxy_cache_bypass $cookie_session;, а proxy_no_cache нет. Как
это произошло?
Авторизованный пользователь пришёл с cookie - bypass честно отправил его мимо
кеша на бэкенд. Но запретить запись было нечем: персональный ответ уехал в
кеш и стал общим. Пара директив пишется всегда вместе; проверяется это тем же
опытом с приватным окном.
Главное
Ключ кеша должен различать запросы с разным содержимым ответа. Для персональных запросов отдельно запрети чтение общей копии и запись персонального ответа. Проверь последовательность «авторизованный запрос, затем анонимный»: одиночный BYPASS ещё не доказывает, что личные данные не попали в кеш.
Комментарии
Пока нет комментариев. Будь первым!
Оставить комментарий