Как реализовать graceful shutdown в Go?
Что проверяют
Понимаете ли вы, что происходит с процессом между сигналом и выходом: какие запросы доживают, какие фоновые задачи завершаются и в каком порядке. Следом спрашивают про Kubernetes: сколько времени есть у пода и почему часть запросов всё равно падает при деплое.
Как отвечать
Каркас в современном Go:
- сигнал в контекст:
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM). Короче ручного канала, а отменённый контекст пробрасывается во всё приложение; - сервер в горутине:
ListenAndServeвозвращаетhttp.ErrServerClosedсразу после начала остановки, это нормальный исход, а не ошибка; srv.Shutdown(shutdownCtx)с отдельным таймаутом: сервер закрывает слушатели и простаивающие соединения, затем ждёт завершения активных запросов. Истёк таймаут, вернётся ошибка контекста, и тогда можно вызватьsrv.Close();- фоновые воркеры: слушают
ctx.Done(), аmainждёт их черезsync.WaitGroupилиerrgroup; - порядок: перестать принимать трафик, дождаться HTTP, остановить консьюмеры очередей с коммитом смещений и только потом закрыть пул базы.
Shutdown не ждёт захваченные соединения вроде WebSocket. Для них есть srv.RegisterOnShutdown, где вы сообщаете клиентам, что пора закрываться.
В Kubernetes после SIGTERM у пода по умолчанию 30 секунд (terminationGracePeriodSeconds), затем SIGKILL. Под убирается из балансировки асинхронно, поэтому первые секунды трафик ещё идёт. Помогает preStop с короткой паузой или перевод readiness в «не готов» до вызова Shutdown. Таймаут Shutdown должен укладываться в grace period с запасом.
Где спотыкаются
Выходят из main, как только вернулся ListenAndServe. Он возвращается мгновенно, процесс завершается, не дождавшись Shutdown, и корректной остановки не происходит вовсе.
Запускают бинарник через shell-форму CMD в Dockerfile. Главным процессом становится /bin/sh -c, и в зависимости от оболочки сигнал до приложения может не дойти. Exec-форма снимает вопрос.
Пишут log.Fatal в коде остановки. Он вызывает os.Exit, отложенные defer не выполняются, соединения с базой и буферы логов не закрываются.