Новинка Виртуальный VDS сервер в Финляндии от 390р
Выберите продукт

Умный кэш Nginx для API и внешних бэкендов: proxy_cache, stale‑while‑revalidate и cache lock

Как снизить латентность и нагрузку на внешние бэкенды, не ломая логику API? Разберём боевые паттерны Nginx: proxy_cache, ключи и TTL, stale‑while‑revalidate, cache lock, revalidate по ETag/Last‑Modified и устойчивость к сбоям.
Умный кэш Nginx для API и внешних бэкендов: proxy_cache, stale‑while‑revalidate и cache lock

API, которые ходят к внешним бэкендам (SaaS, микросервисы в другом кластере, сторонние сервисы), часто упираются в сеть, квоты и холостые повторные запросы. Грамотный reverse proxy на Nginx с proxy_cache, stale‑while‑revalidate и cache lock резко снижает латентность и защищает бэкенд от «бурстов». Ниже — практические схемы и конфиги для продакшна.

Когда кэшировать API — и когда нельзя

Кэширование хорошо работает для идемпотентных запросов (GET, HEAD) и ответов, которые зависят только от публичных параметров (путь, query, заголовки согласования). Если в ответе присутствуют персональные данные пользователя или привязка к сессии, кэшировать это без сегментации ключа недопустимо. Минимальные правила:

  • Кэшируем только GET/HEAD; POST — по умолчанию не кэшируем.
  • Если приходит Authorization или есть авторизационные куки — кэш пропускаем.
  • Ответы с Set-Cookie не кладем в кэш (или аккуратно скрываем этот заголовок и сегментируем ключ, если это не персональные данные).
  • Уважаем Cache-Control/Expires, ETag, Last-Modified, Vary.

Если API возвращает персональные данные, а вариации определяются токеном или cookie, лучше пропускать кэш. Альтернатива — строгая сегментация ключа по токену, что фактически превращает кэш в per-user и редко оправдано.

Базовая архитектура proxy_cache

Кэш в Nginx состоит из зоны метаданных в памяти (keys_zone) и файлов на диске. Минимальная настройка: путь, размер, уровни каталогов, неактивность и лимиты загрузчика. Если разворачиваете прокси на отдельном сервере — удобно взять VDS и выделить быстрый диск под каталог кэша.

# http {}
proxy_cache_path /var/cache/nginx/api levels=1:2 keys_zone=api_cache:200m max_size=20g inactive=1h use_temp_path=off loader_threshold=300 loader_files=200;

Пояснения по параметрам:

  • levels=1:2 — разбиение кеш-файлов на подпапки, чтобы избежать огромных директорий.
  • keys_zone=api_cache:200m — индекс в памяти для быстрых lookup. 200 МБ обычно достаточно для десятков миллионов ключей, но зависит от длины ключей.
  • max_size — лимит суммарного размера кэша на диске.
  • inactive — срок, после которого неиспользуемые элементы удаляются, даже если их TTL ещё не истёк.
  • loader_threshold/loader_files — фоновая подгрузка индекса при старте.
  • use_temp_path=off — безопаснее и быстрее писать сразу в каталог кэша.

Серверный блок: минимальная рабочая конфигурация

server {
    listen 80;
    server_name api.example.local;

    # Ключ кэша: метод + хост + URI + query
    set $cache_key "$request_method|$host|$request_uri";

    location / {
        proxy_pass http://backend_api;

        proxy_cache api_cache;
        proxy_cache_key $cache_key;
        proxy_cache_valid 200 301 404 10m;
        proxy_cache_valid any 1m;

        proxy_cache_background_update on;      # stale‑while‑revalidate
        proxy_cache_use_stale updating error timeout http_500 http_502 http_503 http_504;
        proxy_cache_lock on;                   # защита от thundering herd
        proxy_cache_lock_age 60s;
        proxy_cache_lock_timeout 10s;
        proxy_cache_revalidate on;             # 304 по ETag/Last-Modified

        proxy_buffering on;
        proxy_buffers 16 32k;
        proxy_busy_buffers_size 64k;

        proxy_connect_timeout 3s;
        proxy_read_timeout 15s;
        proxy_send_timeout 15s;

        add_header X-Cache-Status $upstream_cache_status always;
    }
}

