MAATRIX / Блог / Миф: больше воркеров nginx — больше держит нагрузки

Миф: больше воркеров nginx — больше держит нагрузки

MAATRIX

Сайт под нагрузкой начинает подтормаживать, и первое, что советуют на форумах и в чатах поддержки: «поставьте worker_processes побольше — 16, 32, сколько не жалко». Логика звучит убедительно: больше воркеров — больше параллельных обработчиков — больше запросов в секунду. На практике после такой правки нагрузка не расходится, а иногда сайт начинает работать даже хуже. Разберём, почему nginx устроен не так, как Apache prefork или пул PHP-FPM, и что на самом деле определяет его пропускную способность.

Как звучит миф и откуда он берётся

Формулировка мифа предельно простая: «чем больше worker_processes в конфиге nginx, тем больше запросов сервер выдержит». Она приходит по прямой аналогии с другими частями стека, где эта логика действительно работает. PHP-FPM с директивой pm.max_children — это буквально пул процессов, и каждый дополнительный процесс — это ещё один параллельный обработчик PHP-запроса. Apache в режиме prefork — то же самое: один процесс на одно соединение, больше процессов — больше одновременных клиентов. Админ, который настраивал PHP-FPM или Apache, переносит этот же принцип на nginx, потому что визуально директивы выглядят похоже — тоже «число воркеров» в конфиге.

Панели управления и типовые гайды только закрепляют путаницу: часто встречается совет вида «поставьте worker_processes равным числу ядер, умноженному на 2» или вовсе фиксированное число вроде 8 независимо от того, сколько у сервера реальных ядер. Смысл такой рекомендации редко объясняется — просто «так надёжнее».

Рациональное зерно в переносе логики есть: для моделей с блокирующим вводом-выводом (один процесс = один запрос, и пока идёт обращение к диску или базе, процесс простаивает) больше процессов действительно означает больше параллельно обслуживаемых клиентов. Проблема в том, что nginx с 2004 года спроектирован принципиально иначе, и именно эта разница — причина, по которой он вообще стал стандартом.

Как на самом деле работает nginx: событийная модель

nginx появился как прямой ответ на C10K problem — задачу удержать десять тысяч одновременных соединений на одном сервере без десяти тысяч процессов или потоков ОС. Решение — событийный цикл (event loop) поверх механизмов ядра epoll (Linux) или kqueue (BSD/macOS).

Master-процесс nginx запускает несколько worker-процессов (их число задаёт worker_processes), но дальше каждый воркер работает не так, как процесс Apache prefork или PHP-FPM. Внутри воркера нет отдельного потока или процесса на каждое соединение — один воркер обслуживает тысячи соединений одновременно за счёт неблокирующего ввода-вывода:

Apache prefork / PHP-FPM (process-per-connection):
  Соединение 1 → Процесс 1 (блокируется на I/O, ждёт)
  Соединение 2 → Процесс 2 (блокируется на I/O, ждёт)
  Соединение 3 → Процесс 3 (блокируется на I/O, ждёт)
  ...
  1000 соединений = 1000 процессов/потоков

nginx (event-driven):
  Соединение 1 ─┐
  Соединение 2 ─┼─→ Worker 1 (один поток, epoll)
  Соединение 3 ─┘     не блокируется — переключается
  ...                  между сокетами по готовности данных
  1000 соединений = один воркер, без блокировки

Пока одно соединение ждёт данных от клиента, ответа от бэкенда или записи на диск, воркер не простаивает и не блокируется — он переключается на обработку другого соединения, у которого данные уже готовы. Ядро ОС через epoll сообщает воркеру, какие файловые дескрипторы готовы к чтению или записи, и воркер обслуживает именно их, не тратя циклы CPU на ожидание. Именно поэтому один воркер nginx физически не эквивалентен одному процессу Apache — он эквивалентен целому пулу таких процессов, но без накладных расходов на их создание, переключение контекста между ними и раздельную память под каждый.

Это ключевое архитектурное отличие, и на нём стоит вся репутация nginx как лёгкого и производительного веб-сервера. Похожая по духу путаница разбирается в статье про миф о превосходстве nginx над Apache — там показано, что современный Apache тоже умеет событийные режимы, но по умолчанию у него другая модель, и именно она объясняет разницу в потреблении памяти при большом числе соединений.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Сколько реально держит один воркер

Раз воркер не блокируется на ожидании, единственное, что физически ограничивает его пропускную способность, — это процессорное время, которое требуется на активную обработку: разбор HTTP-заголовков, TLS-рукопожатие, сжатие ответа, применение правил rewrite, проксирование на бэкенд. Всё это происходит в одном потоке на одном ядре — воркер nginx не многопоточный и сам по себе не размазывает вычисления одного запроса по нескольким ядрам.

