Go微服务优雅停机需同时监听os.Interrupt和syscall.SIGTERM,用带缓冲通道注册;必须传超时context调用http.Server.Shutdown();并按反向依赖顺序关闭gRPC、DB、Kafka等资源。

Go 微服务的优雅停机不是“加个Shutdown就完事”,而是信号捕获、资源协同、超时兜底三者缺一不可;服务治理也不能只靠注册中心,得把健康检查、流量控制、熔断逻辑和停机流程串成一条线。
signal.Notify 必须同时监听 os.Interrupt 和 syscall.SIGTERM
本地开发按 Ctrl+C 发的是 os.Interrupt,Kubernetes 终止 Pod 发的是 syscall.SIGTERM。只监听其中一个,要么本地测试不生效,要么线上直接硬杀。
- 用
make(chan os.Signal, 1)创建带缓冲通道,防止信号丢失(尤其 SIGTERM 和 SIGINT 连续到达时) - 必须一次性注册两个信号:
signal.Notify(quit, os.Interrupt, syscall.SIGTERM),不能分两次调用 - 注册操作要放在
http.ListenAndServe()或grpc.Serve()之前,否则主 goroutine 已阻塞,信号收不到
http.Server.Shutdown() 必须传带超时的 context,不能用 context.Background()
http.Server.Shutdown() 本身不设超时。如果某个请求卡在没设 deadline 的数据库查询或阻塞 I/O 上,它就永远等下去,进程 hang 死。
- 超时值建议设为略大于 P99 请求耗时,比如 30 秒;长轮询或流式接口需上调,不能硬写 5 秒
- 必须用
context.WithTimeout(context.Background(), 30*time.Second),而非context.Background()或未设超时的context.WithCancel() - 调用后立即
cancel(),否则 context 泄漏;Shutdown()返回context.DeadlineExceeded是正常现象,记录日志即可,不必 panic
gRPC Server GracefulStop() 不等于“等所有请求结束”
grpc.Server.GracefulStop() 会关闭 listener、拒绝新请求,但不等待流式 RPC(stream)或长时间 unary RPC 完成——它只等 accept loop 结束。结果常是进程退出了,客户端还在收数据,报 transport is closing。
立即学习“go语言免费学习笔记(深入)”;
- 必须用外层统一 context 控制整体超时,例如:
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) - 先调
grpcServer.GracefulStop(),再调httpServer.Shutdown(ctx),避免 HTTP 健康探针还在返回 200 而 gRPC 已停 - 流式接口需在 handler 内主动监听
ctx.Done(),及时中断 send/recv,不能依赖GracefulStop()自动处理
服务治理组件必须参与停机生命周期
Consul 注册、Nacos 配置监听、GORM 连接池、Kafka 消费者这些都不是“自动退出”的。它们不关,停机就只是表面功夫:注册中心里服务还挂着,下游继续发请求;DB 连接池没关,连接泄漏;消费者还在拉消息,但 handler 已停。
- 关闭顺序必须反向依赖:先停 HTTP/gRPC server → 再注销 Consul 服务(调
client.Agent().ServiceDeregister())→ 再关 DB(db.Close())→ 最后停 Kafka 消费器 - 所有后台 goroutine(如 metrics 上报、心跳)必须接收
ctx.Done(),不能靠time.Sleep或无缓冲 channel 等待 - K8s 中务必配
preStop钩子,给应用留出缓冲时间;健康检查探针 timeout 要大于 Shutdown 超时,否则实例被过早剔除
真正难的不是写 Shutdown,而是让每个组件都响应同一个 context、按序退出、不互相拖慢。一个没关干净的 goroutine,就能让整个停机流程卡住——而它往往藏在你没注意的定时任务或日志上报里。


















