Ответ
Graceful Shutdown (корректное завершение) — это процесс остановки приложения, при котором оно не прерывает текущие операции, а дожидается их завершения, закрывает все соединения (с БД, брокерами сообщений) и освобождает ресурсы перед полной остановкой.
Зачем это нужно?
- Предотвращение потери данных: Запросы, которые уже находятся в обработке, успевают завершиться.
- Сохранение консистентности: Незавершённые транзакции в БД могут быть корректно обработаны.
- Нулевое время простоя (Zero-downtime deployment): В системах с балансировщиком нагрузки старый экземпляр приложения корректно завершает работу, пока новый уже принимает трафик.
Пример реализации для HTTP-сервера в Go:
func main() {
// Создаем контекст, который будет отменен при получении сигнала
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
server := &http.Server{
Addr: ":8080",
Handler: nil, // ваш http.Handler
}
// Запускаем сервер в отдельной горутине, чтобы не блокировать основной поток
go func() {
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %sn", err)
}
}()
log.Println("Server started")
// Ждем сигнала на завершение (SIGINT, SIGTERM)
<-ctx.Done()
log.Println("Shutting down server...")
// Даем 5 секунд на корректное завершение.
// Если за это время сервер не остановится, он будет принудительно завершен.
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := server.Shutdown(shutdownCtx); err != nil {
log.Fatal("Server forced to shutdown:", err)
}
log.Println("Server exiting")
}
Ключевые шаги реализации:
- Перехват сигналов ОС: Используем
signal.NotifyContext
для создания контекста, который отменяется при полученииSIGINT
илиSIGTERM
. - Запуск сервера в горутине: Основной поток блокируется в ожидании отмены контекста (
<-ctx.Done()
), а HTTP-сервер работает в отдельной горутине. - Вызов
server.Shutdown()
: При получении сигнала вызывается методShutdown()
, который мягко останавливает сервер. Он перестаёт принимать новые запросы, но ждёт завершения текущих. - Использование
context.WithTimeout
:Shutdown()
вызывается с контекстом, который имеет таймаут. Это гарантия того, что если текущие запросы "зависли", приложение всё равно завершится через заданное время.