Nginx не умеет reload: почему на самом деле он запускает новое поколение процессов
Есть конфигурация, знакомая почти каждому администратору:
```nginx
server {
listen 8080;
keepalive_timeout 300s;
location /api {
proxy_pass http://backend;
}
}
```
Во время деплоя выполняется привычная команда:
```bash
nginx -s reload
```
Она завершается с кодом `0`, `nginx -T` показывает обновлённые параметры, а в `error.log` появляется единственная строка:
```text
reconfiguring
```
На первый взгляд кажется, что сервер просто перечитал файл и продолжил работу. Но это не совсем так. Внутри nginx не происходит классического "перезапуска конфигурации" в привычном смысле. Мастер-процесс собирает новый цикл работы, запускает новое поколение воркеров, а затем завершает старое. Поэтому точнее было бы сказать: nginx не умеет reload - он умеет fork.
Что происходит с keep-alive
На тестовом стенде выясняется неприятная деталь: keep-alive-соединение, созданное непосредственно перед reload, может быть закрыто в тот же момент. Само по себе это не нарушение протокола. Сервер имеет право закрыть простаивающее соединение, а корректный клиент должен установить новое.
Проблема появляется из-за гонки. Клиент может отправить следующий запрос уже после записи сервером пакета `FIN`, но до того, как операционная система окончательно сообщит ему о закрытии соединения. Идемпотентный запрос обычно можно повторить без последствий. Для `POST` повторная отправка способна привести к двойной операции, если приложение не использует идемпотентные ключи.
Именно поэтому фраза из документации о том, что старые рабочие процессы "продолжают обслуживать старых клиентов", не означает, что любое существующее TCP-соединение гарантированно доживёт до естественного завершения. Активный запрос действительно стараются не обрывать, а простаивающий keep-alive может быть закрыт немедленно.
Подробный разбор поведения разных серверов, включая экспериментальные стенды и анализ реализации, представлен в материале о том, почему nginx reload конфигурации без перезапуска фактически является запуском нового поколения процессов: как работает reload в nginx и почему это fork.
Как nginx обрабатывает SIGHUP
Команда `nginx -s reload` отправляет мастер-процессу сигнал `SIGHUP`. Обработчик сигнала выставляет флаг `ngx_reconfigure`, после чего мастер обрабатывает его в основном цикле.
Ключевая операция находится в `src/os/unix/ngx_process_cycle.c`. Вызов `ngx_init_cycle` не изменяет текущую структуру конфигурации на месте. Вместо этого nginx создаёт новый объект cycle целиком:
- отдельный пул памяти;
- новый массив модулей;
- собственные структуры `ngx_listening_t`;
- новые конфигурации всех модулей;
- отдельные параметры для будущих рабочих процессов.
Старый cycle продолжает существовать, потому что на него всё ещё ссылаются уже запущенные воркеры. Затем мастер вызывает `ngx_start_worker_processes` и форкает новые процессы. В течение некоторого времени на машине параллельно работают два поколения воркеров, причём каждое использует собственную конфигурацию.
Именно это и есть nginx fork процессы при reload: старые воркеры не превращаются в новые и не получают обновлённые настройки. Вместо этого создаются новые процессы, которым передаётся новый cycle.
Зачем нужен флаг JUST_RESPAWN
После запуска новых воркеров мастер должен отправить старым процессам сигнал `QUIT`. Для этого nginx вызывает `ngx_signal_worker_processes`.
Флаг `NGX_PROCESS_JUST_RESPAWN` нужен, чтобы новые процессы не получили сигнал завершения сразу после создания. Мастер пропускает такие процессы в текущем проходе, снимает с них отметку, а старым отправляет `QUIT`.
При этом nginx не поддерживает отдельный реестр поколений воркеров. Термин "старый процесс" в данном месте означает не возраст процесса в общем смысле, а наличие конкретного состояния, которое ещё не было сброшено во время текущей процедуры reload.
Отдельно nginx хранит старые cycle в `ngx_old_cycles`, но этот список не используется для рассылки сигналов рабочим процессам. Это важное различие: поколения конфигураций существуют, а полноценного управления поколениями процессов в логике reload нет.
Жёсткое окно в 100 миллисекунд
Между fork новых воркеров и отправкой сигналов старым находится вызов:
```c
ngx_msleep(100);
```
В коде он сопровождается комментарием `/* allow new processes to start */`. Задержка зафиксирована непосредственно в исходниках и не вынесена в конфигурацию.
Задача паузы проста: дать новым процессам время запуститься и дойти до `accept()`, прежде чем старые перестанут принимать подключения. На типичной конфигурации ста миллисекунд достаточно. Но при большом количестве shared memory zones, тяжёлой инициализации Lua, многочисленных модулях или медленном хранилище этого может оказаться мало.
Так появляется короткий период, когда новые воркеры ещё не готовы, а старые уже получили команду завершения. Это не обязательно приводит к потере соединений, поскольку слушающий сокет принадлежит не воркеру, а мастер-процессу.
Мастер открывает дескриптор, а дочерние процессы получают его копию после fork. Когда старый воркер закрывает свой дескриптор через `ngx_close_socket`, закрывается только его ссылка на сокет. Сам объект в ядре продолжает существовать, как и очередь подключений. Новые SYN-пакеты могут продолжать поступать, даже если в конкретный момент их некому обработать.
При использовании `reuseport` механизм выглядит иначе на уровне количества сокетов, но принцип остаётся близким. Такие сокеты создаются мастером через `ngx_clone_listening`, после чего копии передаются воркерам. Поэтому ожидание, что `reuseport` полностью исключит описанное поведение, не оправдывается.
Сравнение с другими прокси
Особенность nginx становится заметнее при сравнении с другими серверами. Apache HTTP Server и HAProxy, несмотря на разный возраст, архитектуру и внутренние механизмы, в проверенном сценарии демонстрируют практически тот же внешний контракт: активный запрос не обрывается, а idle keep-alive может быть закрыт во время применения новой конфигурации.
Traefik ведёт себя иначе. В одном из тестов именно он позволил второму запросу по тому же соединению получить уже обновлённый ответ. Это не означает, что Traefik всегда "лучше" или что nginx работает неправильно. Просто у этих продуктов различаются модель владения соединением и способ применения конфигурации.
Сравнение проводилось на фиксированных версиях: nginx `release-1.31.3`, Apache `2.4.68`, Traefik `v3.7.10` и HAProxy `2.6.12-1+deb12u3`. nginx и Apache собирались из соответствующих тегов, HAProxy использовался в виде пакета Debian. Все замеры выполнялись в одном стенде с единым циклом сборки и запуска контейнеров.
Практические последствия для деплоя
Из этого следует важный вывод: успешное выполнение `nginx -s reload` не гарантирует, что каждый клиент продолжит использовать прежнее TCP-соединение. Команда подтверждает, что мастер принял сигнал, собрал новую конфигурацию и начал переключение процессов. Она не обещает сохранение idle keep-alive.
Поэтому приложение должно корректно реагировать на закрытие соединения. Для безопасного повторения запросов полезны идемпотентные операции, заголовки вроде `Idempotency-Key`, повторная отправка только для заранее разрешённых методов и разумные таймауты на стороне клиента.
При возникновении проблем полезно разделять несколько ситуаций. Если новая конфигурация не применяется, требуется ошибки nginx reload диагностика: проверка `nginx -t`, содержимого `error.log`, состояния мастер-процесса и фактической командной строки. Если соединения рвутся только во время деплоя, нужно анализировать keep-alive, балансировщик, клиентские retry и длительность инициализации воркеров.
Для сложных конфигураций также важна nginx graceful reload настройка: корректные таймауты, отсутствие слишком долгих операций в старте воркера, контроль длительных запросов и наблюдение за количеством процессов. Сам reload не является магической бесшовной заменой конфигурации - это управляемая смена поколений.
Итог прост: nginx действительно умеет обновлять конфигурацию без полного перезапуска мастер-процесса, но делает это не редактированием работающего состояния, а построением нового cycle и fork новых воркеров. Поэтому корректная формулировка - nginx reload не работает как обычный reload. Он запускает новую конфигурацию рядом со старой, даёт новым процессам короткое время на старт и затем просит старое поколение завершиться.


