Зачем кэшировать S3 через Nginx, если S3 и так быстрый
Объектное хранилище по S3-протоколу хорошо подходит для раздачи статических файлов, но в реальном проде часто всплывают нюансы: лишняя задержка из‑за географии, рост стоимости egress при всплесках трафика, лимиты по запросам, а также эффект «стада» (cache stampede), когда после истечения TTL десятки или сотни клиентов одновременно пробивают origin.
nginx proxy_cache позволяет поставить перед S3 локальный edge‑кэш на диске: Nginx отдаёт популярную статику с вашего сервера, а в S3 ходит только при промахе, при revalidation или при прогреве. Это удобно, когда несколько сайтов/приложений используют общий bucket, или вы хотите жёстко контролировать TTL, заголовки и поведение при сбоях.
Ниже соберём практичную конфигурацию: S3 как origin, Nginx как reverse proxy со стабильным cache key, корректной revalidation по ETag/Last-Modified, защитой от stampede через cache lock и предсказуемым stale-поведением.
Архитектура и границы ответственности
Базовая схема: клиент → Nginx → S3. Nginx кэширует ответы (чаще всего 200, иногда редиректы, и в некоторых случаях часть 404), а при истечении TTL может «перепроверять» объект в S3 условными запросами (If-None-Match / If-Modified-Since).
Перед настройкой стоит договориться с самим собой о нескольких вещах:
- Что именно кэшируем: картинки, CSS/JS, шрифты, архивы, media-сегменты. Для динамики кэш тоже возможен, но это отдельный класс задач.
- Как инвалидируем: лучший вариант — версионирование в URL (например,
app.9f3a1c.js), альтернативный — revalidation по заголовкам. - Как различаем варианты: по host, пути, query string, иногда по заголовкам (например,
Accept-Encoding). - Что делаем при сбоях origin: отдаём stale или честно возвращаем ошибку.
Практическое правило: для статики проще всего жить с «immutable + versioned URLs». Тогда кэш можно держать долго, а revalidation понадобится редко. Если URL не версионирован, ставка на revalidation становится критичной.
Если вы раздаёте статику с отдельного домена, заранее проверьте, что на нём корректно настроены DNS и сертификат. Для собственных доменов удобна регистрация доменов, а для HTTPS — подходящие SSL-сертификаты.

