Graceful shutdown: SIGTERM и дренаж соединений
Почему приложение должно корректно завершаться по SIGTERM, как дослуживать запросы и не рвать соединения при деплое.
Что происходит при деплое
Любой выкат из прошлого трека (rolling, blue-green) останавливает старую
копию приложения. Оркестратор не убивает процесс сразу: он шлёт SIGTERM
и ждёт. Если приложение умеет на это реагировать, оно успевает доделать
текущие запросы. Если нет, оркестратор через таймаут шлёт SIGKILL, и все
открытые соединения рвутся вместе с недообработанными запросами.
Graceful shutdown значит завершиться аккуратно: перестать брать новое, доделать начатое, освободить ресурсы.
Сигналы из прошлого урока
SIGTERM (15) — вежливая просьба завершиться, её можно перехватить.
SIGKILL (9) перехватить нельзя, процесс умирает мгновенно. Оркестратор и
docker stop всегда шлют сначала SIGTERM, ждут (дефолт зависит от
системы: docker stop около 10 секунд, Kubernetes 30 секунд), и только
потом SIGKILL.
Задача приложения: поймать SIGTERM и за отведённое время свернуться
правильно.
Порядок корректного завершения
Когда пришёл SIGTERM, делают по шагам:
- Перестать принимать новое. Закрыть приёмный сокет, чтобы новые запросы не приходили.
- Дослужить начатое. Дать текущим запросам завершиться (это и есть дренаж, draining).
- Закрыть ресурсы. Соединения с базой, файлы, продюсеры очередей.
- Выйти с кодом 0.
Схематично, на псевдокоде:
on SIGTERM:
server.stop_accepting() # п.1
server.wait_active_requests(timeout=25s) # п.2
db.close(); queue.close() # п.3
exit(0) # п.4
Откуда берутся 502 при «правильном» завершении
Первый шаг звучит безобидно, но именно на нём чаще всего и ловят ошибки. Логика «закрою сокет, а балансировщик сам уберёт копию из ротации» не работает, потому что порядок событий обратный.
В Kubernetes при удалении Pod запускаются два процесса параллельно, и никто их не синхронизирует:
- kubelet шлёт контейнеру
SIGTERM; - контроллер эндпоинтов убирает Pod из Endpoints, дальше это изменение разъезжается по kube-proxy на всех узлах, по Ingress-контроллерам и по клиентским балансировщикам.
Первое занимает миллисекунды, второе — от сотен миллисекунд до нескольких секунд, в зависимости от размера кластера. Получается окно, в котором приложение уже закрыло сокет, а трафик на него ещё идёт. Каждый такой запрос упирается в connection refused, и клиент видит 502 или 503 на ровном деплое.
Лечится это не в коде, а паузой перед завершением. Приложение должно
продолжать обслуживать запросы ещё несколько секунд после SIGTERM —
ровно столько, сколько расходится информация об удалении эндпоинта
(propagation delay).
Проще всего добавить хук preStop. Kubernetes выполняет его до
SIGTERM, а удаление из эндпоинтов в это время уже идёт:
lifecycle:
preStop:
exec:
command: ["sleep", "10"]
terminationGracePeriodSeconds: 45
Контейнер эти 10 секунд работает как обычно и отвечает на запросы. К
моменту, когда придёт SIGTERM, балансировщики уже знают, что копии
нет, и новых соединений не пришлют.
Второй вариант, если менять манифест не хочется: обработчик SIGTERM
сначала переводит readiness в «не готов», спит несколько секунд и только
потом закрывает сокет. Эффект тот же.
Про terminationGracePeriodSeconds не забудь: пауза preStop считается
внутри него. Десять секунд сна плюс двадцать пять на дренаж не влезут в
дефолтные тридцать, и приложение прибьют на середине.
Таймаут на стороне приложения и оркестратора
Здесь есть две связанные величины. Оркестратор ждёт SIGTERM-таймаут
(grace period) перед SIGKILL. Приложение должно уложить свой дренаж в это
же время. Если приложению нужно 25 секунд, а grace period 10, оркестратор
прибьёт его на середине. Поэтому grace period настраивают под самый долгий
запрос.
В Kubernetes это terminationGracePeriodSeconds, в systemd
TimeoutStopSec, в docker stop флаг -t.
Частая ошибка с PID 1
В контейнере приложение часто запущено как PID 1. Из урока про процессы:
PID 1 особенный, и сигналы ему доставляются иначе. Если приложение
запущено через шелл (sh -c "myapp"), SIGTERM поймает шелл, а не само
приложение, и graceful shutdown не сработает. Поэтому в контейнере
приложение запускают exec-формой (CMD ["myapp"]), чтобы оно стало PID 1
и получало сигналы напрямую.
Что важно запомнить
Оркестратор шлёт SIGTERM, ждёт grace period и только потом SIGKILL.
Приложение должно за это время перестать принимать новое, дослужить
начатое, закрыть ресурсы и выйти с кодом 0.
Дренаж приложения согласуют с grace period оркестратора, иначе его прибьют на середине. В контейнере приложение запускают exec-формой, чтобы оно было PID 1 и получало сигналы напрямую.
Закрывать приёмный сокет сразу по SIGTERM нельзя: в Kubernetes
удаление эндпоинта идёт параллельно сигналу и доезжает до
балансировщиков позже. Пауза через preStop: sleep (или сон в самом
обработчике) закрывает это окно и убирает 502 при деплое.
В следующем уроке разберём health-эндпоинты: как оркестратор и балансировщик понимают, что приложение живо и готово.
Прошли материал до конца? Отметьте урок пройденным.
Прогресс сохранён в этом браузере
Почистишь историю или откроешь с телефона, и отметки пропадут. Оставь почту, они переедут в аккаунт. Пароль придумывать не нужно.
Готово. теперь в аккаунте: прогресс виден с любого устройства. Мой прогресс