В прошлом месяце мы разбирали проблему клиента с SaaS-бэкендом: VPS 2 vCPU / 4 ГБ, CPU стабильно около 20 %, а каждый день в 15:00 сайт превращался в слайд-шоу. На графиках облака исходящая полоса упиралась в потолок — 100 Мбит/с насыщались сорок минут подряд. Первая мысль: «Пора апгрейдить». Мы открыли access-лог Nginx: 70 % трафика — несжатые JSON-ответы API и статический JS с случайным ?v= в URL; одни и те же ресурсы отдавались целиком снова и снова.
После Gzip, заголовков кэша для статики и upstream keepalive пик упал до 35 Мбит/с, загрузка — с 8 до 1,2 с, без лишних расходов. На небольших VPS это типично: узкое место — исходящая полоса, а не CPU. Nginx как reverse proxy между пользователями и бэкендом — лучший рычаг экономии трафика, если знать, какие «винты» крутить.
Это не гайд по установке Nginx, а чеклист оптимизации под полосу пропускания, проверенный в продакшене: диагностика, сжатие, кэш, затем переиспользование соединений и буферизация. Предполагается, что Nginx уже стоит как HTTPS reverse proxy на 443 (если файрвола ещё нет — сначала наш гайд по UFW для облачного сервера: открыть 22/80/443, потом оптимизировать).
Почему упирается полоса, а не CPU?
У облачных VPS тарификация полосы часто асимметрична: небольшой пакет — 100 Мбит/с пик и 1–2 ТБ в месяц; кажется достаточно, но несжатый JSON 500 КБ при 200 req/s теоретически даёт 800 Мбит/с и мгновенно пробивает лимит. Nginx отдаёт статику и простой reverse proxy с низкой загрузкой CPU — отсюда картина «CPU свободен, полоса забита».
Ещё одна ошибка — гнать весь трафик на origin. Без CDN, без кэша браузера, без proxy_cache каждый пользователь при обновлении снова тянет bundle.js и logo.png с VPS. http-модули в официальной документации Nginx по сути помогают передавать меньше байт или открывать меньше соединений — важно комбинировать по сценарию, а не копировать «универсальный» конфиг.
| Симптом | Частая причина | Первое действие |
|---|---|---|
| Медленный API, высокая полоса | JSON/HTML без сжатия | Включить gzip / brotli |
| Большой трафик при обновлении | Статика без Cache-Control | expires + имена с хешем |
| Рост соединений к бэкенду | Новый upstream TCP на запрос | Пул keepalive |
| Крупные загрузки забивают исходящий канал | Прямая отдача с origin, нет сегментного кэша | CDN или proxy_cache |
Шаг 1: сначала диагностика, не слепой апгрейд полосы
Перед правками конфига — десять минут, чтобы понять, куда уходит трафик. На сервере обычно используем:
# Трафик по соединениям в реальном времени (apt install iftop) sudo iftop -i eth0 # Почасовая/дневная статистика (vnstat) vnstat -h vnstat -d # Access-лог Nginx: самые тяжёлые URL awk '{print $7, $10}' /var/log/nginx/access.log | \ awk '{a[$1]+=$2} END {for(i in a) print a[i], i}' | sort -rn | head -20 # Текущие соединения ESTABLISHED ss -s ss -tn state established | wc -l
Если в топе /api/ с телами по сотням КБ — приоритет №1 сжатие. Если .js, .css, .woff2 всегда с кодом 200 (без 304) — не настроены заголовки кэша. Разделяйте входящий и исходящий трафик: DDoS и краулеры забивают вход — другая тема (rate limit, WAF, fail2ban). Здесь — оптимизация исходящего в нормальной нагрузке.
Графики полосы в облачной консоли агрегируются с задержкой 1–5 минут; при разборе ориентируйтесь на iftop на машине. Если тормозит SSH — наш гайд по отказу SSH на Linux-сервере, чтобы исключить сетевой сбой.
Шаг 2: Gzip / Brotli — самый дешёвый способ срезать полосу
Текстовые ответы (JSON, HTML, JS, CSS, SVG, XML) обычно сжимаются на 60–80 %. Встроенный модуль gzip настраивается несколькими строками; нагрузка на CPU на большинстве VPS приемлема.
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 5; # 6–9 дают убывающий эффект, 5 — разумный баланс
gzip_min_length 256; # Мелкие ответы не сжимать
gzip_types
text/plain
text/css
text/javascript
application/javascript
application/json
application/xml
image/svg+xml
font/woff2;
На практике: gzip_proxied any сжимает и проксированный контент с бэкенда (по умолчанию — только прямые ответы). JPEG/PNG/WebP и видео gzip не нужен — бинарь уже сжат, CPU зря. Проверка: curl -H 'Accept-Encoding: gzip' -I https://your.site/api/foo — есть ли Content-Encoding: gzip.
С модулем Brotli (некоторые дистрибутивы или своя сборка) при том же качестве ещё −15–20 % — удобно для текстовых API. Без модуля достаточно стабильного gzip.
Шаг 3: кэш статики и sendfile
Сборщики (Vite, Webpack) добавляют content hash в имя, например app.a3f2b1.js. Такие файлы можно кэшировать надолго — при втором визите сеть не нужна. Часто hash есть в билде, но Nginx не отдаёт Cache-Control, и браузер каждый раз запрашивает всё заново.
location ~* \.(js|css|woff2?|ttf|ico|svg)$ {
root /var/www/app/dist;
expires 30d;
add_header Cache-Control "public, immutable";
access_log off; # При cache hit отключить access_log — меньше IO
}
# Рекомендуется в блоке http
sendfile on;
tcp_nopush on;
tcp_nodelay on;
sendfile передаёт страницы файла из ядра в сокет, меньше копий в userspace — полезно для крупной статики и параллельных загрузок. При связке с AIO или пулами потоков протестируйте; для большинства сайтов хватает sendfile + tcp_nopush + tcp_nodelay.
HTML и index.html без immutable — иначе после деплоя пользователи увидят старую оболочку. Обычно: хешированные ресурсы на 30 дней, HTML с no-cache (согласованное кэширование).
Шаг 4: кэш reverse proxy (proxy_cache)
Для read-heavy API (списки настроек, каталоги, статьи) короткий TTL в Nginx делит полосу origin на hit rate. Одна из недооценённых возможностей модуля proxy.
# блок http proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=apicache:50m max_size=2g inactive=60m use_temp_path=off; upstream backend { server 127.0.0.1:3000; keepalive 32; # см. следующий раздел } server { location /api/ { proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_cache apicache; proxy_cache_valid 200 5m; proxy_cache_key "$scheme$request_method$host$request_uri"; add_header X-Cache-Status $upstream_cache_status; proxy_buffering on; proxy_buffers 16 16k; proxy_busy_buffers_size 64k; } }
$upstream_cache_status показывает HIT, MISS или BYPASS — сразу видно, работает ли кэш. Запись (POST/PUT/DELETE) должна обходить кэш; персонализированные API с cookie — осторожно, чтобы не отдать данные A пользователю B.
При proxy_buffering on Nginx сначала собирает ответ бэкенда в буфер, потом отдаёт клиенту — медленный бэкенд и быстрый клиент не держат соединение зря. Для SSE или крупных upload — proxy_buffering off в нужной location.
Шаг 5: upstream keepalive — меньше TCP-handshake
По умолчанию Nginx может открывать новое TCP к бэкенду на каждый запрос клиента. При высокой QPS TIME_WAIT и трёхсторонние рукопожатия съедают CPU и локальные порты — ответы медленнее, эффективная полоса падает. upstream keepalive держит пул соединений к бэкенду; после ответа соединение возвращается в пул.
upstream backend {
server 127.0.0.1:8080;
keepalive 64; # размер пула по QPS
}
location / {
proxy_pass http://backend;
proxy_http_version 1.1; # HTTP/1.1 нужен для keepalive
proxy_set_header Connection ""; # убрать Connection: close
}
Без proxy_http_version 1.1 или Connection "" keepalive не работает — самый частый случай «настроили зря» у клиентов. keepalive 64 хватает при PV до миллиона в день; слишком большой пул нагружает дескрипторы бэкенда.
Шаг 6: workers, соединения и ограничение скорости
При забитой полосе проверьте, не упирается ли Nginx в лимит соединений. Ключевые параметры в начале nginx.conf:
worker_processes auto;
worker_rlimit_nofile 65535;
events {
worker_connections 4096;
use epoll;
multi_accept on;
}
# Опционально: лимит по IP против монополии полосы
limit_req_zone $binary_remote_addr zone=perip:10m rate=10r/s;
limit_conn_zone $binary_remote_addr zone=conn:10m;
server {
limit_req zone=perip burst=20 nodelay;
limit_conn conn 50;
}
worker_connections × worker_processes должно превышать пик одновременных соединений. На 2 ядрах worker_processes auto обычно даёт 2 — 4096 × 2 = 8192 с запасом для средних сайтов. Согласуйте fs.file-max и worker_rlimit_nofile, иначе в логах too many open files.
limit_rate ограничивает одну загрузку; limit_req защищает API от злоупотреблений. Это не «добавляет» полосу, но не даёт единичным аномалиям положить всех — последний рубеж управления трафиком.
Шаг 7: когда пора подключать CDN?
Если после всех оптимизаций полоса периодически упирается в потолок — CDN или больший тариф. Простые критерии:
- Статика > 60 % исходящего → CDN для JS/CSS/картинок, origin только HTML и API
- Пользователи в разных регионах → edge ближе к аудитории, меньше нагрузка на origin
- Всплески (акции, стримы) → CDN держит пики, не покупать полосу «под максимум» на origin
С CDN на стороне Nginx: origin принимает только IP CDN (whitelist файрвола) и пробрасывает реальный IP клиента (X-Forwarded-For / real_ip). Иначе атакующий обходит защиту и кэш, бью напрямую по IP origin.
Полный пример блока server (подставьте домен и деплойте)
Все фрагменты в минимальный рабочий пример — бэкенд Node / Python на 127.0.0.1:3000:
upstream app {
server 127.0.0.1:3000;
keepalive 32;
}
server {
listen 443 ssl http2;
server_name app.example.com;
ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;
gzip on;
gzip_types application/json application/javascript text/css text/plain;
gzip_min_length 256;
location /assets/ {
alias /var/www/app/dist/assets/;
expires 30d;
add_header Cache-Control "public, immutable";
access_log off;
}
location / {
proxy_pass http://app;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Connection "";
}
}
Затем sudo nginx -t && sudo systemctl reload nginx и вкладка Network в браузере: кэш 30 дней для assets, gzip для API. В нашем release-чеклисте: curl -sI https://app.example.com/assets/main.js | grep -i cache.
Проверка и постоянный мониторинг
Тюнинг — не разовая задача. В Grafana или облачном мониторинге смотрите четыре кривые: исходящая полоса, active connections Nginx, время ответа upstream, hit rate кэша (если есть proxy_cache). Перед релизом или акцией — нагрузочный тест: ab -n 1000 -c 50 https://app.example.com/ или hey -n 5000 -c 100, сравните пик полосы и P95 до/после.
Полоса упала, а задержка высокая — узкое место, скорее всего, БД или диск; другая тема. Но вы уже знаете: деньги в индексы, а не в слепую покупку 500 Мбит/с.
Помимо тюнинга: правильный узел тоже экономит полосу
Nginx уменьшает объём отдаваемых байт, но физическое расстояние пользователь–origin конфигом не сократить. Аудитория в APAC на VPS в US East — высокий RTT, ретрансмиты, ниже реальный throughput. Многие выносят сборку и превью на облачный Mac или ближайший узел, статику — в object storage + CDN, на origin остаётся API — счёт за трафик выглядит лучше.
Облачный Mac mini M4 VPSSPark подходит для CI, внутренних превью и jump-хоста: нативный macOS, Terminal и OpenSSH из коробки; в простое ~4 Вт, удобно 7×24 как релей или build-машина — экономичнее ещё одного маленького x86. С UFW только на 443 и приложениями на 127.0.0.1 меньше поверхность атаки и лишний трафик.
Если проектируете архитектуру с экономией полосы и простой эксплуатацией, облачный Mac mini VPSSPark — практичный low-power jump- и build-узел — посмотреть тарифы — чтобы результат тюнинга Nginx не свёл на нет неверный выбор региона.