Отсюда следует простой вывод: пока воркер не упирается в 100% загрузку своего ядра, у него есть запас для обслуживания дополнительных соединений — почти без разницы, сколько их сейчас активно, потому что большую часть времени соединение просто ждёт данных, а не грузит CPU. Именно поэтому один воркер способен держать очень большое число одновременных keep-alive-соединений — цифра сильно зависит от характера трафика (статика, проксирование, TLS, размер ответов) и от worker_connections, но порядок величины принципиально другой, чем при process-per-connection.

Верхний предел одного воркера — это момент, когда его единственное ядро загружено вычислениями на 100%. С этой точки добавить пропускной способности можно только одним способом — дать воркеру ещё одно физическое ядро, то есть запустить ещё один воркер. Отсюда и рабочее правило: один воркер на одно ядро CPU — это, как правило, естественный потолок, при котором вычислительные ресурсы сервера используются полностью, без лишней конкуренции между воркерами. Про то, где именно проходит предел по числу соединений на одном воркере и как его нащупать на конкретном сервере, — в статье про предел worker_connections.

Что происходит, если воркеров больше, чем ядер

Если поставить worker_processes 32 на сервере с 4 ядрами, планировщику ОС приходится распределять 32 процесса между 4 физическими исполнителями. Каждый воркер по-прежнему хочет работать непрерывно, но реально может исполняться только четыре из них одновременно — остальные ждут своей кванты времени. Это не даёт дополнительной вычислительной мощности: суммарное процессорное время, доступное системе, не увеличилось, оно просто нарезается на больше кусков.

Хуже того, лишние воркеры создают чистые накладные расходы без какой-либо выгоды:

  • Переключения контекста. Планировщику приходится чаще переключаться между процессами, а каждое переключение — это инвалидация кэша процессора (L1/L2). Полезной работы от этого не прибавляется, только накладные расходы.
  • Конкуренция за один и тот же слушающий сокет. Несколько воркеров одновременно принимают новые соединения на одном порту; nginx смягчает эффект «громкого стада» через accept_mutex и SO_REUSEPORT, но это лишь по-другому раскладывает ту же нагрузку, а не увеличивает её.
  • Память. Каждый воркер держит собственный набор буферов на активное соединение (client_body_buffer_size, proxy_buffers). Лишние процессы — это лишний расход RAM без прироста производительности, а на памяти, близкой к пределу VPS, это может довести дело до свопа, который убивает отклик сервера сильнее, чем нехватка CPU, которую пытались компенсировать лишними воркерами.

Проверить эффект на практике можно через mpstat под нагрузкой:

# Загрузка по каждому ядру отдельно
mpstat -P ALL 1 5

Если после увеличения числа воркеров сверх числа ядер доля %sys (системное время, куда входят переключения контекста) заметно растёт, а доля %usr (полезная вычислительная работа) не увеличивается — это именно тот случай: сервер тратит больше циклов на административные накладные расходы планировщика, а не на обработку запросов.

Где узкое место на самом деле

Практика показывает: когда nginx-сервер «не тянет» нагрузку, причина почти никогда не в числе воркеров nginx как таковых. Сам событийный слой nginx настолько эффективен, что упирается в потолок последним — раньше него обычно сдаётся что-то другое:

  • Бэкенд-приложение. nginx в типичной схеме — это только приёмник соединений и реверс-прокси; реальная работа (запрос к базе, рендер шаблона, бизнес-логика) выполняется в PHP-FPM, Node.js, Gunicorn или другом апстриме. Если там кончился пул воркеров или процесс упёрся в CPU, nginx честно ждёт ответа и ничего не может с этим сделать — добавление ему воркеров бэкенду не поможет.
  • Диск. Запись логов доступа на каждый запрос, отдача больших статических файлов без sendfile, кэш на медленном томе — во всех этих случаях воркер простаивает не из-за нехватки CPU, а из-за ожидания диска. Профилировать стоит через iostat -x 1 5 и смотреть на %util и await.
  • Память под буферы и кэш. При большом числе одновременных соединений или при агрессивном proxy_cache/fastcgi_cache именно оперативная память, выделенная под буферы и кэш-зону, может стать пределом раньше CPU. Признак — рост %sys и время в свопе, а не загрузка воркеров.
  • Сеть и TLS. Обработка TLS-рукопожатий действительно требует CPU, особенно на серверах без аппаратного ускорения AES — но это вопрос настройки шифров и кэша сессий, а не числа воркеров; подробнее в статье про AES-NI и нагрузку на CPU при шифровании.
  • Сам CPU, но не из-за nginx. Иногда высокая загрузка процессора вообще не связана с веб-сервером — соседний процесс, бэкап, антивирусный сканер или крон-задача съедают ядра в тот же момент, когда растёт трафик. Как находить настоящего виновника нагрузки на процессор — в материале про поиск причины высокой нагрузки на CPU.

