⚙️ Теория
🧩 Master / Worker model
master-процесс:
- читает конфигурацию;
- открывает listening sockets;
- запускает и контролирует
worker-процессы; - выполняет
reload,graceful shutdown,log reopen.
worker-процессы:
- принимают подключения;
- обрабатывают HTTP-запросы;
- работают независимо друг от друга.
🧠 Single-threaded worker
Каждый worker в классической модели NGINX однопоточный: один процесс = один event loop.
NGINX не создаёт поток на каждое соединение, поэтому не платит цену за постоянные переключения контекста между тысячами потоков.
🔌 epoll / kqueue
NGINX использует событийные механизмы ядра:
epollна Linux;kqueueна BSD/macOS.
Вместо ожидания каждого сокета отдельно worker получает от ядра только готовые события: "можно читать" / "можно писать".
🚦 accept mutex
accept_mutex снижает эффект thundering herd (когда много воркеров одновременно пытаются принять одно и то же входящее соединение).
Механизм сериализует accept() между воркерами и уменьшает лишнюю конкуренцию за listening socket.
Важно по документации:
- по умолчанию
accept_mutex off; - на Linux с
EPOLLEXCLUSIVE(ядро >= 4.5) включать его обычно не нужно; - при
reuseportон также не требуется.
♻️ reuseport
reuseport даёт каждому worker собственный listening socket на одном порту.
Распределение входящих соединений делает ядро, что обычно уменьшает lock-contention на этапе accept.
🎯 Фокус
NGINX не масштабируется потоками — он масштабируется процессами.
❓ Что нужно уметь объяснить
Почему один worker может держать тысячи соединений?
Коротко: у worker нет модели «один поток = одно соединение».
Один процесс ведёт много сокетов сразу и работает только с теми, которые уже готовы к чтению/записи.
Как это происходит по шагам:
workerвызываетepoll_wait()и "засыпает".- Ядро будит его только когда есть готовые события.
workerбыстро обрабатывает событие (прочитал/отправил часть данных).- Переходит к следующему готовому сокету.
- Возвращается в
epoll_wait().
Итог: тысячи соединений возможны, потому что нет тысяч потоков и нет ожидания каждого клиента по отдельности.
Почему worker не блокируется?
- сокеты открыты в неблокирующем режиме;
- если данные пока не готовы (
EAGAIN/EWOULDBLOCK),workerне ждёт, а идёт дальше; - ожидание готовности делает ядро через
epoll_wait()/kevent(), а не самworker.
То есть worker тратит CPU на обработку событий, а не на "пассивное ожидание".
Где возможен contention?
Основные точки конкуренции:
accept()на одном listening socket (смягчаютaccept_mutexиreuseport);- общие структуры в shared memory (
limit_req_zone,keys_zone,upstream zone); - CPU (слишком мало
worker_processes, long requests, перегруженный upstream); - диск и сеть (очереди I/O, packet drops, медленные ответы backend).
Признаки contention:
- растут latency p95/p99;
- падает requests/sec;
- увеличиваются
timeoutsи5xx.
🧪 Практика
1. Посмотреть количество воркеров
ps aux | grep nginx
2. Проверить event loop через strace
strace -p <pid_worker>
Полезнее сузить до ключевых syscalls:
strace -f -e trace=epoll_wait,accept4,recvfrom,sendto -p <pid_worker>
3. Изменить параметры NGINX
В nginx.conf:
worker_processes auto;
events {
worker_connections 4096;
use epoll;
}
worker_connections — это максимум всех соединений на worker (клиентских, upstream и т.д.), а не только входящих клиентов.
Применить изменения:
nginx -t && sudo nginx -s reload
4. Нагрузить сервер и сравнить поведение
ab -n 50000 -c 500 http://127.0.0.1/
или
wrk -t4 -c400 -d30s http://127.0.0.1/
Сравнивайте до/после изменения:
- latency (p50/p95/p99);
- requests/sec;
- ошибки (5xx, timeouts);
- загрузку CPU по воркерам.
🧾 Вывод
Сила NGINX в том, что он масштабируется процессами и событийной моделью, а не потоками на каждое соединение.
worker остаётся эффективным под высокой нагрузкой, пока он быстро обрабатывает готовые события и не упирается в contention (accept, shared memory, CPU, I/O).
Главная практическая задача инженера: правильно подобрать worker_processes/worker_connections и проверять поведение под реальной нагрузкой, а не по теории.