Событийная модель Nginx: что происходит внутри worker
Nginx обслуживает тысячи соединений не за счёт потока на каждый запрос, а через неблокирующий цикл событий. Разбираем путь соединения от accept до ответа, роль epoll, таймеров и настройки worker_connections.

Nginx не создаёт отдельный поток для каждого клиента: worker-процесс держит множество неблокирующих соединений и просыпается только тогда, когда операционная система сообщает о готовности сокета. Поэтому один worker может обслуживать тысячи клиентов, но только пока обработчики не блокируют цикл событий. Ниже — разбор Nginx 1.27.x: от принятия TCP-соединения до передачи ответа, с ограничениями, которые проявляются под нагрузкой.
Из чего состоит модель Nginx
После запуска master-процесс читает конфигурацию, открывает слушающие сокеты и создаёт worker-процессы. Основная работа с HTTP выполняется именно worker. Master не проксирует запросы и не раздаёт их между worker вручную.
- Master запускает worker, перечитывает конфигурацию, принимает сигналы и управляет перезапуском.
- Worker принимает соединения, читает запросы, запускает обработчики и отправляет ответы.
- Событийный модуль связывает Nginx с механизмом ядра: epoll на Linux, kqueue на системах BSD и macOS, event ports на Solaris и другими доступными вариантами.
- HTTP-модули обрабатывают конкретные стадии: разбор заголовков, выбор location, проксирование, фильтрацию и запись ответа.
Worker обычно работает в одном основном потоке. Это не означает, что он выполняет только одну операцию за раз: пока сетевой сокет или upstream не готовы, worker отдаёт управление циклу событий и занимается другим соединением.
Главное ограничение Nginx — не число открытых соединений само по себе, а время, которое один worker проводит внутри блокирующего обработчика.
Что такое событие
Событие — это уведомление о состоянии дескриптора, чаще всего сетевого сокета. Ядро сообщает, что из него можно читать, в него можно писать, соединение закрыто или произошла ошибка. Nginx не обязан постоянно опрашивать каждый сокет и проверять его вручную.
Дескриптор — числовой идентификатор открытого ресурса процесса: TCP-соединения, файла, pipe или другого объекта, который можно читать и записывать через системные вызовы.
Путь запроса внутри worker
Жизненный цикл запроса состоит из нескольких переходов между состояниями. Один системный вызов может изменить сразу несколько соединений, после чего Nginx последовательно обработает готовые события.
- Ожидание. Worker вызывает механизм уведомлений и засыпает до появления событий или истечения таймера. На Linux в типичной конфигурации это epoll. Если в этот момент нет готовых сокетов, CPU не тратится на постоянный busy loop.
- Приём TCP-соединения. После уведомления о готовом listening socket Nginx вызывает accept и получает новый дескриптор. Для него включается неблокирующий режим, чтобы чтение или запись не остановили worker.
- Чтение запроса. При готовности клиентского сокета Nginx забирает доступные байты. Один вызов read не обязан вернуть весь HTTP-запрос: заголовки могут прийти частями. Поэтому обработка продолжается после следующего события чтения.
- Разбор и маршрутизация. Когда заголовки собраны, Nginx выбирает виртуальный сервер и location, применяет ограничения, читает тело запроса при необходимости и передаёт управление цепочке HTTP-модулей.
- Ожидание upstream. При проксировании Nginx отправляет запрос на backend. Если backend ещё не ответил, worker не ждёт его синхронно, а регистрирует интерес к событию чтения на upstream-сокете.
- Формирование ответа. Полученные данные проходят через upstream- и filter-модули. Если клиентский сокет временно не принимает новые данные, остаток ответа остаётся в буферах и будет отправлен после следующего события записи.
- Закрытие или keep-alive. После ответа соединение либо закрывается, либо возвращается в состояние ожидания следующего запроса. Таймер keep-alive ограничивает время простоя.
Ключевой момент: переход к следующему этапу не обязан происходить в том же проходе цикла. Событие может поставить работу в очередь, а worker продолжит её позже, когда появятся данные или освободится сокет.
Почему Nginx использует epoll и аналоги
Старые модели уведомлений заставляли процесс просматривать большой набор дескрипторов и искать среди них готовые. Это становится дорогим, когда открытых соединений много, а активны только единицы. epoll хранит интересы процесса в ядре и возвращает список готовых событий.
| Механизм | Где встречается | Что получает Nginx | Ограничение |
|---|---|---|---|
| epoll | Linux | Список готовых дескрипторов без полного прохода по набору | Поведение зависит от корректной неблокирующей работы и версии ядра |
| kqueue | FreeBSD, macOS и другие BSD-системы | События чтения, записи и изменения состояния дескрипторов | Набор флагов и детали отличаются от Linux |
| select | Совместимый запасной вариант | Проверка готовности дескрипторов | Ограничение набора и стоимость проверки растут вместе с числом дескрипторов |
| poll | Совместимый вариант в Unix-системах | Проверка массива дескрипторов без фиксированного лимита select | При большом наборе требуется просматривать массив |
Точный механизм можно увидеть в выводе nginx -V и в диагностике запуска, но выбирать его директивой обычно не требуется: Nginx определяет доступный вариант при сборке и запуске. Смена epoll на другой backend не исправит блокирующий код модуля или медленный upstream.
Где в цикле живут таймеры
Не все события приходят от сокетов. Nginx должен закрывать idle keep-alive, прекращать ожидание ответа upstream, ограничивать чтение тела запроса и выполнять другие действия по времени. Для этого worker вычисляет ближайший срок и передаёт его механизму ожидания событий.
Если первым наступил таймер, worker обрабатывает просроченные таймеры. Если раньше готов сокет, сначала выполняется сетевое событие, затем проверяются таймеры. Наличие таймера не создаёт отдельный поток и не означает, что callback сработает секунда в секунду: фактический момент зависит от загрузки worker и продолжительности текущих обработчиков.
Несколько worker и конкуренция за accept
Каждый worker имеет собственный цикл событий, но слушающий сокет обычно доступен всем worker. Когда приходит новое соединение, несколько процессов могут проснуться и попытаться вызвать accept. Такое явление называют thundering herd — «стадным эффектом»: просыпаются процессы, которым в итоге ничего не достаётся.
Механизм accept_mutex может ограничивать, какой worker в конкретный момент принимает новые соединения. В актуальных конфигурациях Nginx его поведение зависит от выбранного event-модуля и настроек. Не стоит включать или выключать директиву вслепую: сравнивайте нагрузку на CPU, скорость принятия новых соединений и распределение запросов между worker.
Отдельный фактор — reuseport. Он позволяет создать отдельный listening socket для каждого worker, а распределение новых соединений частично выполняет ядро. Это может уменьшить конкуренцию за accept, но меняет балансировку и взаимодействие с операционной системой. Подход нужно проверять на конкретном Linux и конкретном трафике, а не переносить из чужого конфига.
Что ломается первым под нагрузкой
Блокирующий обработчик
Сетевые операции Nginx в штатном пути неблокирующие. Однако сторонний модуль, Lua-код, синхронная работа с диском или неудачная интеграция может задержать worker. Пока обработчик не вернёт управление, готовые события других клиентов не обслуживаются. Симптом — растущая задержка при невысокой загрузке сети и скачки времени ответа у разных запросов.
Ограничение worker_connections
worker_connections задаёт максимальное число соединений, которые может обслуживать один worker, но это не простая гарантия количества клиентов. В прокси-сценарии один клиентский запрос часто занимает как минимум клиентское и upstream-соединение. Кроме того, дескрипторы нужны для других ресурсов процесса.
Грубая верхняя оценка для HTTP-прокси выглядит так: число worker умножается на worker_connections, затем учитываются лимиты открытых файлов и соединения, которые создаёт каждый запрос. Это оценка, а не расчёт ёмкости. Проверяйте фактическое число дескрипторов, ошибки открытия сокетов и очередь соединений.
Медленный upstream
Неблокирующая модель не делает backend быстрее. Если приложение отвечает долго, соединения остаются занятыми, буферы копят данные, а новые клиенты конкурируют за тот же набор worker и файловых дескрипторов. Причину ищут по отдельности в времени соединения с upstream, времени получения заголовков, времени передачи ответа и длине очередей backend.
Ошибочная диагностика
Высокий CPU не всегда означает, что epoll работает неправильно. Причиной могут быть TLS, сжатие, фильтрация больших ответов, слишком частые лог-записи или бесконечный поток мелких событий. Смотрите метрики процесса и системные счётчики вместе: число активных соединений, accept rate, задержку upstream, чтение и запись на сокеты, загрузку CPU каждого worker.
Как проверять поведение, а не угадывать
Точных универсальных цифр для этой модели нет: результат зависит от размера ответа, TLS, дисков, ядра, числа worker, типа backend и характера соединений. Сравнение делайте на одной машине и с одинаковым сценарием.
- Зафиксируйте версию Nginx, версию ядра, число CPU, объём памяти и конфигурацию worker.
- Разделите сценарии: короткие ответы из Nginx, прокси к быстрому backend, медленный backend, keep-alive и большое тело ответа.
- Снимите базовые значения: запросы в секунду, p50/p95/p99 задержки, ошибки 4xx/5xx, активные соединения и загрузку CPU.
- Проверьте число открытых файлов и фактические ошибки вида «too many open files».
- Повторите тест после изменения одной настройки. Иначе нельзя понять, что именно повлияло на результат.
Критерий успеха — не максимальный показатель запросов в секунду, а отсутствие роста хвостовой задержки и ошибок при целевой параллельности. Если p99 резко растёт, ищите очередь или блокирующий участок, даже когда среднее время ответа выглядит приемлемо.
Что делать дальше
Начните с карты соединений: клиент, Nginx, upstream и файловые дескрипторы. Затем измерьте каждый участок отдельно и проверьте, не упирается ли система в лимит открытых файлов. После этого сравните поведение при изменении числа worker и worker_connections, не меняя одновременно таймауты, буферы и настройки ядра. Разбор upstream-буферов и инвалидация кеша — отдельные темы; здесь важно сначала понять, на каком событии ваш worker перестаёт успевать.
Создатель презентаций: Структура и текст каждого слайда: заголовки-утверждения, одна мысль на слайд, заметки докладчику.
Спросить за 150 ₽Источники: Nginx documentation: ngx_core_module, Nginx documentation: события и worker_connections
Частые вопросы
Nginx создаёт отдельный поток для каждого запроса?
Почему один медленный backend влияет на другие запросы?
Что будет, если worker_connections слишком мало?
Нужно ли вручную включать epoll?
Что делать, если после изменения числа worker стало хуже?
Как понять, что проблема именно в цикле событий?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.