Тюнинг Nginx до 50 000 RPS: конфигурация для высоконагруженного e-commerce

Исходная ситуация

Интернет-магазин «ШопМастер» работал на одном сервере с Nginx + PHP-FPM + PostgreSQL. Каталог насчитывал 280 000 товаров, ежедневный трафик — 1.2 миллиона просмотров. В период распродаж сервер не справлялся: Nginx возвращал 502 при 3 000 одновременных подключений, среднее время ответа превышало 4 секунды, а конверсия падала на 40%.

После аудита мы обнаружили типичные проблемы дефолтной конфигурации:

  • worker_processes 1 — на 8-ядерном сервере использовалось одно ядро
  • worker_connections 512 — лимит соединений съедался за секунды
  • Отсутствие gzip — каждая страница весила 800 КБ вместо 180 КБ
  • Нет кэширования статики — каждый запрос к CSS/JS шёл до бэкенда
  • Один upstream без балансировки — PHP-FPM задыхался на 50 воркерах

Настройка воркеров и подключений

Первый шаг — привести количество воркеров в соответствие с аппаратными ресурсами. Каждый worker — однопоточный процесс, привязываемый к отдельному ядру CPU:

# Определяем количество ядер
nproc
# 8

# Или автоматически
worker_processes auto;

# Приоритет процесса (ниже = выше приоритет)
worker_priority -5;

# Лимит дескрипторов файлов на воркер
worker_rlimit_nofile 65536;

# Оптимизация системных вызовов
timer_resolution 100ms;

Блок events — ядро конкурентности Nginx:

events {
    worker_connections 4096;
    use epoll;              # Linux: epoll, FreeBSD: kqueue
    multi_accept on;        # Принимать все подключения за один цикл
    accept_mutex off;       # На 1.11.3+ не нужен — reuseport лучше
}

Теоретический максимум одновременных соединений: worker_processes × worker_connections = 8 × 4096 = 32 768. С учётом keepalive и проксирования (два соединения на запрос) — около 16 000 одновременных клиентов.

Важно поднять лимиты на уровне ОС:

# /etc/security/limits.conf
nginx soft nofile 65536
nginx hard nofile 65536

# /etc/sysctl.conf
net.core.somaxconn = 65536
net.ipv4.tcp_max_tw_buckets = 1440000
net.ipv4.ip_local_port_range = 1024 65535
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_tw_reuse = 1
net.core.netdev_max_backlog = 65536

sysctl -p

Keepalive и управление соединениями

Keepalive позволяет использовать одно TCP-соединение для нескольких HTTP-запросов, экономя время на three-way handshake и TLS-рукопожатии:

http {
    # Клиентские keepalive
    keepalive_timeout 65;
    keepalive_requests 1000;   # Макс. запросов на одно соединение

    # Отключаем Nagle's algorithm для динамики
    tcp_nodelay on;

    # Эффективная передача файлов через sendfile
    sendfile on;
    tcp_nopush on;             # Отправлять заголовки и начало файла одним пакетом

    # Буферы для клиентских запросов
    client_body_buffer_size 16k;
    client_header_buffer_size 1k;
    large_client_header_buffers 4 16k;
    client_max_body_size 20m;

    # Таймауты
    client_body_timeout 12;
    client_header_timeout 12;
    send_timeout 10;
    reset_timedout_connection on;
}

Keepalive к upstream — критически важная настройка, которую часто забывают. Без неё Nginx открывает новое TCP-соединение к бэкенду на каждый запрос:

upstream php_backend {
    server 127.0.0.1:9000;
    server 127.0.0.1:9001;
    server 127.0.0.1:9002;

    keepalive 64;              # Пул постоянных соединений к бэкенду
    keepalive_requests 500;
    keepalive_timeout 60s;
}

server {
    location ~ \.php$ {
        # Обязательно для keepalive к upstream
        proxy_http_version 1.1;
        proxy_set_header Connection "";

        fastcgi_pass php_backend;
        fastcgi_keep_conn on;  # Для FastCGI upstream
    }
}

Сжатие и оптимизация передачи данных

Gzip-сжатие снижает объём передаваемых данных в 4-6 раз. Для «ШопМастер» средний размер страницы каталога упал с 820 КБ до 165 КБ:

http {
    gzip on;
    gzip_vary on;             # Добавляет Vary: Accept-Encoding
    gzip_proxied any;         # Сжимаем ответы от upstream
    gzip_comp_level 4;        # Баланс CPU/сжатие (1-9, рекомендуем 3-5)
    gzip_min_length 1100;     # Не сжимаем мелкие ответы
    gzip_buffers 64 8k;
    gzip_http_version 1.1;

    gzip_types
        text/plain
        text/css
        text/javascript
        text/xml
        application/json
        application/javascript
        application/xml
        application/xml+rss
        application/x-javascript
        image/svg+xml;

    # Предварительно сжатые файлы (gzip_static)
    gzip_static on;           # Отдаёт .gz файл, если существует
}

Для статических ресурсов мы заранее создаём .gz-файлы при сборке:

# В CI/CD pipeline
find /var/www/shopmaster/static -type f \( -name '*.js' -o -name '*.css' -o -name '*.svg' -o -name '*.html' \) \
    -exec gzip -9 -k {} \;

С gzip_static on Nginx отдаёт готовый .gz файл без затрат CPU на сжатие в реальном времени. Для сайта с 2 000 статических файлов это освободило 15% CPU.

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

Open file cache хранит дескрипторы открытых файлов, информацию об ошибках и метаданные. Это устраняет системные вызовы open() и stat() при каждом запросе:

http {
    open_file_cache max=2048 inactive=600s;
    open_file_cache_valid 2000s;
    open_file_cache_min_uses 1;
    open_file_cache_errors on;
}

Для каталога с 45 000 изображений товаров мы дополнительно монтировали горячий кэш в tmpfs:

# /etc/fstab
tmpfs /var/www/shopmaster/cache/hot tmpfs size=2g,mode=1777 0 0

# Монтируем
mount /var/www/shopmaster/cache/hot

Proxy cache для ответов бэкенда — главное оружие против нагрузки на PHP-FPM:

# Определяем зону кэша
proxy_cache_path /var/cache/nginx/shopmaster
    levels=1:2
    keys_zone=shop_cache:64m    # 64 МБ для ключей (~500K записей)
    max_size=4g                 # 4 ГБ для данных
    inactive=60m
    use_temp_path=off;

server {
    # Кэширование страниц каталога
    location /catalog/ {
        proxy_pass http://php_backend;
        proxy_cache shop_cache;
        proxy_cache_valid 200 30m;
        proxy_cache_valid 404 1m;
        proxy_cache_key $scheme$host$request_uri;
        proxy_cache_use_stale error timeout updating http_500 http_502;

        # Заголовок для отладки (HIT/MISS/STALE)
        add_header X-Cache-Status $upstream_cache_status;

        # Блокировка дублирующих запросов
        proxy_cache_lock on;
        proxy_cache_lock_timeout 5s;
    }

    # Статика — кэшируем на клиенте
    location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff2|svg)$ {
        expires 30d;
        add_header Cache-Control "public, immutable";
        access_log off;
    }
}

После включения proxy_cache hit rate достиг 78% для страниц каталога. Нагрузка на PHP-FPM упала в 4.5 раза, среднее время ответа — с 850 мс до 35 мс для кэшированных страниц.

Балансировка нагрузки и upstream

«ШопМастер» масштабировался до трёх бэкенд-серверов. Конфигурация upstream с health check и весами:

upstream app_cluster {
    least_conn;                  # Направляем на наименее загруженный

    server 10.0.1.10:8080 weight=3 max_fails=3 fail_timeout=30s;
    server 10.0.1.11:8080 weight=3 max_fails=3 fail_timeout=30s;
    server 10.0.1.12:8080 weight=2 max_fails=3 fail_timeout=30s;
    server 10.0.1.13:8080 backup;  # Резервный сервер

    keepalive 128;
}

server {
    location / {
        proxy_pass http://app_cluster;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        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_buffering on;
        proxy_buffer_size 8k;
        proxy_buffers 32 8k;
        proxy_busy_buffers_size 64k;

        # Таймауты к бэкенду
        proxy_connect_timeout 5s;
        proxy_send_timeout 30s;
        proxy_read_timeout 60s;

        # При ошибке — переключить на следующий сервер
        proxy_next_upstream error timeout http_502 http_503;
        proxy_next_upstream_tries 2;
    }
}

Алгоритмы балансировки, которые мы тестировали:

АлгоритмКогда использоватьРезультат для ШопМастер
round-robin (по умолчанию)Однородные бэкендыПерекос нагрузки из-за разной мощности серверов
least_connРазные по мощности бэкендыРавномерная нагрузка, выбрали этот
ip_hashSticky sessions нужныПерекос при NAT-клиентах
hash $request_uri consistentКэширование на бэкендахХорош для CDN, не подошёл

Rate limiting и защита от злоупотреблений

