Прямой ответ
Два механизма делают деплои невидимыми для пользователей. Graceful shutdown: по SIGTERM сервис прекращает принимать новые соединения, дорабатывает активные запросы (в пределах таймаута), закрывает ресурсы (БД-пулы, клиенты) и выходит — systemd/Docker/Kubernetes отправляют SIGTERM и ждут grace-период до SIGKILL. Health-проверки: отдельная от «процесс жив» семантика — readiness («готов принимать трафик»: зависимости доступны, прогрев завершён; не готов — трафик отводится, но процесс не убивают) и liveness («процесс в сознании»: провал — перезапуск). Частая ошибка — один /health для всего: liveness-проверка, ходящая в БД, устраивает шторм перезапусков при каждом тормозе базы (сервис «лечат» убийством, хотя он был здоров и просто ждал зависимость). Правильная пара: liveness — поверхностный (процесс отвечает), readiness — с реальными зависимостями.
Graceful shutdown: механика по слоям
Сигнальная цепочка: оркестратор шлёт SIGTERM → приложение ловит и начинает остановку → по истечении grace-тайма (systemd TimeoutStopSec, docker stop_grace_period, K8s terminationGracePeriodSeconds) — SIGKILL без разговоров. Что должно произойти за это окно в приложении: закрыть слушающий сокет (новые соединения отклоняются — nginx перед ним получит connection refused и уйдёт на другие инстансы), дождаться активных запросов (uvicorn/gunicorn делают это штатно по SIGTERM — ваша задача не мешать и не перехватывать сигнал «своей обработкой» без доработки), закрыть фоновые задачи (Celery-воркер имеет warm shutdown — задачи возвращаются в очередь при обрыве, отсюда идемпотентность), освободить пулы БД. Ловушки: PID 1 в контейнере не пересылает сигналы (приложение за sh -c не получает SIGTERM вовсе — лечится exec-формой CMD или tini-инициализатором); долгая обработка запроса > grace-тайма (либо укорачивайте запросы, либо поднимайте таймаут осознанно); подключение к shutdown-хукам закрытия всего на свете с сетевыми вызовами (остановка сама становится ненадёжной — закрывайте только необходимое).
Readiness против liveness: разные вопросы
Readiness отвечает «слать ли трафик»: проверка реальных предпосылок обработки (подключение к БД живёт, обязательные клиенты инициализированы, кеш прогрет) — провал отводит трафик (K8s убирает pod из Service, nginx upstream помечает down), но процесс продолжает работать и может выздороветь. Liveness отвечает «не завис ли процесс намертво» (deadlock, бесконечный цикл, исчерпание потоков) — проверка максимально дешёвая и без внешних зависимостей (отдать 200 из памяти); провал означает «перезапуск — единственное лекарство». Почему их нельзя смешивать: readiness с БД при деградации БД отведёт трафик от всех инстансов (правильно), liveness с БД — перезапустит все инстансы одновременно (катастрофа: перезапуски не чинят БД, а холодные инстансы ещё и греются). Startup probe — третий вид для медленных стартов (тяжёлая инициализация модели): пока не пройдена, liveness не спрашивают — иначе медленный старт уходит в цикл убийств.
Health-эндпоинты: реализация без граблей
Практика для Python-сервиса: два роута. /livez — мгновенный 200 без зависимостей (процесс и event loop живы; в async-приложении сам факт ответа уже проверяет loop). /readyz — быстрая проверка предпосылок: SELECT 1 в БД с коротким таймаутом (не «тяжёлый запрос», а ping), ping Redis, статус критичных клиентов; ответ 503 с JSON-детализацией (какая зависимость не готова — дежурный видит причину без логов). Грабли: таймауты внутри readyz обязательны (проверка, висящая минуту, хуже отсутствия проверки); readyz не должен быть дорогим (его дёргают каждые 5–10 с с каждого инстанса — SELECT 1 с таймаутом 2 с, не пересчёт статистики); кеш результатов на 1–2 с при агрессивных опросах; и health-роуты не логируются в access-лог (половина «трафика» в логах — healthcheck'и; в nginx — access_log off на location — конфиг).
Оркестрация: кто когда кого останавливает
Порядок в Kubernetes (переносим мысленно на любой оркестратор): pod помечен Terminating → параллельно исключение из Service-endpoints (трафик уходит, но не мгновенно — propagation занимает секунды) и SIGTERM приложению → grace-период → SIGKILL. Отсюда тонкость «первых секунд»: запросы ещё могут приходить в останавливающийся pod (endpoints не обновились) — приложение уже отказывает, клиент получает ошибку. Лечение: preStop-hook с паузой в пару секунд (приложение ещё принимает, пока endpoints сходятся) — стандартный паттерн нуль-даунтайм деплоя. В systemd проще: KillSignal=SIGTERM (дефолт), TimeoutStopSec=30, KillMode=mixed (сигнал основному процессу, не всей cgroup сразу); nginx на той же машине переживает перезапуск приложения за счёт ретраев на upstream (или нескольких инстансов — стратегии деплоя). Docker-контейнеры: stop_grace_period в compose и exec-форма команды — два пункта чек-листа «контейнер корректно останавливается».
Практический сценарий: деплой без единой ошибки
Проверочный эксперимент, который стоит провести до настоящего релиза: нагрузочный клиент (locust) гонит 20 RPS на сервис из двух инстансов за nginx; деплоим новую версию по инстансу. Наблюдение без graceful-механики: на каждом рестарте — всплеск 502 и connection reset (процесс убит на середине запросов, nginx не знал). С механикой: SIGTERM → инстанс закрывает listener → nginx по healthcheck/ошибкам уводит трафик на соседа → активные запросы дорабатываются → выход через 3–5 с → новый инстанс стартует, проходит startup+readiness → возвращается в ротацию. Итоговый график ошибок — прямая линия на нуле; деплой занимает минуты, но невидим. Чек-лист, который делает этот результат повторяемым: exec-форма в Dockerfile, обработчики сигналов не перехвачены, readyz/livez разделены, grace-таймауты согласованы (приложение укладывается в оркестраторный), preStop-пауза, health-роуты вне логов — восемь пунктов, каждый из которых закрывает конкретный способ потерять запросы.
FAQ
Что делать с долгоживущими WebSocket при остановке? Закрывать корректно close-фреймом с кодом (клиенты переподключатся на живой инстанс — reconnect с backoff обязателен на клиенте); grace-таймаут на WS не растягивают до бесконечности — секунды на закрытие, дальше обрыв (механика WS — в статье про WebSocket).
Нужен ли graceful shutdown для CLI/скриптов? Да, в миниатюре: обработчик SIGINT/SIGTERM, закрывающий файлы и соединения и выходящий с внятным кодом (Ctrl-C в середине записи не должен оставлять рваные файлы — атомарная запись из bash-практик решает ту же задачу на уровне данных).
Как тестировать graceful shutdown? Интеграционный тест: поднять сервис, начать медленный запрос (эндпоинт со sleep 3 с), послать SIGTERM, проверить — запрос завершился 200, новые соединения отклоняются, процесс вышел до таймаута с кодом 0. Один такой тест ловит большинство регрессий остановки.
Чем плох healthcheck «пинг внешнего API»? Он проверяет чужой сервис, а не ваш: деградация внешнего API отправит ваши инстансы в перезапуск/неготовность, хотя локально всё здорово. Внешние зависимости — в readiness осознанно (только критичные, без которых обработка невозможна) и с кешем/таймаутами, в liveness — никогда.
Источники: документация gunicorn (signals), Kubernetes probes, uvicorn deployment — ссылки в шапке.