Ответ
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()вызывается с контекстом, который имеет таймаут. Это гарантия того, что если текущие запросы "зависли", приложение всё равно завершится через заданное время.
Ответ 18+ 🔞
А, вот эта тема про то, как сервис не должен просто так сдохнуть, как последний мудак, когда его выключают! Graceful Shutdown, блядь, или "корректное завершение" — это когда твоё приложение не бросает всё на полпути, а как порядочный человек, дожидается, пока закончит текущие делишки, закроет все коннекты к базе и прочим сервисам, и только потом ложится спать.
А нахуя это вообще нужно, спросишь ты?
- Чтобы не терять данные, как последний распиздяй. Запросы, которые уже в работе, должны спокойно доделаться.
- Чтобы не наебать с консистентностью. Незавершённые транзакции в базе — это пиздец, они могут всё похерить.
- Чтобы было "нулевое время простоя", ёпта. В продвинутых схемах развёртывания старый экземпляр тихо-мирно доделывает работу, пока новый уже вовсю ловит трафик. Красота!
Смотри, как это выглядит на Go для обычного HTTP-сервера:
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 (Ctrl+C) или 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(это когда Ctrl+C жмёшь) илиSIGTERM(это когда оркестратор типа Kubernetes хочет убить под), контекст тут же отменяется. - Сервер — в отдельную горутину. Основной поток тогда свободен и может просто ждать сигнала, а сервер в это время пашет.
- Вызов
server.Shutdown()— это магия. Когда сигнал прилетел, мы вызываем этот метод. Сервер перестаёт принимать новые запросы, но терпеливо ждёт, пока все текущие доложат о выполнении. Умница, блядь. - Таймаут — наша последняя надежда. Обернули вызов в контекст с таймаутом. Если какой-то запрос завис, как мудак, и не завершается, через 5 секунд мы ему говорим "всё, поезд ушёл" и принудительно вырубаем всё. Чтобы не висеть до скончания веков.
Вот и вся философия. Не бросать всё на полпути, а завершать дела как взрослый.