Go graceful shutdown: остановить сервер без обрыва
Graceful shutdown даёт серверу завершить текущие запросы, перестать принимать новые и только потом выйти. В Go для этого используют http.Server.Shutdown с контекстом и signal.NotifyContext, который превращает SIGINT/SIGTERM в отмену контекста. Без этого процесса контейнер обрывает соединения при каждом деплое.
Остановить HTTP-сервер по сигналу
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.