正确做法是收到信号后再创建带超时的 context,确保超时倒计时从关闭开始而非启动;错误写法在程序启动时就创建,导致 Shutdown 时 context 可能已过期。
为什么 context.WithTimeout 必须在信号接收后创建
直接在程序启动时就创建带超时的 context.context,然后传给 http.server.shutdown,会导致超时倒计时从启动就开始跑,而不是从收到 sigint 或 sigterm 时才开始。等你真按下 ctrl+c,可能 context 已过期,shutdown 立刻返回 context.deadlineexceeded,正在处理的请求被强制中断——这根本不是“优雅”。
正确做法是:先用无缓冲 channel 阻塞等待信号;收到信号后,再调用 context.WithTimeout 创建新 context,并立刻 defer cancel()。这样超时窗口只在关闭阶段开启,和业务生命周期对齐。
- 错误写法:
ctx, _ := context.WithTimeout(context.Background(), 10*time.Second)放在 main 开头 - 正确写法:
后再创建ctx, cancel := context.WithTimeout(...) - 漏掉
defer cancel()会导致 goroutine 泄漏(尤其当 shutdown 失败重试时)
http.Server.Shutdown 的 context 是怎么级联影响子 goroutine 的
http.Server.Shutdown 本身不主动 kill 任何 goroutine,它只是停止接受新连接,并等待所有已建立连接上的 handler 执行完毕。真正起级联作用的是你传入的 context —— 它要靠你自己在 handler 内部显式监听 ctx.Done(),否则超时不会中断长任务。
比如一个 handler 里用了 time.Sleep(15 * time.Second),但没检查 context,那么即使 shutdown ctx 已超时,这个 handler 还会卡满 15 秒才返回,拖慢整个退出流程。
- 必须在 handler 中用
select监听ctx.Done()和业务完成信号 - Gin 用户注意:
c.Request.Context()是继承自 server context 的,可直接用 - 数据库查询、HTTP 调用等阻塞操作,务必传入该 context,否则无法响应超时
多个服务共存时,如何避免 shutdown 顺序错乱导致 panic
一个 Go 进程常同时运行 HTTP Server、gRPC Server、后台定时任务、数据库连接池等。如果它们共用同一个 shutdown context,但关闭顺序不合理(比如 DB 连接比 HTTP handler 先关),handler 在执行中访问 DB 就会 panic。
关键不是“一起关”,而是“按依赖反序关”:先停新请求入口(HTTP/gRPC listener),再等业务逻辑结束,最后释放底层资源(DB、Redis、文件句柄)。
- 不要把所有
Shutdown调用塞进一个select或并行 goroutine - HTTP Server 的
Shutdown应最先触发,且需等待其返回后再关 DB(或用sync.WaitGroup控制) - 若 DB 层封装了
Close方法,确认它是否阻塞(如 pgx 的Close会等连接池空闲)
超时时间设多少才合理:5 秒还是 30 秒
没有通用值。5 秒适合轻量 API 服务(平均响应
线上服务建议分层设超时:HTTP shutdown 用 10 秒,DB close 单独设 5 秒,后台任务用 3 秒 —— 并各自捕获 error,避免一个失败拖垮全部。
- 永远不要用
context.Background()调用Shutdown,那等于放弃超时控制 - 日志里记下 shutdown 各阶段耗时(如 “HTTP shutdown took 8.2s”),用于后续调优
- 容器环境(K8s)中,terminationGracePeriodSeconds 必须 ≥ 最大 shutdown 超时,否则 kubelet 会发
SIGKILL
最易被忽略的一点:shutdown context 的 deadline 是硬限制,但 handler 内部的清理逻辑(比如写日志、发告警)如果也依赖这个 context,可能被截断。需要为关键清理动作单独开一个不超时的 goroutine,或至少用 context.WithDeadline 做二次兜底。











