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

Дизайн ключа proxy_cache_key: cookies, Vary и map для API и статики

Корректный дизайн proxy_cache_key в Nginx снижает промахи, убирает дубликаты и исключает утечки персональных ответов. Разберём, какие заголовки учитывать, как нормализовать Accept-Encoding и язык через map, и что делать с cookies и Vary.
Дизайн ключа proxy_cache_key: cookies, Vary и map для API и статики

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

Что такое proxy_cache_key и как nginx собирает ключ

В nginx ключ кэширования для блока proxy_* задаётся директивой proxy_cache_key. По умолчанию используется:

proxy_cache_key $scheme$proxy_host$request_uri;

Это значит, что один и тот же путь с разными параметрами запроса — разные объекты. Домены и схема (HTTP/HTTPS) тоже разделяются. Этого часто недостаточно: ответы могут отличаться из-за Accept-Encoding, языка, устройства, авторизации, наличия специфичных cookies.

Важно понимать, как влияет Vary. Если ответ апстрима приходит с заголовком Vary, nginx хранит отдельные варианты для значений соответствующих заголовков запроса. Это позволяет корректно разделять, например, gzip и br варианты при Vary: Accept-Encoding. Отключать уважение к Vary через proxy_ignore_headers Vary почти всегда плохая идея: вы рискуете смешать несовместимые варианты в одном ключе.

Стратегия проектирования: разделяем API и статику

У API и статических ресурсов разные профили вариативности. Для API часто есть авторизация, локализация, CORS, иногда A/B-флаги; для статики — кодек сжатия (Accept-Encoding), возможные cookies браузеров (которые нужно игнорировать), контроль версий через query-строки.

Рабочая стратегия:

  • Явно нормализуем заголовки запроса в небольшое число классов через map.
  • Избегаем «сырого» включения длинных заголовков в proxy_cache_key.
  • Чётко разделяем персонализированные запросы и публичный кэш: либо байпасим, либо в ключ добавляем ровно те факторы, что влияют на ответ.
  • Для статики игнорируем «шумные» cookies и ненужные заголовки. Для API внимательно относимся к Authorization и сессионным cookies.

Accept-Encoding: нормализация через map

Accept-Encoding бывает длинным и вольным (порядок, q-факторы). Сырая подстановка в ключ приводит к множеству дубликатов. Правильнее свести его к классам: br, gzip, identity. Это стабилизирует ключ и увеличивает hit ratio.

# Нормализуем кодек сжатия
map $http_accept_encoding $ae {
    ~br     br;
    ~gzip   gzip;
    default identity;
}

# Учитываем схему, хост, URI и нормализованный кодек
proxy_cache_key $scheme$proxy_host$request_uri|ae:$ae;

Если вы используете gzip_vary on или модуль brotli, апстрим или сам nginx обычно добавляет Vary: Accept-Encoding. Это гарантирует разделение вариантов. Но нормализовать всё равно стоит, иначе значение заголовка клиента станет частью ключа (через Vary) в сырых, «шумных» формулировках. Отдельно см. разбор WebP/AVIF через Nginx и map для кэша.

Нормализация Accept-Encoding через map и влияние на ключ кэша

Cookies: когда включать в ключ, а когда байпасить

Cookies — главный источник как персонализации, так и случайной фрагментации кэша. Общие правила:

  • Если ответ персонализирован (зависит от сессии/логина), публичный cache следует обходить: proxy_no_cache и proxy_cache_bypass по признаку авторизации.
  • Если ответ одинаковый для всех, но апстрим по привычке ставит Set-Cookie (например, трекинг), можно игнорировать этот заголовок при кэшировании и отдаче.
  • Для статики большинство cookies не должны влиять на ключ вообще.
# Выделим «опасные» cookies, влияющие на персонализацию
map $http_cookie $has_session_cookie {
    ~(session|auth|logged_in)= 1;
    default 0;
}

# Если есть авторизация — не кэшируем и не берём из кэша
proxy_no_cache     $has_session_cookie;
proxy_cache_bypass $has_session_cookie;

# Игнорируем Set-Cookie от апстрима, если кэш публичный и контент не персонализирован
proxy_ignore_headers Set-Cookie;

Обратите внимание: игнорировать Set-Cookie можно только там, где вы уверены, что ответ не зависит от пользователя. Иначе получите утечку данных между посетителями.

Vary: Cookie и почему это опасно

Иногда апстрим ставит Vary: Cookie. Тогда любой отличающийся набор cookies становится частью ключа (через механизм Vary), что раздувает кэш до бесконечности. Лучше попросить приложение уточнить Vary (например, по конкретной cookie), либо на прокси нормализовать значения и устранить лишние заголовки.

Язык, устройство, CORS: нормализуем заголовки

Частые причины вариативности — язык (Accept-Language), класс устройства (User-Agent), CORS (Origin). С ними тоже работаем через map, сокращая количество вариантов:

