Что такое Graceful Shutdown и как его реализовать в Go?

Ответ

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")
}

Ключевые шаги реализации:

  1. Перехват сигналов ОС: Используем signal.NotifyContext для создания контекста, который отменяется при получении SIGINT или SIGTERM.
  2. Запуск сервера в горутине: Основной поток блокируется в ожидании отмены контекста (<-ctx.Done()), а HTTP-сервер работает в отдельной горутине.
  3. Вызов server.Shutdown(): При получении сигнала вызывается метод Shutdown(), который мягко останавливает сервер. Он перестаёт принимать новые запросы, но ждёт завершения текущих.
  4. Использование context.WithTimeout: Shutdown() вызывается с контекстом, который имеет таймаут. Это гарантия того, что если текущие запросы "зависли", приложение всё равно завершится через заданное время.