Выберите продукт

Nginx proxy_cache: cache_key, Vary и как поднять hit ratio без утечек приватных данных

Низкий hit ratio в Nginx proxy_cache чаще связан не с размером кеша, а с неправильным proxy_cache_key и игнорированием Vary. Разберём безопасные схемы для Authorization и Cookie, bypass/no_cache и управляемый cache busting.
Nginx proxy_cache: cache_key, Vary и как поднять hit ratio без утечек приватных данных

Почему связка cache_key + Vary напрямую влияет на hit ratio

Когда вы включаете proxy_cache, ожидаете стабильный HIT на повторных запросах и заметное снижение нагрузки на апстрим. На практике hit ratio часто «плавает» или остаётся низким, даже если кеш большой и TTL выглядит разумным. Обычно причины две:

  • слишком «раздутый» ключ кешаproxy_cache_key меняется от запроса к запросу из-за лишних параметров, cookie и заголовков;
  • неучтённая вариативность ответа — приложение реально меняет тело ответа, а ключ этого не отражает (или, наоборот, дробит кеш слишком мелко).

В HTTP за вариативность отвечает заголовок Vary: он сообщает кешам, что ответ зависит от определённых заголовков запроса. Важный нюанс: в Nginx как reverse proxy заголовок Vary сам по себе не «подмешивается» в ключ автоматически. Nginx выбирает объект по proxy_cache_key, который вы задали (или оставили дефолтный). Если приложение отдаёт разные версии по Accept-Encoding или Accept-Language, а в ключе этого нет, вы рискуете получать неправильные ответы из кеша.

Продакшен-правило: всё, что реально меняет тело ответа, должно быть либо частью proxy_cache_key, либо должно отключать кеширование (bypass и запрет записи).

Как Nginx выбирает объект в кеше: что такое proxy_cache_key

Ключ задаётся директивой proxy_cache_key. В продакшене лучше задавать ключ явно, чтобы поведение было предсказуемым при обновлениях конфигов и переносе между окружениями.

Базовый безопасный ключ для reverse proxy обычно включает:

  • схему ($scheme), чтобы не смешивать нетипичные схемы проксирования;
  • хост ($host), если у вас несколько доменов на одном кеш-зоне;
  • URI с query string ($request_uri), если параметры действительно влияют на ответ.
proxy_cache_key "$scheme://$host$request_uri";

Самая частая ошибка, убивающая hit ratio: оставлять в ключе «шумные» параметры (маркетинговые метки, трекинг, случайные идентификаторы), из-за которых каждый запрос уникален.

Быстрый тест: почему кеш «не попадает»

Если вы добавили отладочный заголовок (например, X-Cache-Status) или логируете $upstream_cache_status, типовые симптомы такие:

  • MISS почти всегда — ключ слишком вариативный, маленький inactive, либо кеш постоянно обходится условиями proxy_cache_bypass/proxy_no_cache;
  • BYPASS — сработало правило обхода (cookie, authorization, метод, заголовок, аргумент);
  • HIT есть, но «не тот контент» — забыли учесть Vary-факторы в ключе и склеили разные варианты ответа в один.

Пример анализа логов Nginx со статусами HIT, MISS и BYPASS

Vary header: что с ним делать в Nginx proxy_cache

Vary чаще всего встречается в сценариях:

  • Vary: Accept-Encoding — разные версии (gzip/br/identity);
  • Vary: Accept-Language — локализация;
  • Vary: Origin — CORS и мульти-ориджин API.

Если апстрим возвращает Vary, это сигнал: без учёта этих заголовков в ключе кеш может отдать неправильный ответ. В Nginx подход практический: вы сами добавляете в proxy_cache_key те факторы, которые действительно разделяют тело ответа.

Пример: учитывать Accept-Encoding

Если сжатие происходит на апстриме и он реально возвращает разные тела (например, br vs gzip), добавьте $http_accept_encoding в ключ. Но учтите: заголовок может быть очень вариативным (порядок значений, лишние токены), и это дробит кеш.

proxy_cache_key "$scheme://$host$request_uri|ae=$http_accept_encoding";

