Graceful shutdown HTTP-сервера без сюрпризов
Почти в каждом сервисе есть код, который ловит SIGTERM и вызывает srv.Shutdown. Выглядит надёжно, но закрывает только HTTP-часть: фоновые воркеры, консьюмеры и пулы соединений продолжают жить своей жизнью.
Базовый вариант
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
go func() {
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatal(err)
}
}()
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
log.Printf("shutdown: %v", err)
}
Таймаут для Shutdown берётся от context.Background(), а не от уже отменённого ctx. Иначе сервер закроется мгновенно и оборвёт активные запросы.
Что обычно забывают
Воркеры, запущенные через go worker(ctx), получают сигнал отмены, но main их не ждёт. Процесс завершается, пока воркер дописывает данные. Я запускаю все долгоживущие горутины через errgroup, а в main делаю g.Wait() после остановки сервера.
И порядок важен: сначала перестаём принимать новые запросы, потом дожидаемся воркеров и только после этого закрываем базу и брокер.