Go服务收到SIGTERM后不应立即退出,而应优雅停机:监听信号→停止接收新连接→等待活跃请求完成→关闭资源;需用context.WithTimeout()控制http.Server.Shutdown()超时,gRPC则需手动处理流式RPC和后台goroutine。

Go 服务收到 SIGTERM 后立即退出?别让正在处理的请求被砍断
Go 默认对 SIGTERM 和 SIGINT 的响应是直接退出,这会导致正在执行的 HTTP 请求、gRPC 调用或数据库事务被强行中断。优雅停机的核心不是“不退出”,而是“等正在跑的活干完再退”。关键在于:监听信号 → 停止接收新连接 → 等待活跃请求完成 → 关闭资源。
http.Server.Shutdown() 是核心,但必须配合 context.WithTimeout()
http.Server.Shutdown() 会尝试关闭所有空闲连接,并等待正在处理的请求返回。但它本身不设超时,如果某个请求卡住(比如死循环、阻塞 I/O、没设 deadline 的数据库查询),服务就永远停不下来。所以必须用带超时的 context 控制它:
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Printf("HTTP server shutdown error: %v", err)
}
- 超时时间建议设为略大于你最长请求的 P99 延迟(比如 30s),太短会强制 kill 活跃请求,太长影响部署节奏
- 别用
context.Background()直接传给Shutdown(),否则它永远不会返回 - 调用
Shutdown()前,确保已停止srv.Serve()或srv.ListenAndServe()的 goroutine
gRPC Server 也要手动排空,GracefulStop() 不等于“等所有请求结束”
gRPC 的 GracefulStop() 会拒绝新请求、关闭 listener,但**不会等待流式 RPC(stream)或长时间运行的 unary RPC 完成**——它只等 server 内部的 accept loop 结束。实际中常出现进程已退出但 stream 还在发数据,客户端报 transport is closing。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 务必在调用
GracefulStop()前,先关闭所有自定义的后台 goroutine(比如健康检查 ticker、定时同步任务) - 对每个长期运行的 handler,自己加
ctx.Done()检查,例如:select { case - 若用
grpc.Server+http.Server共享端口(如用grpc.NewServer(grpc.Creds(...))配合http.Serve),要分别调用两者的停机方法,顺序是:先停 gRPC listener,再调http.Server.Shutdown()
数据库连接池、消息队列消费者这些“外部依赖”必须显式关闭
HTTP/gRPC 层停了,不代表你的 *sql.DB、redis.Client 或 kafka.Consumer 就自动释放了。它们往往持有长连接或后台 goroutine,不关干净会导致连接泄漏、消息重复消费或服务重启后连不上中间件。
-
sql.DB.Close()必须调用,它会等待所有正在执行的查询完成(但不等未提交事务),之后再返回 - Redis 客户端如
github.com/go-redis/redis/v8:调client.Close(),它内部会等待 pending commands 完成 - Kafka 消费者(如
segmentio/kafka-go):调consumer.Close(),它会提交 offset 并退出 fetch loop;注意有些版本需先consumer.Cancel()中断拉取 - 所有关闭操作应放在
Shutdown()调用之后、进程退出之前,且用defer不可靠(信号处理里不能 defer)
真正难的不是写这几行关闭代码,而是确认你用的所有第三方库是否提供可等待的关闭接口,以及它们的文档有没有藏坑——比如某个 SDK 的 Close() 只是关 socket,根本不等 pending request。










