lenec ru

← все посты

SSRF атаки: как защитить бэкенд от server-side request forgery

11K

Дефолтный nginx обрабатывает тысячи запросов в секунду. Но когда трафик растёт до десятков тысяч RPS, дефолты начинают мешать: соединения к upstream пересоздаются на каждый запрос, буферы слишком маленькие, сжатие отключено. Разберём ключевые параметры, которые превращают nginx из «работает» в «летает».

worker_processes и worker_connections

Два параметра, определяющие потолок concurrency:

# /etc/nginx/nginx.conf
worker_processes auto;          # = количество CPU ядер
worker_rlimit_nofile 65535;     # лимит открытых файлов на worker

events {
    worker_connections 16384;   # макс. соединений на один worker
    multi_accept on;            # принимать все новые соединения разом
    use epoll;                  # Linux: epoll эффективнее select/poll
}

Формула максимальной concurrency: worker_processes × worker_connections. При 4 ядрах и 16384 connections — до 65536 одновременных соединений.

Важно: worker_rlimit_nofile должен быть больше worker_connections, потому что каждое проксированное соединение — это 2 файловых дескриптора (клиент + upstream). Также проверьте системный лимит:

# Проверить текущий лимит
ulimit -n

# Установить в /etc/security/limits.conf
nginx soft nofile 65535
nginx hard nofile 65535

Буферы: proxy_buffer_size, client_body_buffer_size

Nginx буферизирует ответы от upstream в памяти. Если буфер мал — данные сбрасываются на диск, что убивает latency:

http {
    # Буфер для заголовков ответа upstream
    proxy_buffer_size 16k;

    # Буферы для тела ответа (количество × размер)
    proxy_buffers 8 32k;
    proxy_busy_buffers_size 64k;

    # Буфер для тела запроса клиента
    client_body_buffer_size 16k;
    client_max_body_size 10m;

    # Буферы для больших заголовков запроса (cookies, JWT)
    large_client_header_buffers 4 16k;
}

Правила подбора:

  • API с JSON-ответами до 32KB — дефолты достаточны
  • Ответы 100KB+ (отчёты, списки) — увеличьте proxy_buffers до 16 64k
  • Большие cookies/JWT — поднимите large_client_header_buffers
  • Загрузка файлов — client_body_buffer_size = типичный размер файла

Keepalive к upstream

По умолчанию nginx открывает новое TCP-соединение к backend на каждый запрос. При 10k RPS это 10k TCP handshake в секунду — бессмысленная нагрузка:

upstream backend {
    server 10.0.1.10:8080;
    server 10.0.1.11:8080;

    keepalive 64;               # пул постоянных соединений
    keepalive_requests 1000;    # запросов на одно соединение
    keepalive_timeout 60s;      # таймаут простоя
}

server {
    location /api/ {
        proxy_pass http://backend;
        proxy_http_version 1.1;                # обязательно для keepalive
        proxy_set_header Connection "";         # убрать "close"
        proxy_connect_timeout 5s;
        proxy_read_timeout 30s;
    }
}

keepalive 64 — это не максимум соединений, а размер пула idle-соединений на каждый worker. При 4 workers и keepalive 64 — до 256 постоянных соединений к backend. Подбирайте под реальный RPS: слишком мало — соединения всё равно пересоздаются, слишком много — держите лишние сокеты.

Gzip vs Brotli: настройка сжатия

Сжатие уменьшает трафик на 60-80% для текстовых ответов:

http {
    # Gzip (встроен)
    gzip on;
    gzip_comp_level 4;          # баланс CPU/сжатие (1-9)
    gzip_min_length 256;        # не жать мелкие ответы
    gzip_vary on;
    gzip_proxied any;
    gzip_types
        text/plain
        text/css
        text/javascript
        application/json
        application/javascript
        application/xml
        image/svg+xml;

    # Brotli (модуль ngx_brotli, ставится отдельно)
    brotli on;
    brotli_comp_level 4;
    brotli_types text/plain text/css application/json
                 application/javascript text/javascript
                 image/svg+xml;
}

Brotli даёт на 15-25% лучшее сжатие при том же CPU, но поддерживается только через HTTPS. Стратегия: brotli для браузеров (Accept-Encoding: br), gzip как fallback.

gzip_comp_level 4 — оптимум. Уровни 6-9 дают +2-3% сжатия при двойном расходе CPU.

Кэширование: proxy_cache

Кэш на nginx снимает нагрузку с backend для повторяющихся запросов:

http {
    proxy_cache_path /var/cache/nginx
        levels=1:2
        keys_zone=app_cache:32m   # 32MB под ключи (~250k записей)
        max_size=1g               # макс. размер на диске
        inactive=10m              # удалять после 10 мин без обращений
        use_temp_path=off;

    server {
        location /api/catalog {
            proxy_pass http://backend;
            proxy_cache app_cache;
            proxy_cache_key "$request_method|$uri|$args";
            proxy_cache_valid 200 5m;
            proxy_cache_valid 404 1m;

            # Bypass кэша по заголовку
            proxy_cache_bypass $http_x_no_cache;

            # Отдавать stale при ошибках backend
            proxy_cache_use_stale error timeout updating
                                  http_500 http_502 http_503;

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

Заголовок X-Cache-Status показывает HIT/MISS/EXPIRED — незаменим для отладки. proxy_cache_use_stale — страховка: если backend упал, отдаём устаревший кэш вместо 502.

Мониторинг: stub_status, логи latency, open_file_cache

Без метрик оптимизация — гадание. Минимальный набор:

# stub_status — базовые счётчики
server {
    listen 127.0.0.1:8080;
    location /nginx_status {
        stub_status;
        allow 127.0.0.1;
        deny all;
    }
}

# Логи с upstream latency
log_format perf '$remote_addr $request_method $uri '
               'status=$status rt=$request_time '
               'uct=$upstream_connect_time '
               'urt=$upstream_response_time '
               'cs=$upstream_cache_status';

access_log /var/log/nginx/perf.log perf;

# Кэш метаданных файлов (статика)
open_file_cache max=10000 inactive=60s;
open_file_cache_valid 30s;
open_file_cache_min_uses 2;

$upstream_response_time — время ответа backend. Если оно растёт — проблема не в nginx. $request_time — полное время обработки запроса. Разница между ними — overhead самого nginx (обычно <1ms).

open_file_cache кэширует дескрипторы и метаданные файлов. Для серверов со статикой (тысячи файлов) это убирает лишние stat() syscalls.

Итого: keepalive к upstream, правильные буферы, сжатие и кэш — четыре рычага, которые дают 3-5x прирост throughput без замены железа. Начните с stub_status и логов latency, найдите узкое место, крутите один параметр за раз.

Комментарии 0

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.