Прямой ответ
Nginx перед приложением — первый рубеж, где три задачи решаются дешевле, чем в коде: rate limit (limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s + limit_req zone=api burst=20 nodelay — грубая защита приложения от скриптов до того, как запрос дойдёт до Python — стратегия лимитов), прокси-кеш (proxy_cache — тяжёлые анонимные страницы отдаются из кеша nginx, приложение разгружается в разы; ключ — cache key без кук для публичного контента), статика напрямую (root/alias + expires + gzip/brotli — браузерный кеш снимает повторные запросы вовсе). Порядок внедрения по эффекту: сначала статика и expires (нулевой риск, мгновенный выигрыш на трафике), потом кеш публичных страниц (осторожно с приватным контентом — кеш по ключу с кукой отдаст чужую страницу), лимиты — параллельно как гигиена. И обязательная проверка каждого рычага: curl -I — заголовки (X-Cache-Status, Cache-Control, Content-Encoding), метрики hit-ratio кеша — иначе «включили кеш» живёт как декларация, а не как работающий слой.
Rate limit: две директивы и их нюансы
Механика: limit_req_zone (в http-блоке) объявляет зону — ключ ($binary_remote_addr — IP, или $http_authorization/$cookie_session для пер-пользовательских), размер памяти (10m ≈ 160k состояний) и rate (средняя скорость: 10r/s — один запрос в 100 мс); limit_req (в location) подключает зону с burst (очередь всплеска: burst=20 — до 20 «лишних» запросов задерживаются, без nodelay — с паузами, с nodelay — пропускаются сразу, но счётчик растёт; nodelay — почти всегда то, что нужно людям) и статусом отказа (limit_req_status 429 — дефолт 503 вводит в заблуждение клиентов). Практика: разные зоны для разных зон риска (zone=login:10m rate=5r/m — строже общих — дифференциация); исключения для своего мониторинга/офиса (geo $limit { default 1; 10.0.0.0/8 0; } map $limit $limit_key { 1 $binary_remote_addr; 0 ""; } — пустой ключ = без лимита); логи отказов отдельным форматом (кто упирается — бот или реальные пользователи за NAT — решение по данным); понимание границ — лимит по IP за CDN видит один адрес CDN (реальный клиент — из X-Forwarded-For с доверенной настройкой — прокси-основы).
Прокси-кеш: разгрузка приложения
Конфигурация: proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=pages:100m max_size=2g inactive=60m; + в location: proxy_cache pages; proxy_cache_key "$scheme$request_uri"; proxy_cache_valid 200 5m; add_header X-Cache-Status $upstream_cache_status;. Решающие детали: cache key — $request_uri без $http_cookie для публичных страниц (кука в ключе = персональный кеш на каждого посетителя = hit-ratio около нуля и раздувание кеша); обход кеша для авторизованных (proxy_cache_bypass $cookie_session + proxy_no_cache $cookie_session — залогиненные идут мимо кеша в приложение); stale-отдача (proxy_cache_use_stale error timeout updating — при упавшем бэкенде отдаётся устаревшая страница вместо 502 — дешёвая отказоустойчивость); инвалидация — временем (valid 5m) или PURGE-методом с ACL (сложнее; начинать с TTL). Что кешировать: анонимные тяжёлые страницы (каталог, статьи, отчёты), API-справочники; что нельзя: персональные данные, результаты POST, всё с Set-Cookie от бэкенда (проверить: бэкенд не должен слать куки на кешируемых ответах). Метрика здоровья — X-Cache-Status в логах: HIT-доля на публичных маршрутах должна быть высокой; MISS-поток — разбираться (ключ/куки/TTL).
Статика: expires, сжатие, HTTP-кеш браузера
Отдача статики самим nginx (location /static/ { root /var/www/app; expires 30d; add_header Cache-Control "public, immutable"; }) — запросы не доходят до приложения вовсе. Три уровня браузерного кеша: expires/Cache-Control — «не спрашивай сервер N дней» (для версионируемых ассетов app.3f2a1c.js — immutable навсегда: новая сборка = новое имя — фронтенд-сборки так умеют); ETag/Last-Modified — «спроси, изменилось ли» (304 без тела — для неверсионируемого: favicon, robots); сжатие — gzip on; gzip_types text/css application/javascript application/json; gzip_min_length 1024; (или brotli-модуль — лучше на тексте; CPU против трафика — почти всегда выгодно; картинки/woff2 не жмём — уже сжаты). Проверка: curl -sI | egrep -i 'cache|etag|content-encoding' — все три слоя видны в одном выводе; повторный запрос с If-None-Match — 304 подтверждает conditional-логику. Частая ошибка: expires на HTML-страницы (пользователь сидит на устаревшей странице часами) — HTML — no-cache (проверка каждый раз), длинные expires — только ассетам.
Практический сценарий: ускорение медленного каталога
Жалоба: страница каталога грузится 3 секунды под нагрузкой, backend (Python) на 90% CPU. Разбор: (1) curl -w time_starttransfer — 2.8 с до первого байта (backend, не сеть); (2) в ответе — тяжёлая HTML-выборка + 40 картинок; картинки идут через приложение (!) — первая правка: location /media/ { root ...; expires 7d; } — трафик картинок мимо Python (CPU −15% сразу); (3) анонимная страница каталога одинакова для всех — proxy_cache c TTL 2 минуты и ключом по URI+query — после прогрева hit-ratio 94% (метрика по логам), CPU backend −70%, время ответа для кеша — 15 мс; (4) залогиненные — bypass по куке сессии (персональные цены — из приложения); (5) limit_req на поисковый эндпоинт (самый дорогой — rate=5r/s на IP) — скрейпер, приходивший ночью, упирается в 429 и уходит; (6) gzip для JSON-API (−70% трафика ответов). Итог: три рычага nginx сняли нагрузку, которая «требовала переписывания каталога»; приложение ускорено отдельно (индексы для miss-ветки), но авария закрыта за вечер конфигурацией. Контроль после: дашборд Grafana — hit-ratio, 429-поток, время ответа backend.
FAQ
Кеш nginx против Redis-кеша в приложении? Разные слои: nginx-кеш — готовый HTTP-ответ целиком (дешевле всего, но грубее — только публичное), Redis — данные/фрагменты внутри приложения (гибко, персонализировано). Работают вместе: nginx снимает публичный трафик, Redis ускоряет miss-ветку в приложении (паттерны).
Почему кеш «не работает» (всё MISS)?
Топ причин: кука/Set-Cookie в ключе или ответе (проверить curl -I бэкенда), разные query-параметры у одного контента (UTM-метки плодят ключи — нормализация ключа), TTL истёк между запросами, proxy_cache_bypass срабатывает шире задуманного; диагностика — X-Cache-Status + proxy_cache_key в debug-лог.
Как инвалидировать кеш при деплое? Если контент версионируется URL — никак (новая версия = новый контент по TTL); иначе — purge-эндпоинг (nginx с PURGE-методом из ACL «только с деплой-сервера») или полная очистка каталога кеша на деплой (грубо, но предсказуемо — прогрев после); «кеш живёт своей жизнью» — признак отсутствия политики инвалидации.
Стоит ли включать http2/server push?
HTTP/2 — да (мультиплексирование статики без костылей — listen 443 ssl http2); server push — нет (депрекейтнут в браузерах, эффект достигался 103 Early Hints и preload-ссылками — push-механика себя не оправдала).
Источники: документация nginx (limit_req, proxy, gzip) — ссылки в шапке.