Загрузка
Все статьи

Nginx + PHP-FPM: тюнинг под высокую нагрузку — от 500 RPS до 5000 RPS

Сайт на PHP + Nginx выдерживал 500 RPS, а при 1000 начинал отдавать 502. Проблема была не в PHP-коде (он был оптимизирован), а в конфигурации Nginx и PHP-FPM: неправильный worker_connections, отсутствие keepalive к upstream, слишком маленький pm.max_children. Рассказываю пошагово, как мы увеличили пропускную способность в 10 раз.

Проект — рекламная платформа с высокочастотным API. PHP-бэкенд на Laravel, Nginx как reverse proxy, PHP-FPM 8.3. PHP-код был оптимизирован ещё три месяца назад: OPcache включён, запросы к БД закэшированы через Redis, тяжёлые вычисления вынесены в воркеры. При 500 RPS всё летало. При 1000 RPS начинались 502 Bad Gateway. Я поехал разбираться.

Диагностика: где узкое горлышко

Сначала нагрузочное тестирование. Использовал wrk — простой и надёжный инструмент:

wrk -t 12 -c 500 -d 60s --latency http://api.example.com/v1/ads

# При 500 RPS:
#  Latency  Distribution
#    50%   12.45ms
#    75%   18.23ms
#    90%   25.67ms
#    99%   89.12ms

# При 1000 RPS:
#  Non-2xx responses: 347
#  502 errors: 312

При 1000 RPS — куча 502. Смотрю логи Nginx:

tail -f /var/log/nginx/error.log

# connect() failed (111: Connection refused) while connecting to upstream
# upstream prematurely closed connection while reading response header

Connection refused от upstream — PHP-FPM не принимает подключения. Смотрю конфиг pool:

[www]
pm = dynamic
pm.max_children = 20
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 8
pm.max_requests = 500

Вот оно. pm.max_children = 20. При latency одного запроса 12ms, 20 воркеров могут обслужить ~1666 RPS в теории. Но на практике — меньше, из-за TCP handshake, keepalive overhead и contention. Когда RPS превышает capacity — новые подключения к PHP-FPM получают connection refused, и Nginx отдаёт 502.

Шаг 1: Тюнинг PHP-FPM

Сервер: 8 vCPU, 32 GB RAM. PHP-процесс ест ~50MB. 32GB / 50MB = 640 процессов (теоретический максимум). Но Nginx, Redis, система тоже нужна память. Реалистично — 400 процессов для PHP-FPM:

[www]
pm = dynamic
pm.max_children = 300
pm.start_servers = 40
pm.min_spare_servers = 30
pm.max_spare_servers = 80
pm.max_requests = 1000
pm.process_idle_timeout = 10s

Ключевые изменения: pm.max_children с 20 до 300, pm.start_servers с 4 до 40 (чтобы не ждать масштабирования при резком всплеске), pm.max_requests = 1000 (перезапуск процесса раз в 1000 запросов для борьбы с memory leaks).

После этого: 2000 RPS без ошибок. Но при 3000 RPS начали появляться latency spikes до 200ms. Проблема перешла в Nginx.

Шаг 2: Тюнинг Nginx

Исходный конфиг Nginx был дефолтный:

worker_processes auto;
events {
    worker_connections 768;
}

http {
    upstream php-fpm {
        server 127.0.0.1:9000;
    }

    server {
        location ~ \.php$ {
            fastcgi_pass php-fpm;
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
            include fastcgi_params;
        }
    }
}

Проблемы: worker_connections 768 — слишком мало при 3000+ одновременных подключений. Нет keepalive к upstream — каждое соединение к PHP-FPM открывается заново, а TCP handshake занимает ~0.5ms. При 5000 RPS это 2.5 секунды только на handshake. Нет buffering — Nginx ждёт ответа от PHP-FPM синхронно.

Исправленный конфиг:

worker_processes auto;
events {
    worker_connections 8192;
    multi_accept on;
    use epoll;
}

http {
    keepalive_timeout 65;
    keepalive_requests 10000;

    upstream php-fpm {
        server 127.0.0.1:9000;
        keepalive 200;
    }

    server {
        listen 80 reuseport;
        location ~ \.php$ {
            fastcgi_pass php-fpm;
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
            include fastcgi_params;

            # Keepalive к upstream
            fastcgi_keep_conn on;

            # Buffering
            fastcgi_buffer_size 32k;
            fastcgi_buffers 8 32k;
            fastcgi_busy_buffers_size 64k;
        }
    }
}

Что изменилось: worker_connections 8192 — теперь Nginx может держать 8192 одновременных подключений на каждый worker (а их 8, итого 65536). keepalive 200 в upstream — 200 постоянных соединений к PHP-FPM, которые переиспользуются. fastcgi_keep_conn on — Nginx не закрывает соединение к PHP-FPM после каждого запроса. reuseport on — все воркеры слушают на одном порту, ядро распределяет подключения равномерно.

Шаг 3: Оптимизация ядра

Ещё пара параметров sysctl, которые дали небольшой, но заметный эффект:

# /etc/sysctl.d/99-tuning.conf
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 65535
net.ipv4.ip_local_port_range = 1024 65535
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15
net.core.netdev_max_backlog = 65535

somaxconn — размер очереди ожидающих подключений. Дефолт 128 — при резком всплеске подключения отбрасываются. tcp_tw_reuse = 1 — разрешает переиспользование TIME_WAIT сокетов.

Результаты

Финальный тест:

wrk -t 12 -c 2000 -d 60s --latency http://api.example.com/v1/ads

# 5000+ RPS sustained
# Latency  Distribution
#   50%    8.12ms
#   75%   11.45ms
#   90%   15.23ms
#   99%   42.67ms
# 0 errors

От 500 RPS с 502 ошибками до 5000+ RPS со стабильной latency. Ускорение в 10 раз. Самое важное: мы не трогали PHP-код. Вся проблема была в инфраструктурном слое — неправильный sizing пулов, отсутствие keepalive, маленькие буферы.

Главный совет: когда видите 502 — не спешите лезть в код. Сначала проверьте capacity вашего приложения-сервера и настройки reverse proxy. Часто проблема не в том, что код медленный, а в том, что инфраструктура не может пропустить через себя нужный объём запросов.

Поделиться статьёй

Комментарии

0
0 / 2000

Пока нет комментариев

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

Читайте также

Docker Compose для локальной разработки Kotlin-микросервисов — от 10 минут до 30 секунд старта

Как мы оптимизировали локальное окружение из 10+ микросервисов с помощью Docker Compose, multi-stage Dockerfile и правильных health checks.

15 июня 2026 10 мин

Terraform vs Pulumi — Infrastructure as Code для Kotlin-команды

Сравниваем Terraform на HCL и Pulumi на Kotlin для управления Kubernetes-инфраструктурой — с реальными примерами кода, болью миграции и нашим итоговым выбором.

14 июня 2026 11 мин

Gradle Kotlin DSL: кэширование и configuration avoidance — ускоряем сборку

Сборка Kotlin-проекта занимала 6 минут. После включения build cache, configuration cache и перехода на configuration avoidance API — 45 секунд. Рассказываю, что именно мы сделали, почему большинство туториалов по ускорению Gradle не работают, и как измерять реальный эффект от каждой оптимизации.

22 мая 2026 10 мин