★ 优雅停机如何实现(signal+Shutdown)

结论先行

标准做法:用 signal.NotifyContext 监听 SIGINT/SIGTERM,收到信号后调用 http.Server.Shutdown(ctx)——它先停止接受新连接、等待存量请求处理完(受 ctx 超时约束),然后才返回。优雅停机的目标是:不丢请求、不留僵尸连接

关键点

环节说明
信号监听signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM);比手写 channel 简洁,自带取消
Shutdown vs CloseShutdown 优雅等待在途请求;Close 立即断开所有连接
等待超时Shutdown 的 ctx 设置上限(如 10s),防止某些 handler 拖死进程
退出条件ListenAndServe 返回 http.ErrServerClosed 视为正常退出
兜底收到第二个信号可强制退出,避免“优雅”变成“永远等不完”

代码示例

func main() {
    r := gin.Default()
    r.GET("/ping", func(c *gin.Context) { c.String(200, "pong") })

    srv := &http.Server{
        Addr:    ":8080",
        Handler: r,
        ReadHeaderTimeout: 5 * time.Second, // 见超时控制章节
    }

    ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
    defer stop()

    go func() {
        if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
            log.Fatalf("listen: %v", err)
        }
    }()
    log.Println("server started")

    <-ctx.Done() // 等信号
    stop()

    // 给存量请求一个宽限期
    shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()
    if err := srv.Shutdown(shutdownCtx); err != nil {
        log.Printf("shutdown: %v", err)
    }
    log.Println("server exited gracefully")
}

生产要点

  • 部署到 K8s 等平台:Pod 收到 SIGTERM 后平台会等待 terminationGracePeriod,服务端 Shutdown 超时要小于该周期,才能“优雅完再被杀”。
  • 数据库/消息队列等连接关闭放在 Shutdown 之后或独立收尾函数里,别先关连接再等请求。
  • 业务 handler 若内部起 goroutine 干活,需要自己等 goroutine 结束(用 WaitGroup/errgroup),Shutdown 只管“HTTP 处理是否返回”。

常见追问 / 记忆点

  • 追问:Shutdown 期间新请求会怎样?答:新 TCP 连接不再被接受;已 keep-alive 的空闲连接会被关闭,让客户端重新建连。
  • 追问:为什么不直接 os.Exit?答:直接退出会掐断在途请求、漏掉收尾任务;优雅停机是生产发布的基本功。
  • 记忆点:signal → Shutdown(带超时) → 等 ErrServerClosed → 收尾资源。
笔记加载中…