Почему `nginx -s reload` иногда не применяет конфигурацию
Перезагрузка конфигурации nginx обычно воспринимается как безопасная операция: мастер-процесс получает сигнал, запускает новые worker-процессы, а старые постепенно завершают работу. Однако у этой схемы есть важные исключения. В некоторых сценариях команда `nginx -s reload` завершается с кодом 0, не показывает ошибок, но часть запросов продолжает обслуживаться по старой конфигурации.
Эксперименты проводились на nginx 1.31.3, собранном из тега `release-1.31.3`, в Ubuntu 22.04.5 с ядром 6.8.0. В тестовом окружении использовались два CPU-ядра, 4 ГБ оперативной памяти и loopback-интерфейс. Клиент, nginx и тестовый backend работали внутри одного контейнера. Все проверки выполнялись на HTTP/1.1; поведение HTTP/2 и QUIC в рассмотрение не включалось.
Главный вывод экспериментов таков: во время бинарного обновления nginx команда `systemctl reload nginx`, а вместе с ней и обычный `nginx -s reload`, может направить сигнал не тому мастер-процессу. В результате новая конфигурация применяется только к части процессов либо не применяется вовсе. При этом команда формально считается успешной: возвращается нулевой код, а в `error.log` может не появиться ни одной записи.
Что происходит при бинарном обновлении
Бинарное обновление nginx позволяет заменить исполняемый файл без остановки текущих соединений. Сначала старый мастер получает `USR2` и запускает новый мастер уже с обновлённым бинарным файлом. Некоторое время оба процесса существуют параллельно, используют собственные копии конфигурации и обслуживают входящие соединения. После проверки нового процесса старому мастеру отправляют `QUIT`.
Проблема возникает именно в промежуточном состоянии. В этот момент в системе одновременно находятся два независимых мастера. Старый процесс переименовывает свой PID-файл в `.oldbin`, а новый начинает использовать прежнее имя PID-файла. Поэтому команда перезагрузки может обратиться только к одному из двух мастеров.
В стандартном Debian-юнитe встречается вариант, при котором сигнал отправляется процессу, указанному в PID-файле. В самописных конфигурациях systemd нередко используется PID, сохранённый менеджером при первоначальном запуске сервиса. Во время бинарного обновления эти PID уже относятся к разным процессам.
Если `reload` обращается к новому мастеру, он перечитывает конфигурацию и запускает новые worker-процессы. Старый мастер при этом продолжает обслуживать запросы со старой конфигурацией. Если сигнал попадает старому мастеру, ситуация меняется: новый процесс может остаться с уже загруженной конфигурацией, а ожидаемое обновление фактически не затронет всю группу nginx.
Именно поэтому результат теста не обязательно выглядит как равномерное распределение запросов между двумя версиями. В одном из прогонов 27 из 30 запросов обработал старый конфиг, а новый ответил только на три. В серии повторных запусков доля запросов от старого мастера менялась примерно от 37 до 93 процентов. Ожидать стабильных 50/50 нельзя: оба мастера используют общий унаследованный listening socket, а распределение зависит от планировщика, момента подключения и внутренней конкуренции процессов.
Практически это означает, что во время бинарного обновления необходимо явно понимать, какой мастер получает сигнал. Для диагностики полезно добавлять в ответы служебные заголовки вроде `X-Config` и `X-Worker: $pid`. Такая маркировка позволяет увидеть, какая версия конфигурации и какой worker фактически обслужили запрос.
Подробное описание экспериментов, включая проверку поведения мастеров и worker-процессов, представлено в материале о том, почему nginx -s reload может не применить конфиг. Там же приведены параметры стенда и результаты повторных запусков.
Почему нулевой код возврата не гарантирует успех
Команда `nginx -s reload` не проверяет, что все существующие мастер-процессы действительно получили сигнал и синхронно загрузили новую конфигурацию. Она лишь отправляет управляющий сигнал выбранному PID. Если адресат существует и операция отправки прошла успешно, оболочка получает код 0.
Поэтому проверка конфигурации nginx перед reload - необходимая, но не достаточная мера. Команда `nginx -t` подтверждает синтаксическую корректность файла, однако не сообщает, какой именно мастер впоследствии перечитает конфигурацию. В результате администратор может увидеть успешный тест, успешный reload и при этом продолжить получать ответы от старых worker-процессов.
Надёжнее проверять не только журнал systemd, но и фактическое состояние процессов:
```bash
nginx -t
systemctl reload nginx
ps -ef | grep nginx
cat /run/nginx.pid
cat /run/nginx.pid.oldbin
```
Если одновременно существуют основной PID и файл `.oldbin`, бинарное обновление ещё не завершено. В этот момент автоматическая настройка nginx сервера должна учитывать наличие двух мастеров, иначе управляющая команда может воздействовать только на один из них.
Другие особенности reload
При обычном reload уходящий worker не прекращает обслуживать уже принятые соединения мгновенно. Он завершает работу после обработки текущих запросов. Для keep-alive это особенно важно: клиент может продолжать использовать существующее соединение, а поведение зависит от параметров таймаутов и версии протокола.
Для HTTP/2 уходящий worker отправляет `GOAWAY`, после чего новые потоки на этом соединении не принимаются. В HTTP/1.1 картина иная: уже установленное keep-alive-соединение может некоторое время обслуживаться старым процессом. Поэтому утверждение "после reload каждое соединение сразу видит новый конфиг" некорректно. На практике старый конфиг закрепляется не столько за соединением, сколько за процессом, который это соединение обслуживает.
Отдельно проверялось поведение keepalive-пулов к upstream-серверам. При reload такой пул может сбрасываться целиком, из-за чего после обновления конфигурации backend временно получает больше новых TCP-соединений. Это не всегда заметно на небольших нагрузках, но способно вызвать кратковременный рост задержек или нагрузки на приложение.
Механизм `reuseport`, вопреки распространённым опасениям, сам по себе не разрушает бесшовную перезагрузку. Новые worker-процессы получают возможность принимать соединения, а старые завершают обслуживание уже принятого трафика. Отдельная проверка также не подтвердила гипотезу о заметной паузе в `accept`, из-за которой SYN-пакеты должны массово накапливаться в backlog.
Как безопаснее применять конфигурацию
Перед изменением настроек следует выполнить `nginx -t`, затем проверить процессную модель и только после этого отправлять сигнал. Во время бинарного обновления лучше не полагаться на универсальный `systemctl reload`, если неизвестно, какой PID использует unit-файл.
Полезно контролировать результат по нескольким признакам:
- изменился ли PID worker-процесса;
- появились ли ожидаемые значения в тестовом заголовке или ответе;
- исчез ли `.oldbin` после завершения обновления;
- нет ли ошибок загрузки модулей и upstream;
- действительно ли новые запросы обслуживаются новым конфигом.
Если nginx не применяет конфигурацию, не стоит сразу повторять reload вслепую. Сначала нужно определить, сколько мастер-процессов запущено, кому принадлежит PID-файл и не продолжается ли бинарный апгрейд. В противном случае повторная команда может снова попасть не в тот процесс.
Таким образом, reload в nginx остаётся безопасным механизмом, но только пока процессная модель прозрачна. Обычная перезагрузка конфигурации и бинарное обновление - разные сценарии, и во втором случае стандартные команды управления могут вести себя не так, как ожидает администратор. Именно различие между "сигнал отправлен" и "вся система перешла на новый конфиг" чаще всего становится причиной трудноуловимых ошибок nginx конфигурации.


