平滑下线必须严格按顺序执行consul反注册→http shutdown→grpc gracefulstop→db/kafka关闭→goroutine收尾,任意跳过将导致幽灵实例或请求中断;反注册须在srv.shutdown()返回后、os.exit(0)前完成,且不可用defer,需校验服务存在、设httpclient超时、加重试与缓冲;http/grpc shutdown需异步调用并设超时ctx;db.close()不等待运行中query,goroutine必须响应ctx.done()并配wg管理。

平滑下线不是等超时后杀进程,而是必须在 SIGTERM 收到后、os.Exit(0) 前,按严格顺序完成 Consul 反注册 → HTTP Shutdown → gRPC GracefulStop → DB/Kafka 关闭 → goroutine 收尾。跳过任意一环,必然出现幽灵实例或请求中断。
Consul 反注册为什么必须放在 Shutdown 之后、os.Exit 之前
早于 srv.Shutdown():Consul 立即摘除实例,但 HTTP 还在收请求,流量直接丢失;晚于 os.Exit(0):进程已退出,调用根本发不出去。唯一安全窗口是 srv.Shutdown(ctx) 返回之后、os.Exit(0) 之前。
- 绝不能写在
defer里——panic或kill -9会跳过 defer,导致实例永远挂在 Consul 上 - 反注册前必须校验
client.Agent().ServiceByID(serviceID)是否存在,避免 404 报错掩盖真实问题 -
client.Config.HttpClient.Timeout必须显式设为1500 * time.Millisecond,否则默认 30s 卡死主线程 - 加最多 3 次重试,每次间隔
200 * time.Millisecond,失败只log.Error,不阻塞后续流程 - 反注册成功后建议
time.Sleep(200 * time.Millisecond),给 Consul 集群状态同步留缓冲时间
http.Server.Shutdown() 和 grpc.Server.GracefulStop() 必须异步调用
srv.Shutdown(ctx) 是阻塞的,如果直接在主 goroutine 调用,后续所有清理逻辑(DB.Close、consumer.Close、wg.Wait)全被卡住,等于只关了 HTTP,其他全漏掉。
- 正确写法:启动一个新 goroutine 执行
srv.Shutdown(ctx),主 goroutine 继续走 deregister → gRPC → DB 流程 -
ctx必须带超时(建议25 * time.Second),且不能defer cancel()——cancel 要留给 Shutdown 内部或显式调用 -
grpcServer.GracefulStop()同样阻塞,也得放 goroutine;别用Stop(),它等于kill -9对 gRPC 连接 - gRPC handler 必须监听
stream.Context().Done()或server.Context().Done(),否则 GracefulStop 永远等不到连接自然退出 - HTTP 和 gRPC 的 listener 都要手动
ln.Close(),否则端口可能被残留 listener 占用
后台 goroutine 和 DB.Close() 的隐藏陷阱
*sql.DB.Close() 只等 idle 连接归还,不等正在执行的 query;goroutine 若没响应 ctx.Done(),就会变成“僵尸协程”,持续占内存、写 DB、发消息。
- 所有长期运行的 goroutine 必须用
context.Context控制生命周期,启动时wg.Add(1),退出前wg.Done() - Shutdown 前统一调
cancel(),goroutine 内部用select { case 主动退出 -
db.Close()必须放在srv.Shutdown()返回之后,确保不再有新 handler 发起 query;但它仍可能卡住——需确认所有 handler 已进入 shutdown 分支 - Kafka 消费者(如 sarama)调
consumer.Close()后,必须等ConsumePartition循环真正退出,否则可能重复消费或 panic - WebSocket 管理器、ticker 定时器等,都要在
ctx.Done()触发后显式关闭底层连接或 stop ticker
最易被忽略的是:K8s 的 preStop 钩子若用 exec 类型调 curl -X POST http://localhost:/shutdown,只是切换 readinessProbe 状态,并不触发真正的资源关闭;真正的反注册和 Shutdown 必须由 Go 主进程自己完成,且必须确保容器主进程是 PID 1 —— 否则信号根本收不到。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