Эта базовая схема уже даёт: кэширование успешных ответов, фоновые обновления при протухании, раздачу stale при сбое бэкенда и блокировку запросов‑дубликатов на холодном промахе.

Как работает cache lock в Nginx: один запрос к апстриму, остальные ждут

Контроль ключа кэша: чтобы не кэшировать приватное

Главная ошибка — общий ключ для всех пользователей. В API часто приходит Authorization: Bearer ..., сессионные куки, локаль. Покажем безопасную матрицу пропуска кэша:

# http {}
map $request_method $cache_method_ok {
    default 0;
    GET 1;
    HEAD 1;
}

map $http_authorization $auth_present {
    default 1;  # любой непустой — есть авторизация
    "" 0;
}

map $cookie_session $sess_present {
    default 1;  # любая сессия — считаем приватным
    "" 0;
}

# 0 — кэш разрешен; 1 — пропуск кэша
map "$cache_method_ok$auth_present$sess_present" $skip_cache {
    default 1;
    "100" 0;  # GET/HEAD без авторизации и без сессии
}
location /api/ {
    proxy_pass http://backend_api;

    proxy_cache api_cache;
    proxy_cache_key "$request_method|$host|$request_uri";

    proxy_no_cache $skip_cache;
    proxy_cache_bypass $skip_cache;

    # не кэшировать, если апстрим прислал Set-Cookie
    map $upstream_http_set_cookie $no_cache_setcookie {
        default 1;
        "" 0;
    }
    proxy_no_cache $no_cache_setcookie;

    proxy_cache_valid 200 301 404 10m;
    proxy_cache_background_update on;
    proxy_cache_use_stale updating error timeout http_500 http_502 http_503 http_504;
    proxy_cache_lock on;
    proxy_cache_lock_age 60s;
    proxy_cache_lock_timeout 10s;
    proxy_cache_revalidate on;

    add_header X-Cache-Status $upstream_cache_status always;
}

Ключ кэша можно обогащать, если API зависит, например, от локали: добавьте |$http_accept_language или вводите явный параметр lang и включите query в ключ (в примерах он уже учтен). Заголовок Vary Nginx учитывает: кэширующая прослойка хранит отдельные варианты, если апстрим вернул Vary.

TTL: откуда он берется и как им управлять

Есть два пути:

  • Доверять апстриму: он присылает Cache-Control: s-maxage=N, stale-while-revalidate=M, stale-if-error=K и/или Expires. Тогда можно не задавать proxy_cache_valid или оставить только fallback any.
  • Принудительно задавать TTL на прокси: через proxy_cache_valid для кодов 200/301/404 и резервный на «любой» ответ.

В API удобно совмещать: уважать заголовки апстрима, но иметь дефолтные TTL на случай их отсутствия. Если вы получаете из внешнего сервиса агрессивные no-store, а ваш кейс допускает кэш — можно выборочно игнорировать:

# Осторожно: осознанно переопределяем политику апстрима
proxy_ignore_headers Cache-Control Expires;
proxy_cache_valid 200 1m;

Избыточное игнорирование Cache-Control чревато устаревшими данными. Применяйте только для безопасных публичных эндпоинтов (справочники, списки, конфигурации, которые редко меняются).

stale‑while‑revalidate: быстрые ответы при протухшем кэше

Паттерн «отдаём устаревшее, но сразу в фоне обновляем» перекрывает пики и резкие всплески трафика. В Nginx это включается двумя настройками:

  • proxy_cache_background_update on; — при просроченном объекте ответа клиенту отдаётся текущая копия, а обновление выполняется в фоне.
  • proxy_cache_use_stale updating; — разрешает использовать устаревшую копию, пока идёт обновление.

Если апстрим присылает Cache-Control: stale-while-revalidate=30, Nginx будет в эти 30 секунд после истечения основного TTL раздавать копию, параллельно инициализируя обновление. Даже без этого заголовка вы можете имитировать поведение, подобрав TTL и включив фоновые апдейты.