Компромисс для hit ratio: если вы сами сжимаете на Nginx, а к апстриму ходите за несжатым (или принудительно «снимаете» Accept-Encoding к апстриму), тогда дробление по Accept-Encoding часто можно убрать. Перед изменениями проверьте, где именно происходит компрессия и какие заголовки возвращает апстрим.

Пример: учитывать язык (лучше нормализованный)

Если локаль реально меняет HTML/JSON, добавляйте язык в ключ. Но Accept-Language тоже «шумный», поэтому лучше свести его к 2–3 значениям через map и класть в ключ нормализованный маркер.

map $http_accept_language $lang_key {
    default en;
    ~*^ru ru;
    ~*^en en;
}

proxy_cache_key "$scheme://$host$request_uri|lang=$lang_key";

Так вы получаете корректность (разные языки не смешиваются) и высокий hit ratio (не плодите десятки вариантов ключей).

FastFox VDS
Облачный VDS-сервер в России
Аренда виртуальных серверов с моментальным развертыванием инфраструктуры от 195₽ / мес

Authorization и private cache: где чаще всего делают утечки

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

  • если есть Authorizationпо умолчанию обход (самое безопасное);
  • если вам осознанно нужен private cache — ключ должен разделять пользователей без хранения секретов в явном виде.

Вариант 1 (рекомендуется для большинства): bypass по Authorization

map $http_authorization $skip_cache_auth {
    default 1;
    ""      0;
}

proxy_cache_bypass $skip_cache_auth;
proxy_no_cache     $skip_cache_auth;

Да, это может снизить hit ratio в авторизованной зоне, зато резко снижает шанс закешировать приватный ответ.

Вариант 2: private cache для авторизованных (только если уверены)

Иногда нужно кешировать ответы «на пользователя» (например, тяжёлая агрегация, но одинаковая в рамках учётки). Тогда:

  • делите кеш по идентификатору пользователя, а не по сырому токену;
  • ставьте небольшой TTL;
  • не кешируйте ответы, содержащие чувствительные данные, если нет жёсткой необходимости.

Рабочий паттерн: апстрим после проверки токена добавляет доверенный заголовок X-User-Id, и вы используете его для ключа.

proxy_cache_key "$scheme://$host$request_uri|uid=$upstream_http_x_user_id";

Важно: не используйте клиентский $http_x_user_id для private cache. Только то, что выставляет апстрим после валидации, и только если вы уверены, что этот заголовок нельзя подменить на пути.

Cookie: главный убийца hit ratio (и как его приручить)

Многие приложения выставляют cookie «на каждый чих»: сессии, A/B тесты, аналитика, флаги интерфейса. Если вы добавите $http_cookie в proxy_cache_key, hit ratio легко падает почти до нуля: cookie уникальны у каждого пользователя.

Практический подход для публичного контента:

  • обход кеша при наличии «значимых» cookie (сессия, корзина, логин);
  • маркетинговые/аналитические cookie игнорировать для кеша (не добавлять в ключ и не делать bypass);
  • в спорных местах безопаснее отключить кеширование, чем пытаться «угадать» персонализацию.

Пример: bypass по cookie сессии

map $http_cookie $skip_cache_cookie {
    default 0;
    ~*"(session|sessid|phpsessid|wordpress_logged_in)" 1;
}

proxy_cache_bypass $skip_cache_cookie;
proxy_no_cache     $skip_cache_cookie;

Эта схема хорошо поднимает hit ratio на публичных страницах и снижает риск закешировать персональные ответы.

Cache busting: когда «обновить кеш» превращается в постоянные MISS

cache busting — это любая техника принудительного обхода кеша: уникальный query string, специальные заголовки, ручной purge (если реализован), смена ключа. В разработке удобно, в проде опасно: если фронтенд начинает добавлять уникальный параметр к каждому запросу (например, ?t=timestamp), вы фактически отключаете кеш и постоянно бьёте апстрим.

Практика, которая обычно работает:

  • для статических ассетов используйте версионирование в имени файла, а не timestamp в query string;
  • для API опирайтесь на TTL, ETag/If-None-Match и версионирование схемы;
  • если нужен ручной обход — делайте отдельный служебный заголовок/параметр и ограничивайте доступ (IP/ACL/доступ к админке).

Управляемый bypass через заголовок