Иными словами, увеличение worker_processes — это попытка лечить симптом («сайт тормозит») инструментом, который решает совсем узкий класс проблем (нехватка воркеров nginx именно на уровне event loop). Такая проблема на практике встречается редко, потому что событийная модель по конструкции экономна на ресурсах.

Как правильно настроить worker_processes и worker_connections

Базовая и в подавляющем большинстве случаев достаточная настройка:

# /etc/nginx/nginx.conf

user www-data;
worker_processes auto;
worker_rlimit_nofile 65535;

events {
    worker_connections 4096;
    use epoll;
    multi_accept on;
}

http {
    # ...остальная конфигурация
}

Что означает каждая директива:

ДирективаРекомендацияПочему
worker_processesautonginx сам определяет число доступных ядер и запускает ровно столько воркеров — не больше и не меньше
worker_connections1024–4096 (по памяти и профилю трафика)Максимум одновременных соединений на один воркер
worker_rlimit_nofile65535Лимит открытых файловых дескрипторов на воркер; должен быть не меньше worker_connections, иначе воркер упрётся в лимит ОС раньше, чем в собственный конфиг
use epollявно на LinuxЭффективный механизм опроса сокетов ядром; на современных дистрибутивах nginx выбирает его автоматически, но явное указание не помешает
multi_accepton при высокой частоте новых соединенийРазрешает воркеру принимать сразу несколько новых соединений за одну итерацию цикла, а не по одному

Теоретическая формула для оценки предела: worker_processes × worker_connections ≈ максимум одновременных соединений на сервер. Для схемы reverse proxy стоит помнить, что каждое клиентское соединение обычно держит открытым ещё одно — от nginx к бэкенду, — так что реальный запас по клиентам примерно вдвое меньше формального числа.

Опционально, для серверов под очень высокую и стабильную нагрузку, можно закрепить воркеры за конкретными ядрами:

worker_cpu_affinity auto;

Это снижает вероятность, что планировщик ОС будет перекидывать воркер между ядрами, теряя кэш процессора при каждом переносе. На типичном VPS с умеренной нагрузкой эффект обычно неощутим — планировщик Linux и без неё справляется хорошо, — но на выделенном сервере под высокую нагрузку она может дать небольшой осмысленный выигрыш.

Проверить, сколько воркеров реально запущено и как распределена нагрузка, можно так:

# Число доступных ядер
nproc

# Сколько воркеров nginx реально запущено
ps aux | grep "nginx: worker process" | grep -v grep | wc -l

# Что nginx считает текущим значением worker_processes
nginx -T 2>/dev/null | grep worker_processes

Для наблюдения за активными соединениями в реальном времени полезен модуль stub_status:

location /nginx_status {
    stub_status;
    allow 127.0.0.1;
    deny all;
}
curl http://127.0.0.1/nginx_status

Он покажет число активных соединений, принятых, обработанных запросов и соединений в состояниях reading/writing/waiting — по этим цифрам гораздо честнее судить, упирается ли nginx в реальный предел, чем по интуитивному «на всякий случай побольше воркеров». Если после изменения worker_processes вы хотите применить настройку без разрыва активных соединений, используется штатный reload:

nginx -t && systemctl reload nginx

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

У меня VPS на 2 ядрах — сколько ставить worker_processes?

auto, то есть 2. Для подавляющего большинства сайтов с умеренным и даже высоким трафиком этого достаточно — узкое место почти наверняка окажется не здесь.

Можно ли поставить worker_processes с запасом, "на будущее"?

Смысла нет: лишние воркеры не создают запас пропускной способности, они создают запас накладных расходов на переключение контекста и памяти под буферы. Если трафик вырастет настолько, что понадобятся дополнительные вычислительные ресурсы, — логичнее добавить ядра на сервере (тогда auto сам подхватит новое число) или вынести часть нагрузки на отдельный сервер.

Как понять, что мне действительно не хватает воркеров nginx, а не чего-то другого?

Смотрите mpstat -P ALL под пиковой нагрузкой: если все ядра забиты близко к 100% именно полезной работой (%usr), а не системными вызовами, и при этом stub_status показывает соединения в очереди на обработку — это первый честный сигнал. Такое встречается заметно реже, чем принято думать.

worker_connections 65535 — это нормально?

Само по себе число не проблема, если под него хватает worker_rlimit_nofile и оперативной памяти на буферы. Но завышенное значение "про запас" без реальной потребности просто резервирует лимиты, которые никогда не используются — вреда немного, а практической пользы тоже нет.

А что насчёт worker_processes больше числа ядер на сервере с гипертредингом?

Гипертрединг (SMT) даёт ОС дополнительные логические ядра, и auto их тоже учтёт — это не противоречит правилу "один воркер на ядро", просто "ядро" в данном случае включает и логические потоки, которые видит nproc.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →