★ 优雅停机如何实现(signal+Shutdown)
结论先行
标准做法:用 signal.NotifyContext 监听 SIGINT/SIGTERM,收到信号后调用 http.Server.Shutdown(ctx)——它先停止接受新连接、等待存量请求处理完(受 ctx 超时约束),然后才返回。优雅停机的目标是:不丢请求、不留僵尸连接。
关键点
| 环节 | 说明 |
|---|---|
| 信号监听 | signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM);比手写 channel 简洁,自带取消 |
| Shutdown vs Close | Shutdown 优雅等待在途请求;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 → 收尾资源。