Без rate limiting любой парсер или DDoS-бот мог положить сервер. Мы настроили многоуровневую защиту:

http {
    # Зоны ограничения
    limit_req_zone $binary_remote_addr zone=general:32m rate=30r/s;
    limit_req_zone $binary_remote_addr zone=api:16m rate=10r/s;
    limit_req_zone $binary_remote_addr zone=login:8m rate=3r/m;
    limit_conn_zone $binary_remote_addr zone=addr:16m;

    server {
        # Общий лимит
        limit_req zone=general burst=50 nodelay;
        limit_conn addr 100;

        # API — строже
        location /api/ {
            limit_req zone=api burst=20 nodelay;
            proxy_pass http://app_cluster;
        }

        # Авторизация — максимально строго
        location /login {
            limit_req zone=login burst=5;
            proxy_pass http://app_cluster;
        }
    }
}

Security-заголовки для защиты от XSS, clickjacking и других атак:

server {
    # Заголовки безопасности
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;

    # Скрываем версию Nginx
    server_tokens off;

    # Блокируем подозрительные User-Agent
    if ($http_user_agent ~* (scrapy|curl|wget|python-requests|Go-http-client)) {
        return 403;
    }
}

Результаты и мониторинг

Для отслеживания эффективности мы включили stub_status и подключили Prometheus через nginx-exporter:

# Встроенный мониторинг
location = /nginx_status {
    stub_status on;
    access_log off;
    allow 10.0.0.0/8;
    deny all;
}

# Форматированный лог для анализа
log_format detailed '$remote_addr - $remote_user [$time_local] '
    '"$request" $status $body_bytes_sent '
    '"$http_referer" "$http_user_agent" '
    'rt=$request_time uct=$upstream_connect_time '
    'uht=$upstream_header_time urt=$upstream_response_time '
    'cs=$upstream_cache_status';

access_log /var/log/nginx/access.log detailed buffer=32k flush=5s;

Итоговые показатели после всех оптимизаций:

МетрикаДоПосле
Максимум RPS3 20052 000
Среднее время ответа4.1 с85 мс
P99 латентность12 с450 мс
Ошибки 502 при пиковой нагрузке18%0.01%
Трафик на клиента (средняя страница)820 КБ165 КБ
Нагрузка на PHP-FPM100% CPU22% CPU

Ключевой вклад в результат: proxy_cache дал 78% попаданий, gzip уменьшил трафик в 5 раз, keepalive к upstream сократил время на установление соединений на 85%, а балансировка на 3 бэкенда распределила вычислительную нагрузку равномерно.

Часто задаваемые вопросы

Формула: worker_connections = максимальное_число_соединений / worker_processes. Учитывайте, что при проксировании каждый клиент занимает два соединения (к клиенту и к бэкенду). Начните с 2048 на воркер и увеличивайте, мониторя через stub_status число активных соединений. Не забудьте поднять worker_rlimit_nofile и системные лимиты в /etc/security/limits.conf.
Рекомендуем 3-5. Уровень 1-2 даёт минимальное сжатие при минимальных затратах CPU. Уровень 6-9 увеличивает сжатие на 5-10%, но нагрузка на CPU растёт в 3-4 раза. Для высоконагруженных серверов лучше использовать gzip_static on и сжимать файлы заранее на уровне 9 при сборке.
Да, для FastCGI используйте директиву fastcgi_keep_conn on в блоке location. Без неё Nginx закрывает соединение после каждого запроса, и PHP-FPM тратит ресурсы на повторное установление. При 10 000 RPS экономия на keepalive к upstream составляет 15-20% CPU.
Без proxy_cache_lock при истечении кэша 100 одновременных запросов к одному URL все пойдут на бэкенд (thundering herd). С включённым lock только первый запрос идёт к бэкенду, остальные ждут результата и получают его из кэша. Это радикально снижает пиковую нагрузку на бэкенд.
Монтирование кэша в tmpfs (RAM-диск) имеет смысл для горячих данных небольшого объёма — до 2-4 ГБ. Это устраняет дисковый I/O полностью. Для больших кэшей (десятки ГБ) используйте NVMe SSD. Помните, что tmpfs теряет данные при перезагрузке, поэтому предусмотрите прогрев кэша.

Нужна помощь с проектом?

Специалисты АйТи Фреш помогут с архитектурой, DevOps, безопасностью и разработкой — 15+ лет опыта

📞 Связаться с нами
#nginx#worker_processes#worker_connections#keepalive#gzip#open_file_cache#upstream#rate limiting
Комментарии 0

Оставить комментарий

загрузка...