Top.Mail.Ru
OSEN-НИЙ SAAALEСкидка 50% на виртуальный хостинг и VDS
до 30.11.2025 Подробнее
Выберите продукт

Умный кэш 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-сервер в России
Аренда виртуальных серверов с моментальным развертыванием инфраструктуры от 195₽ / мес

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 и делает систему заметно спокойнее под пиками.

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

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

Hardening сервисов на VDS: sandbox опции systemd — ProtectSystem, PrivateTmp, CapabilityBoundingSet OpenAI Статья написана AI Fastfox

Hardening сервисов на VDS: sandbox опции systemd — ProtectSystem, PrivateTmp, CapabilityBoundingSet

Как ограничить права Linux‑сервисов на VDS с помощью sandbox‑опций systemd. Разбираем ProtectSystem, PrivateTmp, CapabilityBoundin ...
Память на малом VDS без сюрпризов: swap, zram, vm.overcommit и OOM‑killer на практике OpenAI Статья написана AI Fastfox

Память на малом VDS без сюрпризов: swap, zram, vm.overcommit и OOM‑killer на практике

Малый VDS часто упирается в память: PHP‑FPM, базы, кэш и воркеры делят считанные гигабайты. Ошибка Killed в логах и зависания — пр ...
Wildcard DNS и превью‑стенды: поддомены на каждую ветку Git через Nginx map на VDS OpenAI Статья написана AI Fastfox

Wildcard DNS и превью‑стенды: поддомены на каждую ветку Git через Nginx map на VDS

Показываю рабочую схему превью‑стендов: одна VDS, wildcard DNS на dev‑домен, Nginx с map и скрипты, поднимающие приложения на уник ...