Базовая конфигурация proxy_cache: зона, путь, ключ
Фундамент начинается в контексте http: объявляем путь кэша, размер метаданных, лимиты и политику очистки.
proxy_cache_path /var/cache/nginx/s3 levels=1:2 keys_zone=s3cache:200m max_size=50g inactive=7d use_temp_path=off;
proxy_temp_path /var/cache/nginx/tmp;
Ключевые параметры:
keys_zone=s3cache:200m— память под метаданные (ключи/индекс), а не под сами файлы. Если зона мала, кэш будет чаще «забывать» объекты.max_size=50g— жёсткий потолок по диску под кэш.inactive=7d— если объект не запрашивали 7 дней, его можно удалить при очистке.use_temp_path=off— запись сразу в итоговую структуру кэша (часто меньше лишнего I/O).
Дальше — серверный блок. Ниже пример, где origin — HTTPS endpoint S3 (или S3‑совместимого хранилища). Обратите внимание на Host, keepalive и диагностический заголовок.
server {
listen 80;
server_name static.example.com;
resolver 1.1.1.1 1.0.0.1 valid=300s;
resolver_timeout 5s;
location / {
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host bucket-name.s3-region.amazonaws.com;
proxy_hide_header x-amz-id-2;
proxy_hide_header x-amz-request-id;
proxy_cache s3cache;
proxy_cache_key "$scheme$proxy_host$uri$is_args$args";
proxy_cache_valid 200 206 1h;
proxy_cache_valid 301 302 10m;
proxy_cache_valid 404 5m;
proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504;
add_header X-Cache-Status $upstream_cache_status always;
proxy_pass https://bucket-name.s3-region.amazonaws.com;
}
}
Самый важный момент здесь — proxy_cache_key. В примере ключ включает query string. Это безопасно по корректности, но может резко раздуть кэш, если в URL летят «мусорные» параметры (например, utm_*).
Query string и «размножение» объектов
Для static caching обычно верно одно из двух:
- query string не используется — тогда убирайте
$argsиз ключа и кэш станет гораздо компактнее; - query string — это версия (например,
?v=123) — тогда оставляйте.
Если хотите оставить только «разрешённые» параметры (например, только v), делайте это через map. В отдельной заметке мы разбирали похожую технику на примере картинок и форматов: как нормализовать ключ кэша через map.
Revalidation: проверка свежести по ETag и Last-Modified
Revalidation нужна, когда вы не можете (или не хотите) полностью полагаться на версионирование URL, и вам важно, чтобы Nginx после истечения TTL перепроверял объект в S3 условным запросом. В идеале origin отдаёт ETag и/или Last-Modified, а Nginx использует их для If-None-Match/If-Modified-Since.
Включается это директивой proxy_cache_revalidate. Когда TTL истёк, Nginx попытается получить 304 Not Modified вместо полной перекачки тела.
location / {
proxy_cache s3cache;
proxy_cache_valid 200 206 10m;
proxy_cache_revalidate on;
proxy_pass https://bucket-name.s3-region.amazonaws.com;
}
Три практических наблюдения:
- 304 экономит трафик, но это всё равно запрос в S3. При высокой нагрузке доля revalidation-запросов может быть заметна по RPS.
- ETag в S3 не всегда равен хэшу (особенно при multipart upload). Для revalidation это обычно не важно: вам нужна стабильная «версия», а не криптография.
- Last-Modified часто достаточно, если вы обновляете объект перезаписью. Но если у вас нестандартный пайплайн (копирование с метаданными, специфические заголовки), проверьте поведение на тестовом bucket.
Cache lock: защита от stampede при истечении TTL
Stampede проявляется в момент, когда популярный объект протух: множество клиентов одновременно получают EXPIRED и начинают пробивать origin. Для S3 это означает всплеск запросов и задержек.
Решение — включить proxy_cache_lock. Тогда один запрос обновляет объект, а остальные ждут результат или получают stale (если вы так настроите).
location / {
proxy_cache s3cache;
proxy_cache_valid 200 206 10m;
proxy_cache_lock on;
proxy_cache_lock_timeout 10s;
proxy_cache_lock_age 30s;
proxy_cache_use_stale updating;
add_header X-Cache-Status $upstream_cache_status always;
proxy_pass https://bucket-name.s3-region.amazonaws.com;
}
Как это читать:
proxy_cache_lock— включает «замок» на ключ кэша.proxy_cache_lock_timeout— сколько клиент готов ждать lock; после таймаута Nginx может пойти в origin сам (и тем самым частично вернуть stampede), поэтому подбирайте аккуратно.proxy_cache_lock_age— страховка от зависшего lock (например, при проблемах с origin).
Связка proxy_cache_lock + proxy_cache_use_stale updating обычно даёт лучший UX: один запрос обновляет, остальные мгновенно получают старую версию без ожидания.
Stale и background update: как переживать сбои S3 и сеть
Если S3 endpoint на секунду «подвис» или сеть дала потери, превращать это в лавину 502/504 на клиенте чаще всего не хочется. Для этого Nginx умеет отдавать протухший кэш в заданных сценариях.
location / {
proxy_cache s3cache;
proxy_cache_valid 200 206 30m;
proxy_cache_use_stale error timeout invalid_header http_500 http_502 http_503 http_504;
proxy_cache_background_update on;
proxy_pass https://bucket-name.s3-region.amazonaws.com;
}
proxy_cache_background_update означает: клиенту можно отдать stale сразу, а обновление выполнить в фоне. Это снижает хвосты latency и делает поведение более «CDN‑похожим».
Для статики отдача stale при ошибках почти всегда лучше, чем «честная» 502. Исключение — когда устаревшая версия критична (например, юридически значимые файлы) и обновления происходят часто без версионирования.
Range/206 и большие файлы: что учитывать
S3 часто используют для видео, архивов и образов, где клиенты активно делают Range‑запросы. Nginx умеет кэшировать ответы 206 Partial Content, но у этого есть цена: один и тот же объект может храниться «кусочками», увеличивая количество файлов в кэше и нагрузку на диск.
Если Range‑трафика много, имеет смысл отдельно протестировать:
- нужно ли кэшировать
206вообще или достаточно кэшировать только200; - каковы реальные паттерны клиентов (плееры, докачки, распараллеливание);
- как быстро растёт кэш по числу объектов и inode.
Тему Range и кэширования больших файлов мы подробно разбирали отдельно: как Nginx ведёт себя с Range и кэшем.

