Go graceful shutdown: остановить сервер без обрыва

Категория: Backend · Язык: Go · Обновлено:

Graceful shutdown даёт серверу завершить текущие запросы, перестать принимать новые и только потом выйти. В Go для этого используют http.Server.Shutdown с контекстом и signal.NotifyContext, который превращает SIGINT/SIGTERM в отмену контекста. Без этого процесса контейнер обрывает соединения при каждом деплое.

Остановить HTTP-сервер по сигналу

Go
package main

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

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        time.Sleep(200 * time.Millisecond)
        fmt.Fprintln(w, "ok")
    })

    srv := &http.Server{Addr: ":8080", Handler: mux}
    ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
    defer stop()

    go func() {
        if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
            fmt.Println("server error:", err)
        }
    }()

    <-ctx.Done()
    shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    if err := srv.Shutdown(shutdownCtx); err != nil {
        fmt.Println("shutdown error:", err)
    }
    fmt.Println("server stopped")
}

Shutdown перестаёт принимать новые соединения и ждёт активные запросы, пока не истечёт контекст. ListenAndServe в этот момент возвращает http.ErrServerClosed; это ожидаемая ошибка, её не нужно логировать как сбой. signal.NotifyContext доступен с Go 1.16, а Shutdown — с Go 1.8.

Таймаут shutdown должен быть меньше, чем terminationGracePeriodSeconds в Kubernetes или stop_grace_period в Compose. Иначе оркестратор пришлёт SIGKILL раньше, чем сервер закончит. Для долгих WebSocket-соединений Shutdown не ждёт hijacked-соединения: их нужно закрывать отдельно через RegisterOnShutdown или собственный трекер.

Перед остановкой снимите сервер с балансировщика и переведите readiness-проверку в состояние отказа. Иначе новый трафик продолжит приходить, пока процесс уже завершается. Не вызывайте os.Exit(0) сразу после Shutdown: сначала дайте отработать отложенным функциям.

Если Shutdown вернул context.DeadlineExceeded, часть запросов не успела завершиться. В этом случае можно вызвать srv.Close() для принудительного закрытия, но лучше увеличить таймаут или разобраться, почему запросы такие долгие.

Документация: http.Server.Shutdown, signal.NotifyContext.