# теория · шаг 2 из 4
Обновление бинарника, сигналы и systemd
Коротко
nginx умеет заменить сам себя без разрыва соединений: USR2 поднимает второй
мастер рядом со старым, оба слушают те же сокеты; WINCH гасит старые воркеры,
QUIT добивает старый мастер. Пока он жив, откат стоит две команды. Приём
требует, чтобы nginx был запущен абсолютным путём - иначе USR2 молча не
сработает. QUIT даёт запросам доработать, TERM рвёт их на полуслове. С
systemd этот приём не дружит, а в контейнере не нужен вовсе.
Нужен только порядок команд - тебе хватит «Правило» и первого разбора.
Ты шлёшь USR2, ждёшь появления второго мастера - и ничего не происходит.
Процесс один, nginx.pid.oldbin не создан, сайт работает как работал. В логе:
[notice] 9#9: changing binary
[notice] 9#9: start new binary process 24
[alert] 24#24: execve() failed while executing new binary process "nginx" (2: No such file or directory)
Файл на месте, права на месте. Причина в том, как nginx запускали.
На что это похоже
Обычная замена программы - это «выключить и включить»: дверь закрыта, пока меняют вывеску. Здесь другое: в зал заводят вторую смену с новой вывеской, и какое-то время работают обе - за одними и теми же дверями.
Ключевое слово - двери. Слушающие сокеты открыты один раз, и новый мастер их наследует, а не открывает заново. Поэтому между двумя версиями нет ни одного мгновения, когда порт никем не занят, - и поэтому же откат мгновенный: старая смена всё это время стоит рядом с ключами в кармане.
Механизм: два мастера на одних сокетах
Правило
kill -USR2 $(cat /run/nginx.pid) # pid - номер процесса; /run/nginx.pid хранит pid мастера. новый мастер рядом со старым
kill -WINCH $(cat /run/nginx.pid.oldbin) # погасить старые воркеры
kill -QUIT $(cat /run/nginx.pid.oldbin) # всё хорошо - добить старого
Если новая версия ведёт себя плохо, вместо последней строки идут две другие:
kill -HUP $(cat /run/nginx.pid.oldbin) # старый снова поднимает воркеров
kill -QUIT $(cat /run/nginx.pid) # новый уходит
| Сигнал | Что делает | Через nginx -s |
|---|---|---|
HUP |
перечитать конфиг, сменить воркеров | reload |
USR1 |
переоткрыть лог-файлы | reopen |
USR2 |
запустить новый бинарник рядом | - |
WINCH |
погасить воркеров, мастер остаётся | - |
QUIT |
завершиться плавно | quit |
TERM |
завершиться немедленно | stop |
Разбор: полный цикл со стенда
nginx: master process /usr/sbin/nginx -c /etc/nginx/nginx.conf
nginx: worker process
После USR2 процессов вчетверо больше, а pid-файлов два:
два мастера: старый (pid 9) и новый (pid 24), у каждого свои воркеры
в /run: nginx.pid (новый) и nginx.pid.oldbin (старый)
сайт при этом отвечает 200 без перерыва
После WINCH старому у него не остаётся воркеров, но сам он жив:
остаются два мастера, но воркеры только у нового; сайт отвечает 200
После отката (HUP старому, QUIT новому) остаётся один мастер с воркером,
nginx.pid.oldbin исчезает - имя возвращается прежнему процессу. Сайт на всех
четырёх шагах отвечал 200: ни одного мгновения без обслуживания.
Разбор: QUIT против TERM в цифрах
Разница «дай доработать» и «выключи сейчас» звучит абстрактно, пока её не измеришь. Стенд: ответ приходит порциями десять секунд, сигнал шлём на третьей.
| сигнал | получено клиентом | код curl | мастер через 2 с |
|---|---|---|---|
QUIT |
100 байт из 100 | 0 | ещё жив |
TERM |
40 байт из 100 | 18 (частичная передача) | уже нет |
TERM не «чуть быстрее», он рвёт ответ на середине - клиент получает битый
файл и код успеха в заголовке. В скриптах выкладки должен стоять QUIT, а
TERM остаётся на случай «процесс завис и надо убрать сейчас».
Что ломается без этого
USR2, запущенный не тем способом. nginx перезапускает себя через
execve (системный вызов «запустить программу поверх себя») с тем именем, каким его позвали. Если в командной строке стояло просто
nginx, аргумент так и остаётся nginx - а execve не ищет по PATH, отсюда
(2: No such file or directory) из крючка. Замер: тот же конфиг, тот же
контейнер, разница только в способе запуска.
| запуск | результат USR2 |
|---|---|
nginx -c ... |
[alert] execve() failed, мастер один, ничего не изменилось |
/usr/sbin/nginx -c ... |
два мастера, nginx.pid.oldbin, сайт 200 |
Init-скрипты и systemd всегда зовут абсолютным путём, поэтому в жизни на это натыкаются те, кто поднял nginx руками для проверки.
systemd и главный PID. Юнит следит за одним процессом. После USR2
главным становится новый мастер, о котором systemd не знает; QUIT старому -
и юнит считает, что сервис умер. Поэтому в дистрибутивных пакетах путь другой:
обновление менеджером пакетов и systemctl restart с микроскопическим, но
реальным разрывом. Если разрыв недопустим, сервер выводят из балансировки,
обновляют и возвращают (восьмой раздел) - это честнее, чем воевать с юнитом.
ExecStartPre=/usr/sbin/nginx -t
ExecStart=/usr/sbin/nginx
ExecReload=/usr/sbin/nginx -s reload
Строка ExecStartPre важнее, чем кажется: без неё systemctl restart с битым
конфигом гасит рабочий сервис и не поднимает новый.
Контейнер. Там мастер - это PID 1, логи - симлинки на stdout и stderr,
а обновление версии - пересборка образа. Ни USR2, ни logrotate не нужны и
работают не так, как задумано. Порядок другой: новый образ → новый контейнер
рядом → проверка → переключение трафика → удаление старого. То же обновление
без простоя, только уровнем выше.
Зачем это в работе
Честно: на машине с systemd ты этим приёмом, скорее всего, не воспользуешься.
Знать его надо по трём причинам. Он объясняет, почему reload ничего не стоит:
механизм наследования сокетов один и тот же. Он выручает на серверах без
systemd и в старых установках, где nginx поднят руками. И он даёт правильную
рамку для контейнерной выкладки: «поднять новое рядом, проверить, переключить,
убрать старое» - тот же порядок, просто вместо процессов контейнеры.
А вот QUIT против TERM пригодится сразу и везде: это разница между
аккуратной остановкой и оборванными ответами у половины пользователей.
Теперь сам
На сервере без systemd ты обновил бинарник, послал USR2, увидел два мастера,
погасил старые воркеры через WINCH - и через минуту в мониторинге всплеск
502. Что делать и какая команда возвращает всё назад быстрее всего?
Ответ: откат - kill -HUP $(cat /run/nginx.pid.oldbin), старый мастер тут же
поднимает воркеров со своим бинарником и конфигом, после чего новый убирается
kill -QUIT $(cat /run/nginx.pid). Именно ради этой минуты старый мастер и
оставляют жить: сокеты не закрывались, откат не требует ни перезапуска, ни
правки конфига. Если бы старый мастер был уже добит QUIT, вернуться можно
было бы только установкой прежней версии и рестартом - с разрывом.
Главное
Замена бинарника на лету: USR2 (новый мастер рядом, старый pid-файл
становится nginx.pid.oldbin) → WINCH (погасить старые воркеры, мастер
остаётся) → QUIT старому. Пока старый жив, откат - это HUP ему и QUIT
новому; сокеты при этом не закрывались ни разу, замер показал 200 на каждом
шаге. USR2 требует запуска абсолютным путём: иначе execve() не найдёт
бинарник и в логе останется [alert], а мастер не изменится. QUIT даёт
запросу доработать (100 байт из 100), TERM рвёт его (40 из 100 и ошибка у
клиента). С systemd приём не дружит из-за смены главного PID - там обновляются
пакетом и рестартом, а без простоя выводят сервер из балансировки. В контейнере
всё это заменяет замена контейнера.
Комментарии
Пока нет комментариев. Будь первым!
Оставить комментарий