Что такое graceful shutdown и зачем он нужен
Что проверяют
Понимаете ли вы, что происходит с сервисом в момент деплоя, и умеете ли остановить его без потерянных запросов и незакоммиченных сообщений. Следом спрашивают, что будет, если сервис не уложится в отведённое время, и как это связано с Kubernetes.
Как отвечать
Graceful shutdown это остановка, при которой сервис перестаёт принимать новую работу, доделывает начатую и аккуратно освобождает ресурсы. Без него каждый деплой обрывает запросы на лету, теряет сообщения из очереди и оставляет транзакции незавершёнными.
Откуда приходит сигнал: оркестратор шлёт SIGTERM. В Kubernetes после этого есть terminationGracePeriodSeconds, по умолчанию 30 секунд, потом приходит SIGKILL, который не перехватить.
Каркас в Go:
- сигнал:
signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)возвращает контекст, который отменится по сигналу; - HTTP:
srv.Shutdown(shutdownCtx)закрывает слушатели и простаивающие соединения, затем ждёт завершения активных. Таймаут ставят меньше grace period, например 20 секунд; - gRPC:
GracefulStop(), а если не уложились,Stop(); - фоновые горутины и консьюмеры: получают отмену контекста, дочитывают текущее сообщение, коммитят оффсеты. Ждут их через
errgroupилиsync.WaitGroup; - в конце: сброс буферов логов и трейсов, закрытие пула базы.
Порядок важен: сначала перестать принимать работу, потом закрывать то, от чего она зависит. В Kubernetes под ещё несколько секунд получает трафик после SIGTERM, пока его убирают из балансировки, поэтому перед Shutdown часто добавляют короткую паузу или preStop с задержкой.
Где спотыкаются
Не ждут окончания Shutdown. ListenAndServe возвращает http.ErrServerClosed сразу после вызова Shutdown, и если main на этом завершается, активные запросы обрываются. Ждать нужно возврата самого Shutdown.
Забывают про долгоживущие соединения. WebSocket и другие соединения, захваченные через Hijack, Shutdown не отслеживает, их закрывают сами через RegisterOnShutdown.
Запускают бинарник через shell-форму CMD в Dockerfile. Тогда PID 1 часто оказывается sh, который не пересылает сигнал приложению, и через 30 секунд контейнер просто убивают.