Cache lock: защита от шторма холодных промахов

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

proxy_cache_lock on;
proxy_cache_lock_timeout 10s;  # сколько ждать лидера
proxy_cache_lock_age 60s;      # не ставить лок на объекты старше указанного возраста

Комбинация cache lock + stale‑while‑revalidate закрывает оба кейса: холодные промахи и горячие апдейты.

stale‑if‑error: устойчивость при сбоях бэкенда

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

proxy_cache_use_stale error timeout invalid_header http_500 http_502 http_503 http_504 updating;

Так вы выровняете SLA даже при кратковременных авариях на стороне внешнего сервиса.

FastFox VDS
Облачный VDS-сервер
Виртуальные серверы с быстрым запуском и гибкой конфигурацией от 390₽ / мес
Доступные локации
Россия Нидерланды Финляндия

Revalidate по ETag/Last‑Modified: экономим трафик и CPU апстрима

Если апстрим умеет ETag и/или Last-Modified, Nginx может слать условные запросы (If-None-Match/If-Modified-Since) вместо полного перезапроса. Это включает proxy_cache_revalidate on;. При 304 кэш продлевает TTL значительно дешевле полного рендеринга на стороне бэкенда.

Продвинутый пример: публичное API и приватные эндпоинты

upstream backend_api {
    server 10.0.0.10:8080;
    keepalive 64;
}

map $request_method $cache_method_ok {
    default 0;
    GET 1;
    HEAD 1;
}
map $http_authorization $auth_present { default 1; "" 0; }
map $cookie_session $sess_present { default 1; "" 0; }
map "$cache_method_ok$auth_present$sess_present" $skip_cache { default 1; "100" 0; }

server {
    listen 80;
    server_name api.example.local;

    # Публично-агрегирующие эндпоинты (кэшируем агрессивно)
    location /v1/catalog/ {
        proxy_pass http://backend_api;
        proxy_cache api_cache;
        proxy_cache_key "$request_method|$host|$request_uri";
        proxy_no_cache $skip_cache;
        proxy_cache_bypass $skip_cache;

        proxy_cache_valid 200 10m;
        proxy_cache_valid 404 1m;
        proxy_cache_background_update on;
        proxy_cache_use_stale updating error timeout http_500 http_502 http_503 http_504;
        proxy_cache_lock on;
        proxy_cache_lock_timeout 8s;
        proxy_cache_lock_age 60s;
        proxy_cache_revalidate on;
        add_header X-Cache-Status $upstream_cache_status always;
    }

    # Приватные эндпоинты — без кэша
    location /v1/user/ {
        proxy_pass http://backend_api;
        proxy_no_cache 1;
        proxy_cache_bypass 1;
    }
}

Такое разделение часто покрывает 80% трафика кэшем и оставляет чувствительные части API нетронутыми.

Особенности API: POST, тело запроса, GraphQL

Технически Nginx умеет кэшировать POST при указании proxy_cache_methods GET HEAD POST. Но это безопасно только если серверная семантика действительно идемпотентна и контент определяется строго телом запроса. Тогда ключ кэша нужно расширить, например, хешом тела:

proxy_cache_methods GET HEAD POST;
set $req_body_hash "";
if ($request_method = POST) { set $req_body_hash $request_body; }
proxy_cache_key "$request_method|$host|$request_uri|$req_body_hash";

Минусы: рост RAM/CPU на прокси, потенциальные лимиты на размер тела, сложнее отлаживать. Для большинства API проще оставить кэш только для GET/HEAD.

Кэширование POST в Nginx с хешем тела запроса

Границы ответственности: Vary, компрессия, клиентские директивы

Nginx корректно поддерживает Vary, храня отдельные варианты. Если вы включили gzip/brotli, убедитесь, что клиенты получают совместимый вариант (обычно достаточен gzip_vary on; и согласованный набор компрессии на прокси и апстриме). Подробно о нюансах статики, WebP/AVIF и заголовков Vary — смотрите материал здесь.

Клиентские заголовки типа Cache-Control: no-cache касаются клиентских кэшей; на прокси они не обязаны сбрасывать серверный кэш — это решается вашими правилами proxy_cache_bypass.

