← Назад к вопросам

Как реализовать Graceful Shutdown?

2.0 Middle🔥 241 комментариев
#Контейнеризация и DevOps#Основы Go

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Реализация Graceful Shutdown в Go

Graceful Shutdown (или "мягкое завершение") — это критически важный паттерн для Go-приложений, обеспечивающий корректное завершение работы сервиса без потери данных и обрывов соединений. Реализация особенно важна для HTTP-серверов, gRPC-сервисов, консьюмеров сообщений из очередей и других фоновых процессов.

Основные этапы Graceful Shutdown

  1. Перехват сигналов ОС для уведомления о необходимости завершения
  2. Уведомление компонентов о начале shutdown
  3. Ожидание завершения текущих операций
  4. Освобождение ресурсов и корректное завершение

Практическая реализация

1. Базовый пример для HTTP-сервера

package main

import (
    "context"
    "fmt"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    // Создаем HTTP-сервер с бизнес-логикой
    mux := http.NewServeMux()
    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        // Имитируем долгую операцию
        time.Sleep(5 * time.Second)
        fmt.Fprintf(w, "Request processed successfully")
    })

    server := &http.Server{
        Addr:    ":8080",
        Handler: mux,
    }

    // Канал для отслеживания ошибок сервера
    serverErr := make(chan error, 1)
    
    // Запускаем сервер в отдельной горутине
    go func() {
        log.Println("Starting server on :8080")
        if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            serverErr <- err
        }
    }()

    // Канал для перехвата сигналов ОС
    stop := make(chan os.Signal, 1)
    signal.Notify(stop, os.Interrupt, syscall.SIGTERM)

    // Ожидаем либо сигнал завершения, либо ошибку сервера
    select {
    case <-stop:
        log.Println("Received shutdown signal")
    case err := <-serverErr:
        log.Printf("Server error: %v", err)
    }

    // Начинаем graceful shutdown
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    log.Println("Starting graceful shutdown...")
    
    if err := server.Shutdown(ctx); err != nil {
        log.Printf("Error during shutdown: %v", err)
        // Принудительно закрываем, если не удалось корректно завершить
        server.Close()
    }

    log.Println("Server stopped gracefully")
}

2. Продвинутая реализация с управлением несколькими компонентами

type Application struct {
    httpServer *http.Server
    grpcServer *grpc.Server
    consumers  []MessageConsumer
    shutdownCh chan struct{}
    wg         sync.WaitGroup
}

func (app *Application) Run() error {
    // Запускаем все компоненты
    app.startComponents()
    
    // Ожидаем сигналы завершения
    return app.waitForShutdown()
}

func (app *Application) startComponents() {
    app.wg.Add(3)
    
    go func() {
        defer app.wg.Done()
        log.Println("Starting HTTP server")
        app.httpServer.ListenAndServe()
    }()
    
    go func() {
        defer app.wg.Done()
        log.Println("Starting gRPC server")
        // Запуск gRPC сервера
    }()
    
    go func() {
        defer app.wg.Done()
        log.Println("Starting message consumers")
        // Запуск консьюмеров
    }()
}

func (app *Application) waitForShutdown() error {
    sigCh := make(chan os.Signal, 1)
    signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
    
    <-sigCh
    log.Println("Initiating graceful shutdown")
    
    // Уведомляем все компоненты о необходимости завершения
    close(app.shutdownCh)
    
    // Завершаем HTTP сервер
    ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
    defer cancel()
    
    if err := app.httpServer.Shutdown(ctx); err != nil {
        log.Printf("HTTP shutdown error: %v", err)
    }
    
    // Завершаем gRPC сервер
    app.grpcServer.GracefulStop()
    
    // Ждем завершения всех горутин
    done := make(chan struct{})
    go func() {
        app.wg.Wait()
        close(done)
    }()
    
    // Таймаут на полное завершение
    select {
    case <-done:
        log.Println("All components stopped gracefully")
        return nil
    case <-time.After(30 * time.Second):
        return fmt.Errorf("shutdown timeout exceeded")
    }
}

Ключевые элементы реализации

Context для управления таймаутами

Использование context.WithTimeout() обязательно для предотвращения бесконечного ожидания завершения операций:

ctx, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
defer cancel()

// Передаем context в методы shutdown
if err := server.Shutdown(ctx); err != nil {
    log.Printf("Forced shutdown: %v", err)
}

WaitGroup для синхронизации горутин

sync.WaitGroup позволяет дождаться завершения всех фоновых операций:

var wg sync.WaitGroup
wg.Add(numberOfWorkers)

for i := 0; i < numberOfWorkers; i++ {
    go func(workerID int) {
        defer wg.Done()
        worker.Run(app.shutdownCh)
    }(i)
}

// В shutdown:
wg.Wait()

Обработка длительных соединений

Для WebSocket или Server-Sent Events потребуется дополнительная логика:

func (h *WSHandler) shutdown() {
    h.mu.Lock()
    defer h.mu.Unlock()
    
    for conn := range h.connections {
        conn.WriteControl(websocket.CloseMessage, 
            websocket.FormatCloseMessage(websocket.CloseGoingAway, ""),
            time.Now().Add(10*time.Second))
        conn.Close()
    }
}

Рекомендации по конфигурации

  1. Таймауты shutdown:

    • HTTP/GRPC серверы: 15-30 секунд
    • Работа с БД: 5-10 секунд
    • Внешние API: 3-5 секунд
  2. Порядок завершения:

    // Правильная последовательность:
    // 1. Остановка приема нового трафика
    // 2. Завершение обработки текущих запросов
    // 3. Закрытие подключений к БД
    // 4. Освобождение прочих ресурсов
    
  3. Health Check during shutdown:

func (app *Application) healthCheck() http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        if app.isShuttingDown {
            w.WriteHeader(http.StatusServiceUnavailable)
            return
        }
        w.WriteHeader(http.StatusOK)
    }
}

Ошибки и их обработка

  • Таймаут shutdown: логировать и принудительно завершать
  • Критические ошибки: завершать немедленно через os.Exit(1)
  • Зависимости от внешних сервисов: реализовать circuit breakers

Тестирование Graceful Shutdown

func TestGracefulShutdown(t *testing.T) {
    app := NewTestApplication()
    go app.Run()
    
    time.Sleep(100 * time.Millisecond)
    
    // Симулируем SIGTERM
    syscall.Kill(syscall.Getpid(), syscall.SIGTERM)
    
    // Проверяем, что приложение корректно завершилось
    select {
    case <-app.Stopped():
        // Успех
    case <-time.After(5 * time.Second):
        t.Fatal("Shutdown timeout")
    }
}

Интеграция с оркестраторами

Для Kubernetes добавьте preStop hook в конфигурацию:

lifecycle:
  preStop:
    exec:
      command: ["/bin/sh", "-c", "sleep 20; kill -TERM 1"]

Заключение

Graceful Shutdown — обязательный компонент production-приложений на Go. Ключевые преимущества правильной реализации:

  • Сохранение целостности данных
  • Отсутствие обрывов пользовательских сессий
  • Корректное завершение транзакций
  • Успешное досылание метрик и логов
  • Плавное снятие нагрузки с балансировщиков

Реализация требует внимания к таймаутам, правильной последовательности остановки компонентов и тщательного тестирования в условиях, имитирующих production-нагрузку.