1. 1
  2. 2
  3. 3
  4. 4

# теория · шаг 2 из 4

Обновление бинарника, сигналы и systemd

Коротко

nginx умеет заменить сам себя без разрыва соединений: USR2 поднимает второй мастер рядом со старым, оба слушают те же сокеты; WINCH гасит старые воркеры, QUIT добивает старый мастер. Пока он жив, откат стоит две команды. Приём требует, чтобы nginx был запущен абсолютным путём - иначе USR2 молча не сработает. QUIT даёт запросам доработать, TERM рвёт их на полуслове. С systemd этот приём не дружит, а в контейнере не нужен вовсе.

Нужен только порядок команд - тебе хватит «Правило» и первого разбора.

Ты шлёшь USR2, ждёшь появления второго мастера - и ничего не происходит. Процесс один, nginx.pid.oldbin не создан, сайт работает как работал. В логе:

↳  вывод/var/log/nginx/error.log
[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 запускали.

На что это похоже

Обычная замена программы - это «выключить и включить»: дверь закрыта, пока меняют вывеску. Здесь другое: в зал заводят вторую смену с новой вывеской, и какое-то время работают обе - за одними и теми же дверями.

Ключевое слово - двери. Слушающие сокеты открыты один раз, и новый мастер их наследует, а не открывает заново. Поэтому между двумя версиями нет ни одного мгновения, когда порт никем не занят, - и поэтому же откат мгновенный: старая смена всё это время стоит рядом с ключами в кармане.

Механизм: два мастера на одних сокетах

слушающие сокеты :80 и :443 старый мастер nginx.pid.oldbin новый мастер nginx.pid откат: HUP старому и QUIT новому, сокеты не закрывались
Пока старый мастер жив, вернуться назад стоит двух команд

Правило

$  команда
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

Разбор: полный цикл со стенда

↳  выводps -o args, после старта
nginx: master process /usr/sbin/nginx -c /etc/nginx/nginx.conf
nginx: worker process

После USR2 процессов вчетверо больше, а pid-файлов два:

↳  выводсводка: два мастера в ps и два pid-файла в /run после USR2
два мастера: старый (pid 9) и новый (pid 24), у каждого свои воркеры
в /run: nginx.pid (новый) и nginx.pid.oldbin (старый)
сайт при этом отвечает 200 без перерыва

После WINCH старому у него не остаётся воркеров, но сам он жив:

↳  выводсводка ps после 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 с микроскопическим, но реальным разрывом. Если разрыв недопустим, сервер выводят из балансировки, обновляют и возвращают (восьмой раздел) - это честнее, чем воевать с юнитом.

↳  выводsystemctl cat nginx
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 - там обновляются пакетом и рестартом, а без простоя выводят сервер из балансировки. В контейнере всё это заменяет замена контейнера.

Комментарии

Пока нет комментариев. Будь первым!

Оставить комментарий

Комментарий появится после проверки. Email не публикуется. Войти, чтобы не вводить имя каждый раз.