Содержание
Сервис получает SIGTERM при каждом деплое. Если в этот момент он просто завершает процесс, клиенты получают обрывы соединений, а воркеры бросают задачи на середине. Graceful shutdown решает это за 30 строк кода.
Разберём, как поймать сигнал, остановить HTTP-сервер, дождаться воркеров и не упереться в таймаут Kubernetes. Этот вопрос регулярно всплывает на собесах на Middle.
Какие сигналы приходят процессу
Оркестратор сначала вежливо просит процесс завершиться, а потом убивает его. Перехватить можно только первые два сигнала.
| Сигнал | Кто шлёт | Что делать |
|---|---|---|
SIGINT | Ctrl+C в терминале | Graceful shutdown |
SIGTERM | Kubernetes, Docker | Graceful shutdown |
SIGKILL | Kubernetes после таймаута | Перехватить нельзя |
Останавливаем HTTP-сервер
С Go 1.16 сигнал удобно превращать в отмену контекста через signal.NotifyContext. Метод Shutdown перестаёт принимать новые соединения и ждёт, пока текущие запросы завершатся.
func main() {
ctx, stop := signal.NotifyContext(context.Background(),
syscall.SIGINT, syscall.SIGTERM)
defer stop()
srv := &http.Server{Addr: ":8080", Handler: router()}
go func() {
err := srv.ListenAndServe()
if err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("listen: %v", err)
}
}()
<-ctx.Done() // ждём сигнал
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
log.Printf("shutdown: %v", err)
}
}
Дожидаемся воркеров
HTTP-сервер — не единственное, что работает в сервисе. Консьюмеры очередей и фоновые воркеры тоже должны закончить текущую задачу. Удобнее всего собрать их в errgroup с общим контекстом.
func (p *Pool) Run(ctx context.Context) error {
g, ctx := errgroup.WithContext(ctx)
for i := 0; i < p.size; i++ {
g.Go(func() error {
for {
select {
case <-ctx.Done():
return nil // дочитали текущую задачу и вышли
case job := <-p.jobs:
p.handle(job)
}
}
})
}
return g.Wait()
}
Порядок остановки
Закрывать зависимости нужно в обратном порядке запуска: сначала то, что принимает трафик, в конце — то, от чего все зависят.
- Снимаем readiness-пробу, чтобы балансировщик перестал слать трафик.
- Останавливаем HTTP и gRPC-серверы через
Shutdown/GracefulStop. - Отменяем контекст воркеров и ждём
g.Wait(). - Закрываем пул соединений с PostgreSQL, Redis и продюсер Kafka.
Чек-лист перед продом
- Таймаут
ShutdownменьшеterminationGracePeriodSeconds. - Воркеры слушают
ctx.Done()и не берут новые задачи после отмены. - Логируем начало и конец остановки, чтобы видеть в Grafana, сколько она длится.
- Проверили локально:
kill -TERMво время нагрузочного теста не даёт 5xx.