Диагностика: как быстро понять, что кэш работает
Первое, что стоит добавить — заголовок со статусом кэша:
add_header X-Cache-Status $upstream_cache_status always;
Типовые значения $upstream_cache_status:
- HIT — отдали из кэша.
- MISS — сходили в origin и закэшировали.
- BYPASS — кэш намеренно пропущен (обычно из‑за условий
proxy_cache_bypass). - EXPIRED — объект протух и пошёл обновляться (часто в паре с revalidation).
- UPDATING — кто-то обновляет, вы получили stale (при
proxy_cache_use_stale updating).
Второй слой — логи. Удобно иметь отдельный log_format для статического прокси, чтобы видеть кэш‑статус, upstream‑статус и тайминги.
log_format s3cache '$remote_addr - $host [$time_local] "$request" $status '
'cache=$upstream_cache_status upstream=$upstream_status '
'rt=$request_time urt=$upstream_response_time '
'uaddr=$upstream_addr';
И применить формат в нужном server:
access_log /var/log/nginx/static-access.log s3cache;
Так вы быстро поймёте, почему вдруг всё стало MISS (ключ «гуляет»), почему слишком много EXPIRED (TTL маловат), и помогает ли revalidation (доля 304 у upstream).
Тонкости S3 origin: Host, SNI и соединения
Для S3‑совместимых endpoint’ов критично корректно задавать Host, потому что маршрутизация часто завязана на виртуальные хосты. Поэтому в примерах есть proxy_set_header Host …. Если провайдер требует path‑style (bucket в пути), меняется только способ формирования URL в proxy_pass, но принцип тот же: запрос должен выглядеть так, как ожидает S3.
Для стабильности соединений полезно:
- использовать
proxy_http_version 1.1иproxy_set_header Connection "", чтобы keepalive работал предсказуемо; - выставить разумные
proxy_connect_timeoutиproxy_read_timeout(особенно для больших объектов); - задать
resolverв Nginx, если origin — доменное имя и вы хотите управлять TTL резолва.
Частые ошибки и быстрые проверки
Кэш есть, но всегда MISS
Проверьте, не меняется ли ключ: $proxy_host, $uri, $args. Частая причина — мусорные query‑параметры или разные варианты по заголовкам (например, сжатие), которые не учтены одинаково для всех запросов.
Кэш разрастается без контроля
Типовые причины: query string в ключе, кэширование 206 для крупных файлов, слишком большой inactive при большом «ассортименте» объектов. Отдельно следите за inode на файловой системе, а не только за гигабайтами.
Revalidation не даёт экономии
Если объекты часто меняются без версионирования URL, условные запросы будут часто возвращать 200 вместо 304. Смотрите в логах $upstream_status и долю EXPIRED. Иногда проще изменить процесс деплоя статики (versioned filenames), чем пытаться «умно» revalidate’ить всё.
При истечении TTL растёт latency
Почти всегда это лечится включением proxy_cache_lock и proxy_cache_use_stale updating. Это сглаживает нагрузку на origin и убирает «хвосты» задержек.
Сбалансированный пресет для статики с S3
Если нужно быстро получить предсказуемый результат, начните с такого набора (при условии, что статика не зависит от query string):
proxy_cache s3cache;
proxy_cache_key "$scheme$proxy_host$uri";
proxy_cache_valid 200 206 30m;
proxy_cache_valid 404 2m;
proxy_cache_revalidate on;
proxy_cache_lock on;
proxy_cache_lock_timeout 10s;
proxy_cache_lock_age 30s;
proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504;
proxy_cache_background_update on;
add_header X-Cache-Status $upstream_cache_status always;
Логика простая: не раздуваем кэш $args, включаем revalidation для «мягкого» обновления, добавляем lock+stale updating от stampede и background update для стабильной задержки.
Финальный чек‑лист перед продом
- Проверьте права и место на диске под
/var/cache/nginx, настройте мониторинг заполнения и inode. - Добавьте
X-Cache-Statusи отдельный лог‑формат хотя бы на время внедрения. - Решите вопрос с query string: исключить, оставить или нормализовать через
map. - Включите
proxy_cache_lockиproxy_cache_use_stale updatingдля горячих объектов. - Если обновления должны быть строгими — внедрите версионирование URL на стороне сборки/деплоя.
При аккуратной настройке связка Nginx + S3 даёт поведение, близкое к CDN: быстрые HIT, мягкие обновления через revalidation и предсказуемую нагрузку на origin за счёт cache lock.