# Язык (пример: RU/EN/прочее)
map $http_accept_language $lang {
    ~^ru   ru;
    ~^en   en;
    default other;
}

# Класс устройства для медиазапросов или шаблонов
map $http_user_agent $device {
    ~Mobile  mobile;
    default  desktop;
}

# CORS: если API реально отвечает по-разному для разных Origin
map $http_origin $origin_key {
    ""      none;
    default  $http_origin;
}

# Итоговый ключ (пример)
proxy_cache_key $scheme$proxy_host$request_uri|ae:$ae|lang:$lang|dev:$device|orig:$origin_key;

Не включайте в ключ сырое значение $http_user_agent — это гарантированная лавина вариантов. Нормализация — обязательна.

API: Authorization и публичные GET

Для API действуют дополнительные правила. По умолчанию nginx кэширует только GET/HEAD. Частные эндпоинты с Authorization или сессионными cookies не должны попадать в публичный кэш.

# Признак авторизованного запроса
map $http_authorization $has_auth_header {
    ~^(?i)Bearer  1;
    ~^(?i)Basic   1;
    default       0;
}

# Объединяем с cookies
map "$has_auth_header$has_session_cookie" $api_private {
    ~1           1;  # есть токен или сессионная cookie
    default      0;
}

# Байпас и запрет кэша для приватных запросов
proxy_no_cache     $api_private;
proxy_cache_bypass $api_private;

# Для публичных GET можно учесть CORS или язык
proxy_cache_key $scheme$proxy_host$request_uri|ae:$ae|lang:$lang|orig:$origin_key;

# Прочее (параметры времени жизни, фоновые апдейты)
proxy_cache_valid 200 10m;
proxy_cache_valid 404 1m;
proxy_cache_background_update on;
proxy_cache_lock on;

Если у вас существуют публичные API-методы без авторизации, кэшируйте их смело, но следите за Vary и не тяните в ключ лишние заголовки. Для приватных методов лучше вовсе не включать cache или держать отдельный, сегрегированный слой (например, пользовательский кэш по токену в приложении). См. также кэширование в Redis/Memcached на уровне приложения.

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

Практически всё из описанного удобнее и безопаснее внедрять на отдельном окружении. Если вы поднимаете обратный прокси под свои задачи, используйте VDS с достаточным объёмом RAM/SSD под кэш и логирование.

Статика: игнорируем лишнее, учитываем версионирование

Статика обычно версионируется через хэши в путях или query-параметры (app.css?v=123). Такую статику удобно кэшировать практически навсегда на уровне прокси. Главные нюансы — кодек сжатия и отсутствие влияния cookies.

# Статика: общий ключ без cookies и прочих заголовков клиента
location ~* \\.(css|js|png|jpg|webp|svg|ico)$ {
    proxy_cache        STATIC;
    proxy_cache_key    $scheme$proxy_host$request_uri|ae:$ae;
    proxy_ignore_headers Set-Cookie;
    proxy_hide_header  Set-Cookie;
    expires            7d;
    add_header         Cache-Control public;
}

Если апстрим заранее отдает сжатую статику (.br или .gz), полезно хранить разные варианты по $ae, либо настроить gunzip on для унификации (учтите нагрузку на CPU). Для больших файлов учитывайте поддержку диапазонов и поведение прокси — см. заметку о работе Range и кэша в материале «HTTP Range в Nginx и Apache».

Стабилизация ключа: общие приёмы

  • Нормализуем всё, что может бесконтрольно расти: Accept-Encoding, Accept-Language, User-Agent, Origin.
  • Разграничиваем приватные запросы: байпас по Authorization и сессионным cookies.
  • Сохраняем в ключе только то, что реально влияет на ответ. Ничего лишнего.
  • Не игнорируйте Vary, если не понимаете последствий. Лучше привести заголовки к канонической форме.
  • Для статики держите ключ максимально коротким и стабильным, учтите лишь кодек сжатия и сам URI.

Диагностика: как понять, что ключ работает

Добавьте служебные заголовки и посмотрите на hit/miss:

add_header X-Cache $upstream_cache_status always;
add_header X-Cache-Key $proxy_cache_key always;

$upstream_cache_status покажет HIT/MISS/BYPASS/EXPIRED, а $proxy_cache_key даст канонический вид ключа (полезно при отладке map-логики). Проверьте, что персонализированные запросы маркируются как BYPASS, а публичные быстро выходят на HIT.

Отладка кэша в Nginx по заголовкам X-Cache и X-Cache-Key

Типичные ошибки

  • Сырой $http_user_agent в ключе. Это мгновенно взрывает кэш и убивает производительность.
  • Игнорирование Vary в надежде «сэкономить». Итог — смешение несовместимых вариантов и некорректная отдача.
  • Кэширование ответов с персональной Set-Cookie без байпаса по сессии. Риск утечек.
  • Учёт всех cookies в ключе «на всякий случай». Это эквивалентно отключению кэша.
  • Отсутствие нормализации Accept-Encoding. Процент хитов падает из-за разнообразия заголовков клиентов.