Надёжность и таймауты к внешним бэкендам

В кэширующем прокси важно выставить честные таймауты и стратегию переключений, иначе кэш не спасёт от хрупкости апстрима. Практика:

  • proxy_connect_timeout 2–5s, proxy_read_timeout 10–20s для API;
  • proxy_next_upstream error timeout http_500 http_502 http_503 http_504 при работе с пулом апстримов;
  • keepalive в апстриме, чтобы снизить TLS/три рукопожатия.

Кэш — не замена ретраям и таймаутам. Он сглаживает пик и экономит апстрим, но не чинит логические ошибки на бэкенде.

Инвалидация: как жить без purge

В open-source Nginx нет штатного purge по ключу. Рабочие варианты:

  • Версионирование URI: добавляете параметр ?v=timestamp или инкремент версии при релизах.
  • Короткие TTL + revalidate: быстрые апдейты через условные запросы.
  • Раздельные зоны кэша под разные сервисы и удаление каталога целиком вне пиков (осторожно и осознанно).

Если нужен точечный purge по тегам/ключам — закладывайте это в архитектуру API (контент‑версии, ключи по бизнес‑объекту, отдельные namespace в proxy_cache_path).

Наблюдаемость: метрики и отладка

Минимум — заголовок X-Cache-Status (MISS/HIT/EXPIRED/STALE/UPDATING и т.д.). Для аналитики удобно вынести отдельный log_format с $upstream_cache_status, временем ответа и кодом. В тестах пользуйтесь curl -I и инструментами нагруженного тестирования, чтобы увидеть эффект background update и cache lock.

Типовые пресеты для API

  • Публичные справочники: proxy_cache_valid 200 10m, background_update on, use_stale updating error timeout.
  • Каталоги/поиск с частыми обновлениями: 200 1m + revalidate on + доверие ETag.
  • Персональные данные: proxy_cache_bypass 1 всегда.
  • Внешние SaaS с квотами: короткие TTL, агрессивный stale-if-error, честные таймауты, cache lock.

Чек-лист перед продом

  • Определите кэшируемые эндпоинты и явно запретите кэш там, где риск утечки данных неприемлем.
  • Согласуйте TTL: от апстрима (заголовки) или от прокси (директивы), либо гибрид.
  • Включите proxy_cache_background_update, proxy_cache_use_stale updating, proxy_cache_lock — это три кита «умного» кэша.
  • Проверьте ETag/Last-Modified и включите proxy_cache_revalidate.
  • Убедитесь, что ответы с Set-Cookie не попадают в кэш.
  • Добавьте X-Cache-Status и графики по кэш-хитам/промахам.

Итог

Умный кэш Nginx для API — это не только proxy_cache и TTL. Это связка фоновых апдейтов (stale‑while‑revalidate), устойчивость на ошибках (stale‑if‑error через use_stale), защита от шторма холодных промахов (cache lock) и корректная сегментация ключей. На практике такая конфигурация даёт 5–20x разгрузку внешних бэкендов, сокращает p95/p99 и делает систему заметно спокойнее под пиками.

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

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

Как безопасно выполнить bootstrap MariaDB Galera после полного останова

Как безопасно выполнить bootstrap MariaDB Galera после полного останова

После одновременной остановки всех узлов Galera обычного запуска MariaDB недостаточно. Сначала найдите сервер с последней транзакц ...
CoreDNS в Kubernetes: исправляем CrashLoopBackOff, SERVFAIL и DNS-петлю

CoreDNS в Kubernetes: исправляем CrashLoopBackOff, SERVFAIL и DNS-петлю

Разберём, почему CoreDNS перезапускается или отвечает SERVFAIL, как обнаружить замкнутую пересылку DNS-запросов, проверить доступн ...
Apache Kafka KRaft на VDS: отдельные controller и broker

Apache Kafka KRaft на VDS: отдельные controller и broker

Соберём новый Kafka-кластер без ZooKeeper на шести Linux VDS: три узла controller образуют отказоустойчивый metadata quorum, а три ...