SIGTERM — не рубить accept сразу. Сначала снять под с балансировки, перестать принимать новое, дождаться in-flight, закрыть пулы и только потом выйти. Иначе клиент ретраит оборванный ответ и может удвоить POST.
Авторизация и устойчивость HTTP http собеседование
SIGTERM простыми словами: вежливая просьба ОС завершиться. Процесс ещё жив и может дописать ответы. Это не убийство. Если не уложился — придёт SIGKILL. Ядро убивает сразу, без вопросов.
Жёсткий kill рвёт TCP посередине ответа. Клиент видит reset или таймаут и повторяет запрос. Для неидемпотентного POST это дубль. Цель graceful shutdown — не «красивые логи», а согласованность с клиентом и с оркестратором.
Пример. Под выкатывают. Процесс сразу закрыл сокет. Клиент получил обрыв на POST /charge и ретраит. Второй заряд проходит. Если бы процесс принял SIGTERM, дописал ответ и потом вышел — клиента не спровоцировали бы на повтор.
Порядок в Kubernetes
Kubelet шлёт SIGTERM. Ждёт terminationGracePeriodSeconds. Затем SIGKILL. Пока Endpoints не обновились, балансировщик ещё может послать трафик на под, который уже не слушает. Поэтому readiness снимают раньше, чем процесс гаснет. preStop часто содержит короткую паузу.
Liveness в это время должен оставаться успешным. Если проба начнёт падать во время drain, kubelet убьёт под раньше, чем Shutdown дождётся хендлеров. Readiness и liveness — разные рычаги.
- Снять Ready (проба или явно), чтобы Service перестал слать новое.
- preStop: sleep 2–5 с (порядок величины), пока Endpoints догонят.
- SIGTERM процессу: http.Server.Shutdown, drain воркеров.
- Закрыть пул БД и исходящие клиенты.
- Выход; если не уложились — SIGKILL.
// порядок: Ready=false → preStop sleep → SIGTERM → Shutdown → закрыть БД
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
_ = srv.Shutdown(ctx) // не accept, ждёт in-flight; не SIGKILL воркерам ОС
// хендлеры сами смотрят r.Context() / дедлайн
// фоновые consumer'ы — отдельный drain
// не писать в БД после Close пула
//
// preStop: sleep, пока Endpoints уберут под, процесс ещё слушает
// не первым: закрыть пул БД или провалить liveness
Shutdown перестаёт accept и ждёт активные хендлеры до дедлайна контекста. Ready снимают раньше.
HTTP-сервер в Go
Shutdown не отменяет хендлеры сам по себе. Они должны уважать r.Context() или общий дедлайн. Долгий upload без дедлайна переживёт Shutdown и упрётся в grace period. Idle соединения Shutdown закрывает.
Фоновые горутины, consumer Kafka, cron внутри процесса — отдельный протокол остановки. Иначе «HTTP уже тих, а запись в топик ещё идёт».
Что не делать первым
Закрыть пул БД, пока хендлеры ещё работают — каскад ошибок на in-flight. Поднять liveness fail, чтобы «быстрее убрали под» — kubelet пришлёт SIGKILL. Сначала убрать новый трафик (Ready), потом закончить старый.
Типичная ошибка
Путать liveness с readiness: liveness kill во время drain ухудшает всё. И звать Shutdown, не сняв Ready: часть запросов прилетает в уже закрытый accept. preStop без паузы часто проигрывает гонку Endpoints.
На собеседовании
Цепочка: Ready=false → preStop → SIGTERM → Shutdown → закрыть БД. Shutdown ждёт in-flight, не рвёт хендлеры мгновенно. preStop sleep даёт Service убрать под, пока процесс ещё слушает.