Как реализовать Graceful Shutdown в Go-приложении и почему это важно?

Ответ

Graceful Shutdown (плавное завершение) — это процесс корректного завершения работы приложения, который позволяет завершить текущие операции, закрыть соединения и освободить ресурсы перед полной остановкой. Это критически важно, чтобы не терять данные и не оставлять незавершенных транзакций.

Основные шаги реализации:

  1. Создать контекст для управления жизненным циклом: Используется context.WithCancel.
  2. Настроить прослушивание системных сигналов: Создается канал для сигналов os.Signal, который будет ловить syscall.SIGINT (Ctrl+C) и syscall.SIGTERM (сигнал завершения от systemd, Docker, Kubernetes).
  3. Запустить горутину-слушателя: Эта горутина блокируется до получения сигнала. При получении она вызывает cancel() для созданного контекста.
  4. Ожидать завершения: Основная горутина блокируется до тех пор, пока контекст не будет отменен (<-ctx.Done()).
  5. Выполнить очистку: После получения сигнала о завершении вызываются методы для плавной остановки (например, http.Server.Shutdown) и закрытия других ресурсов (БД, брокеры сообщений).

Пример для HTTP-сервера:

func main() {
    // 1. Создаем контекст и канал для сигналов
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()

    sigCh := make(chan os.Signal, 1)
    signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)

    srv := &http.Server{
        Addr:    ":8080",
        Handler: http.DefaultServeMux,
    }

    // 2. Запускаем горутину, которая ждет сигнал и отменяет контекст
    go func() {
        <-sigCh
        log.Println("Shutdown signal received, initiating graceful shutdown...")
        cancel()
    }()

    // 3. Запускаем сервер в отдельной горутине
    go func() {
        if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("Could not listen on %s: %vn", srv.Addr, err)
        }
    }()

    log.Println("Server is ready to handle requests.")

    // 4. Блокируемся, пока не будет вызван cancel()
    <-ctx.Done()

    // 5. Создаем контекст с таймаутом для шатдауна
    shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 15*time.Second)
    defer shutdownCancel()

    // Выполняем плавное завершение
    if err := srv.Shutdown(shutdownCtx); err != nil {
        log.Fatalf("Server shutdown failed: %v", err)
    }

    // Здесь можно добавить закрытие других ресурсов, например, БД
    // db.Close()

    log.Println("Server gracefully stopped.")
}

Ключевые моменты:

  • srv.Shutdown() плавно останавливает сервер, не прерывая активные соединения. Он ждет их завершения.
  • context.WithTimeout в Shutdown гарантирует, что приложение не будет висеть вечно, если какие-то соединения не могут завершиться.