map $http_x_cache_bust $skip_cache_bust {
    default 0;
    1 1;
}

proxy_cache_bypass $skip_cache_bust;
proxy_no_cache     $skip_cache_bust;

Так вы не ломаете hit ratio случайными параметрами и не приучаете фронтенд «всегда добавлять timestamp».

FastFox SSL
Надежные SSL-сертификаты
Мы предлагаем широкий спектр SSL-сертификатов от GlobalSign по самым низким ценам. Поможем с покупкой и установкой SSL бесплатно!

proxy_cache_bypass vs proxy_no_cache: в чём разница и где ошибаются

Эти директивы часто ставят рядом, но смысл разный:

  • proxy_cache_bypass — не брать ответ из кеша (запрос пойдёт на апстрим), но ответ всё ещё может быть сохранён;
  • proxy_no_cache — не сохранять ответ в кеш.

Типовая ошибка: включить только bypass, например для авторизованных запросов, и в итоге начать записывать персональные ответы в кеш (просто не отдавая их тем же запросам из кеша). Для Authorization и «значимых» cookie почти всегда ставьте обе директивы.

Дизайн cache_key: практические рецепты для роста hit ratio

Цель простая: ключ должен быть достаточно «узким», чтобы не смешивать разные ответы, и достаточно «широким», чтобы не дробить кеш по незначимым деталям.

1) Нормализуйте query string: уберите мусор

Если сайт получает много параметров вроде utm_*, gclid, fbclid, и они не меняют контент, исключите их из ключа. В Nginx проще всего собрать ключ из значимых аргументов вместо $request_uri.

proxy_cache_key "$scheme://$host$uri?page=$arg_page&sort=$arg_sort";

Минус: нужно договориться с разработкой, какие параметры влияют на ответ. Плюс: hit ratio часто растёт кратно на каталогах, поиске и листингах.

2) Делите по классу устройства, а не по User-Agent

Если приложение действительно отдаёт разные версии HTML для mobile/desktop, не включайте в ключ $http_user_agent целиком: ключ станет почти уникальным. Вместо этого выделите 2–3 класса через map и добавляйте только класс.

3) Осторожнее с Origin и CORS

Если API зависит от Origin, у вас три варианта:

  • не кешировать такие ответы;
  • включать $http_origin в ключ, понимая, что hit ratio просядет при множестве origin;
  • нормализовать origin до ограниченного списка (например, только ваши фронтенды).

Если видите Vary: Origin, воспринимайте это как красный флаг для «общего» кеширования.

Схема: из каких частей собирают безопасный proxy_cache_key в Nginx

Как измерять hit ratio правильно: логи и метрики, а не «ощущения»

Hit ratio — это не только «сколько HIT». Важно понимать доли MISS, BYPASS, EXPIRED, STALE (если используете) и причины.

Минимум, который быстро окупается: добавить $upstream_cache_status в формат логов и агрегировать по location/URI. Пример:

log_format main_ext '$remote_addr - $host "$request" $status '
                    'cache=$upstream_cache_status '
                    'rt=$request_time urt=$upstream_response_time '
                    'ua="$http_user_agent"';

Если у вас кешируются большие ответы (HTML, JSON, файлы), отдельно проверьте отдачу по диапазонам и влияние Range: это легко ломает кеш и может снижать hit ratio на загрузках. По теме полезно: как Range влияет на кеширование в Nginx/Apache.

Типовые безопасные шаблоны конфигурации (соберите под себя)

Ниже «скелет» как отправная точка: явный ключ, обход по авторизации/сессии, управляемый bypass. Дальше вы точечно добавляете вариативность (язык, кодировка, origin) только там, где это реально меняет тело ответа.

proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=appcache:200m max_size=10g inactive=60m use_temp_path=off;

map $http_authorization $skip_cache_auth {
    default 1;
    ""      0;
}

map $http_cookie $skip_cache_cookie {
    default 0;
    ~*"(session|sessid|phpsessid|wordpress_logged_in)" 1;
}

map $http_x_cache_bust $skip_cache_bust {
    default 0;
    1 1;
}