Готовые шаблоны

Универсальный ключ для публичного HTML

# Нормализации (как выше)
map $http_accept_encoding $ae { ~br br; ~gzip gzip; default identity; }
map $http_accept_language $lang { ~^ru ru; ~^en en; default other; }
map $http_user_agent $device { ~Mobile mobile; default desktop; }

# Ключ и базовая политика
proxy_cache_key $scheme$proxy_host$request_uri|ae:$ae|lang:$lang|dev:$device;
proxy_cache_valid 200 3m;
proxy_cache_background_update on;
proxy_cache_lock on;
add_header Vary Accept-Encoding,Accept-Language always;

Добавление Vary гарантирует корректное разделение на стороне клиентов и кэшей по пути (CDN, браузер), а нормализация удерживает число вариантов в разумных пределах.

API: публичные GET без авторизации

map $http_authorization $has_auth_header { ~^(?i)Bearer 1; ~^(?i)Basic 1; default 0; }
map $http_cookie $has_session_cookie { ~(session|auth|logged_in)= 1; default 0; }
map $http_origin $origin_key { "" none; default $http_origin; }

# Байпас приватного
map "$has_auth_header$has_session_cookie" $api_private { ~1 1; default 0; }
proxy_no_cache     $api_private;
proxy_cache_bypass $api_private;

# Ключ для публичного API
proxy_cache_key $scheme$proxy_host$request_uri|orig:$origin_key|ae:$ae;
add_header Vary Origin,Accept-Encoding always;
proxy_cache_valid 200 5m;
proxy_cache_valid 404 30s;

Если CORS-ответы одинаковые для всех источников, лучше вовсе исключить Origin из ключа и не добавлять его в Vary.

Статика с версионированием

map $http_accept_encoding $ae { ~br br; ~gzip gzip; default identity; }
location /assets/ {
    proxy_cache STATIC;
    proxy_cache_key $scheme$proxy_host$request_uri|ae:$ae;
    proxy_ignore_headers Set-Cookie;
    proxy_hide_header Set-Cookie;
    add_header Cache-Control public, max-age=604800;
}

Версионирование по имени файла или параметру запроса сведёт инвалидацию к обновлению ссылки; кэш можно держать долго без риска. Для защиты отдачи приватных ссылок и контроля TTL обратите внимание на техники с secure-link и кэш.

Обслуживание кэша и контроль роста

Даже идеальный ключ не спасёт от переполнения диска, если не настроены лимиты. Проверьте proxy_cache_path с max_size, сроками удаления и количеством уровней каталогов. Регулярно мониторьте процент хитов и число объектов: лавинообразный рост — сигнал, что где-то в ключ попало «сырьё» (например, сырой Origin от множества доменов).

Краткий чек-лист

  • Определите классы вариативности: язык, кодек, устройство, CORS, авторизация.
  • Соберите канонизаторы через map для каждого класса.
  • Сегрегируйте API и статику: разные ключи и политики.
  • Байпас по Authorization и сессионным cookies.
  • Не игнорируйте Vary, но нормализуйте значения заголовков.
  • Для статики игнорируйте Set-Cookie, держите длинный TTL и простой ключ.
  • Добавьте диагностические заголовки X-Cache и X-Cache-Key.

Грамотный дизайн proxy_cache_key в nginx — это не добавление «всего» в ключ, а жёсткий отбор факторов, реальное нормализующее map и дисциплина в отношении приватных запросов. Тогда ваш cache станет предсказуемым, компактным и действительно ускорит сайт и API.

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

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

Debian/Ubuntu: mount: wrong fs type, bad option, bad superblock — как быстро найти и исправить причину OpenAI Статья написана AI (GPT 5)

Debian/Ubuntu: mount: wrong fs type, bad option, bad superblock — как быстро найти и исправить причину

Ошибка mount: wrong fs type, bad option, bad superblock в Debian/Ubuntu может означать и простую опечатку в имени раздела, и пробл ...
Debian/Ubuntu: XFS metadata corruption и emergency read-only — пошаговое восстановление OpenAI Статья написана AI (GPT 5)

Debian/Ubuntu: XFS metadata corruption и emergency read-only — пошаговое восстановление

Если XFS-раздел внезапно стал доступен только для чтения, а сервер ушёл в emergency mode, главное — не спешить. Разберём безопасны ...
Debian/Ubuntu: как исправить Failed to fetch при apt update OpenAI Статья написана AI (GPT 5)

Debian/Ubuntu: как исправить Failed to fetch при apt update

Ошибка Failed to fetch при apt update в Debian и Ubuntu обычно связана не с самим APT, а с DNS, сетью, зеркалом, прокси, временем ...