nginx -s reload может не применить конфиг: разбор пяти мифов

Перезагрузка конфигурации nginx — не всегда то, чем кажется. Эксперименты на стенде показали: systemctl reload может не применить конфиг вовсе, а keepalive-соединения ведут себя неожиданно. Разбираем, что реально происходит при reload.

nginx -s reload может не применить конфиг: разбор пяти мифов

Перезагрузка конфигурации nginx — рутинная операция, которую выполняют тысячи раз в день. Но что если reload не применяет конфиг так, как вы думаете? Автор статьи на Habr провёл серию экспериментов с nginx 1.31.3 и Traefik v3.7.10, чтобы проверить пять ходовых утверждений о reload. Результаты оказались неожиданными: часть мифов развалилась, а подтвердились именно те вещи, о которых почти не пишут.

nginx -s reload: когда конфиг не применяется

Главный вывод эксперимента: во время бинарного апгрейда nginx команда systemctl reload nginx может не применить конфиг ни к одному процессу. В худшем случае новый конфиг не увидит ни один воркер, и при этом код возврата будет ноль, а в error.log — пусто. Причина кроется в том, как systemd отправляет сигналы.

Когда вы выполняете systemctl reload nginx, systemd отправляет сигнал процессу, указанному в MAINPID. Но если nginx был обновлён через USR2, pid-файл уже принадлежит новому мастер-процессу. Старый мастер, который ещё жив, продолжает управлять старыми воркерами. Если ExecReload в юните настроен как -s reload, то сигнал уходит новому мастеру, и он перечитывает конфиг только для своих воркеров. Старые воркеры остаются на старом конфиге. Если же используется kill -s HUP $MAINPID, то сигнал получает старый мастер, который вообще не перечитывает конфиг — это задокументировано в разделе про обновление исполняемого файла.

На практике это означает: если вы обновили nginx и затем сделали reload, вы можете получить ситуацию, когда половина воркеров работает по новому конфигу, а половина — по старому. Или вообще все по старому. При этом ни одна ошибка не появится в логах, и команда завершится успешно. Это особенно опасно, если вы меняете конфиг для применения критических изменений — например, правил доступа или SSL-сертификатов.

Предыстория и контекст

Проблема не нова, но о ней мало говорят. В документации nginx описано, что при обновлении исполняемого файла старый мастер продолжает работать до тех пор, пока не получит сигнал QUIT. Однако reload-сигналы обрабатываются по-разному в зависимости от того, какой мастер их получает. Эта особенность известна опытным администраторам, но большинство разработчиков и DevOps-инженеров не заглядывают в документацию по обновлению бинарника, когда сталкиваются с проблемами reload.

Автор эксперимента решил проверить не только эту проблему, но и другие распространённые утверждения о reload. Он собрал стенд, запустил nginx и Traefik, и проверил каждое утверждение на практике. Результаты показали, что многие «страшилки» не соответствуют действительности, а реальные проблемы скрыты там, где их не ждут.

Как работает reload на самом деле?

Чтобы понять, почему reload может не применить конфиг, нужно разобраться в механике. При nginx -s reload мастер-процесс отправляет сигнал HUP воркерам, после чего они перечитывают конфигурацию. Но когда есть два мастера (старый и новый), сигнал уходит только одному из них. Если reload отправляется через systemd, он использует MAINPID, который может указывать на новый мастер, в то время как старые воркеры всё ещё подчиняются старому мастеру. В результате конфиг применяется частично или не применяется вовсе.

Эксперимент показал, что kill -s HUP $MAINPID в юните — это худший вариант, так как старый мастер игнорирует HUP при обновлении. А -s reload в юните — лучший вариант, но он всё равно не затрагивает старые воркеры. Поэтому если вы используете бинарный апгрейд, после него нужно либо полностью перезапускать nginx, либо аккуратно отправлять сигналы вручную.

Технические подробности: keepalive и reload

Одно из подтверждённых утверждений — поведение keepalive-соединений. Когда воркер получает сигнал на завершение, он должен закрыть все соединения. Но keepalivemintimeout позволяет воркеру оставаться в живых на некоторое время, чтобы обслужить уже установленные keepalive-соединения. Это приводит к тому, что уходящий воркер продолжает принимать новые запросы по старому конфигу, даже если reload уже произошёл. То есть соединение, открытое до reload, будет обслуживаться по старому конфигу, и это нормально — но если воркер живёт дольше, чем ожидалось, это может создать путаницу.

Более серьёзная проблема — сброс пула keepalive к бэкендам. Функция ngxcloseidleconnections при reload закрывает все idle-соединения, не различая их направление. Это значит, что каждый reload сбрасывает пул keepalive-соединений к апстримам. Начиная с версии 1.29.7, пул включён по умолчанию для всех, у кого есть блок upstream, и составляет 32 соединения на воркер. Таким образом, reload приводит к тому, что все keepalive-соединения к бэкендам закрываются, и они устанавливаются заново, что увеличивает задержки и нагрузку на бэкенды.

Эксперимент также опроверг два мифа. Первый — про reuseport: автор проверил, что inode’ы сокетов до и после reload остаются теми же, потому что сокеты держит мастер, а не воркер. Значит, reuseport не ломает бесшовность reload. Второй миф — про паузы в accept: автор обнаружил, что msleep(100) стоит перед QUIT, но это не вызывает паузы в accept, и переполнение backlog на reload — это проблема нагрузки, а не reload.

Кого затронет и как

Эти проблемы касаются всех, кто использует nginx в production: DevOps-инженеров, системных администраторов, разработчиков, которые настраивают CI/CD. Если вы обновляете nginx и затем делаете reload, вы рискуете получить несоответствие конфигурации. Это особенно критично для проектов, где конфиг меняется часто — например, при динамическом добавлении серверов или изменении правил маршрутизации.

Для пользователей в России и СНГ, где nginx — стандарт де-факто для веб-серверов, эта проблема актуальна вдвойне. Многие компании используют nginx в связке с systemd, и не знают о подводных камнях. Рекомендация проста: после бинарного апгрейда лучше выполнить полный перезапуск (systemctl restart nginx), а не reload, чтобы гарантированно применить конфиг. Если же reload необходим, проверьте, какой мастер получает сигнал, и убедитесь, что все воркеры перезапущены.

Что будет дальше

Автор эксперимента планирует продолжить исследование и опубликовать полный репозиторий с кодом для воспроизведения. Это позволит другим инженерам проверить утверждения на своих стендах. В ближайшее время стоит ожидать обсуждения в сообществе и, возможно, изменения в документации nginx, чтобы предупредить пользователей о нюансах reload при бинарном апгрейде.

Также интересно, как поведёт себя Traefik, который не имеет reload в классическом понимании. Вместо этого он использует кольцевой буфер на одно сообщение и двухсекундный дроссель, что может приводить к задержкам применения конфига. Это отдельная тема для исследования.

Итог

Эксперимент показал, что reload в nginx — не такая простая операция, как кажется. Главный вывод: во время бинарного апгрейда systemctl reload может не применить конфиг, и нужно быть осторожным. Также важно помнить, что keepalive-соединения сбрасываются при каждом reload, что может влиять на производительность. Если вы используете nginx, проверьте свои сценарии обновления и reload, чтобы избежать неожиданных проблем. Следите за новыми публикациями автора — он обещает раскрыть ещё больше деталей.