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

Graceful Shutdown HTTP сервера

2.0 Middle🔥 301 комментариев
#Основы Go#Сетевые протоколы и API

Условие

Реализуйте HTTP сервер с корректным завершением (graceful shutdown). Сервер должен корректно обрабатывать сигналы завершения и дожидаться завершения текущих запросов.

Требования

func startServer(addr string) error
  • Запустить HTTP сервер на указанном адресе
  • Перехватывать сигналы SIGINT и SIGTERM
  • При получении сигнала - прекратить приём новых соединений
  • Дождаться завершения текущих запросов (с таймаутом)
  • Использовать context для контроля завершения

Подсказка

Используйте http.Server.Shutdown() и пакет signal.

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

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

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

Решение

Анализ задачи

Требования:

  • Запустить HTTP сервер
  • Перехватывать SIGINT и SIGTERM
  • Остановить приём новых соединений
  • Дождаться завершения активных запросов
  • Использовать context и timeout для корректного завершения

Стратегия:

  • Запустить сервер в горутине
  • Слушать сигналы в другой горутине
  • При получении сигнала вызвать Shutdown() с таймаутом

Решение

package main

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

func startServer(addr string) error {
    // Создаём HTTP сервер
    server := &http.Server{
        Addr:         addr,
        Handler:      http.DefaultServeMux,
        ReadTimeout:  15 * time.Second,
        WriteTimeout: 15 * time.Second,
        IdleTimeout:  60 * time.Second,
    }
    
    // Регистрируем хендлеры
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello, World!\\n")
    })
    
    http.HandleFunc("/slow", func(w http.ResponseWriter, r *http.Request) {
        // Имитируем долгий запрос
        time.Sleep(5 * time.Second)
        fmt.Fprintf(w, "Slow response done\\n")
    })
    
    // Канал для ошибок сервера
    serverErrors := make(chan error, 1)
    
    // Запускаем сервер в горутине
    go func() {
        fmt.Printf("Server starting on %s\\n", addr)
        serverErrors <- server.ListenAndServe()
    }()
    
    // Канал для обработки сигналов
    sigChan := make(chan os.Signal, 1)
    signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
    
    // Ждём либо сигнала, либо ошибки сервера
    select {
    case sig := <-sigChan:
        fmt.Printf("\\nReceived signal: %v\\n", sig)
    case err := <-serverErrors:
        if err != http.ErrServerClosed {
            return fmt.Errorf("server error: %w", err)
        }
    }
    
    // Graceful shutdown с таймаутом 30 секунд
    shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    
    fmt.Println("Gracefully shutting down server...")
    if err := server.Shutdown(shutdownCtx); err != nil {
        fmt.Printf("Shutdown error: %v\\n", err)
        // Если timeout, то вынужденно закрываем
        return server.Close()
    }
    
    fmt.Println("Server shut down successfully")
    return nil
}

func main() {
    if err := startServer(":8080"); err != nil {
        fmt.Printf("Fatal error: %v\\n", err)
        os.Exit(1)
    }
}

Пояснения реализации

1. Создание сервера

server := &http.Server{
    Addr:         addr,
    Handler:      http.DefaultServeMux,
    ReadTimeout:  15 * time.Second,
    WriteTimeout: 15 * time.Second,
    IdleTimeout:  60 * time.Second,
}
  • Таймауты предотвращают зависание соединений
  • Handler может быть любой http.Handler

2. Запуск в горутине

go func() {
    serverErrors <- server.ListenAndServe()
}()
  • Сервер работает асинхронно
  • ListenAndServe блокирует, поэтому нужна горутина

3. Перехват сигналов

signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
  • SIGINT: Ctrl+C
  • SIGTERM: kill (default signal)
  • Оба сигнала обрабатываются одинаково

4. Graceful shutdown

shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
server.Shutdown(shutdownCtx)
  • Shutdown() останавливает приём новых соединений
  • Дождётся завершения текущих запросов (но не бесконечно)
  • Если timeout — вернёт ошибку context.DeadlineExceeded

5. Fallback на Close

if err := server.Shutdown(shutdownCtx); err != nil {
    return server.Close()
}
  • Если даже shutdown не помогает, закрываем вынужденно

Альтернативный вариант: с управлением контекстом

func startServer(addr string) error {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    
    server := &http.Server{
        Addr:    addr,
        Handler: http.DefaultServeMux,
        BaseContext: func(net.Listener) context.Context {
            return ctx
        },
    }
    
    // Остальное как выше...
    
    select {
    case sig := <-sigChan:
        fmt.Printf("Signal: %v\\n", sig)
        cancel() // Отменяем контекст для всех handler'ов
    }
    
    // Затем Shutdown как раньше
    shutdownCtx, cancelShutdown := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancelShutdown()
    return server.Shutdown(shutdownCtx)
}

Плюс: handler'ы могут следить за отменой через context.

Полный пример с отслеживанием активных запросов

package main

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

type Server struct {
    http.Server
    activeRequests atomic.Int32
}

func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    s.activeRequests.Add(1)
    defer s.activeRequests.Add(-1)
    
    fmt.Printf("Request: %s %s (active: %d)\\n", r.Method, r.URL.Path, s.activeRequests.Load())
    
    // Обрабатываем запрос
    http.DefaultServeMux.ServeHTTP(w, r)
}

func main() {
    server := &Server{
        Server: http.Server{
            Addr:         ":8080",
            ReadTimeout:  15 * time.Second,
            WriteTimeout: 15 * time.Second,
        },
    }
    server.Handler = server
    
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello\\n")
    })
    
    http.HandleFunc("/slow", func(w http.ResponseWriter, r *http.Request) {
        time.Sleep(5 * time.Second)
        fmt.Fprintf(w, "Done\\n")
    })
    
    // Запуск
    go server.ListenAndServe()
    
    // Ожидание сигнала
    sigChan := make(chan os.Signal, 1)
    signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
    <-sigChan
    
    fmt.Printf("\\nShutting down (active requests: %d)...\\n", server.activeRequests.Load())
    
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    
    server.Shutdown(ctx)
}

Производительность и безопасность

  • Connection draining: Shutdown() автоматически закрывает keep-alive соединения
  • Request timeout: handler'ы должны иметь свои таймауты
  • Stuck requests: если request не завершится за 30 сек, соединение закроется force
  • Clean exit: нет потери данных, все goroutine завершатся корректно
Graceful Shutdown HTTP сервера | PrepBro