Graceful Shutdown в Go: как не терять запросы при деплое
Представим обычный HTTP-сервис. Идёт деплой: old version ↓ SIGTERM ↓ process exits А в этот момент один запрос выполняется уже 5 секунд. Если просто завершить процесс, запрос может не успеть закончиться. Graceful Shutdown В Go для этого есть: server.Shutdown(ctx) Идея простая: SIGTERM ↓ stop accepting new requests ↓ finish active requests ↓ shutdown Минимальный пример: srv := &http.Server{ Addr: ":8080", Handler: handler, }
go func() { if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatal(err) } }()
ctx, stop := signal.NotifyContext( context.Background(), os.Interrupt, ) defer stop()
<-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) } Но здесь есть важный момент. Shutdown() ждёт завершения активных запросов, но только пока не закончится переданный context. Поэтому обычно задают timeout: SIGTERM ↓ stop accepting new requests ↓ wait up to 10 seconds ↓ requests finished? → exit timeout? → exit И ещё один нюанс: если просто вызвать Shutdown() и сразу завершить main, graceful shutdown не получится. Программа должна дождаться его завершения. Для обычного HTTP API этого часто достаточно. Но в реальном сервисе нужно подумать ещё о: database connections; Redis; background workers; message consumers; gRPC; WebSockets. Например, Shutdown() не закрывает и не ждёт hijacked connections вроде WebSocket - их нужно обрабатывать отдельно. По сути, Graceful Shutdown - это не просто: "поймали SIGTERM и вызвали Shutdown". Это договорённость: перестаём брать новую работу → заканчиваем текущую → закрываем зависимости → завершаем процесс. Именно поэтому graceful shutdown особенно важен при деплое и масштабировании сервисов.