server {
    location / {
        proxy_cache appcache;
        proxy_cache_key "$scheme://$host$request_uri";

        proxy_cache_bypass $skip_cache_auth $skip_cache_cookie $skip_cache_bust;
        proxy_no_cache     $skip_cache_auth $skip_cache_cookie $skip_cache_bust;

        proxy_cache_valid 200 301 302 10m;
        proxy_cache_valid 404 1m;

        add_header X-Cache-Status $upstream_cache_status;

        proxy_pass http://upstream_app;
    }
}

Если вы отдаёте контент с современными форматами изображений и делите ответы по заголовкам клиента, заранее продумайте нормализацию и ключ. См. также: как учитывать WebP/AVIF через map и не убить hit ratio.

Чеклист: повышаем hit ratio и не ломаем приватность

  1. Зафиксируйте proxy_cache_key явно, чтобы не зависеть от дефолтов.
  2. Уберите «шум» из ключа: лишние query-параметры, сырые User-Agent, полные cookie.
  3. Проверьте Vary у апстрима: всё, что меняет тело ответа, отражайте в ключе или отключайте кеширование.
  4. Authorization по умолчанию — bypass + no_cache. Private cache делайте только осознанно и безопасно.
  5. Cookie — отделяйте «значимые» от «мусорных»; по сессии почти всегда нужен обход.
  6. Измеряйте: логируйте $upstream_cache_status, иначе оптимизация превращается в гадание.
  7. Контролируйте cache busting: никаких timestamp-аргументов «везде», только управляемые механизмы.

Частые грабли

Кешируете редиректы/ошибки и потом «не понимаете», почему всё сломалось

Если кешируете 301/302, убедитесь, что редирект стабилен и не зависит от cookie/языка/гео. Иначе один вариант может «залипнуть» и портить трафик. То же касается 404/500: кешировать можно, но с коротким TTL и пониманием причин.

Ключ включает $host, а контент одинаковый на нескольких доменах

Так hit ratio будет ниже: каждый домен получает свой кеш. Если контент полностью одинаковый и вы хотите общий кеш, используйте канонический хост в ключе (осознанно) и проверьте влияние на абсолютные ссылки, редиректы и SEO-логики приложения.

Vary говорит одно, а приложение реально зависит от другого

Иногда приложение меняет контент по cookie, но не выставляет адекватный VaryVary: Cookie часто превращает кеш в тыкву). В таком случае ориентируйтесь на факт: если cookie влияет на ответ, кешируйте только при отсутствии этой cookie или делайте bypass.

Итог

Высокий hit ratio в Nginx — это не магия размера кеша. Это дисциплина: стабильный proxy_cache_key, осмысленная работа с Vary, строгие правила для Authorization и Cookie, плюс контроль над cache busting. Сделаете ключ слишком общим — получите риск неправильного или приватного контента. Сделаете слишком «мелким» — получите вечные MISS. Правильная середина обычно достигается нормализацией и явным обходом кеша для персональных сценариев.

Поделиться статьей

Вам будет интересно

QEMU Guest Agent (QGA) в KVM/VDS: graceful shutdown, IP-адрес и fsfreeze для консистентных снапшотов OpenAI Статья написана AI (GPT 5)

QEMU Guest Agent (QGA) в KVM/VDS: graceful shutdown, IP-адрес и fsfreeze для консистентных снапшотов

QEMU Guest Agent (qga) делает гостевую ОС «видимой» для KVM/QEMU: помогает корректно выключать ВМ, получать IP-адреса так, как их ...
inotify ENOSPC в Linux: увеличиваем лимиты watches/instances для IDE, watch и CI OpenAI Статья написана AI (GPT 5)

inotify ENOSPC в Linux: увеличиваем лимиты watches/instances для IDE, watch и CI

inotify ENOSPC часто появляется в VS Code, PhpStorm, webpack/vite watch и на CI runners: системе не хватает лимитов watches/instan ...
Kubernetes на VDS: conntrack, nf_conntrack_max и всплески dport scan OpenAI Статья написана AI (GPT 5)

Kubernetes на VDS: conntrack, nf_conntrack_max и всплески dport scan

Если на Kubernetes-нодах появляются «nf_conntrack: table full» и «dport scan», чаще виноваты малые лимиты conntrack и поток коротк ...