Go微服务平滑下线关键在于四步严格串行:readiness置false触发LB摘流→HTTP连接自然结束(需设IdleTimeout并异步调Shutdown)→Consul反注册(Shutdown后、os.Exit前)→gRPC/DB/后台goroutine响应ctx.Done()有序关闭。

Go 微服务下线时流量不丢、不中断,关键不是“等几秒再关”,而是让 readiness 状态变更、负载均衡摘流、HTTP 连接自然结束、依赖组件有序关闭这四件事严格串行且无竞态。
readiness 端点必须可写状态且不依赖外部资源
很多服务把 /health 同时当 liveness 和 readiness 用,这是平滑下线失败的根源。liveness 只检查 goroutine 是否卡死(比如用 runtime.NumGoroutine() 做简单阈值判断),readiness 必须能被主动置 false。
- 用
atomic.StoreInt32(&ready, 0)控制 readiness 返回值,避免锁或 channel 阻塞 handler -
readinesshandler 里直接读该变量,不要查 DB、Redis 或调下游——否则摘流延迟不可控 - Kubernetes 中
readinessProbe必须指向这个可变端点,initialDelaySeconds设为 5,periodSeconds不超过 3 - 反向代理(如 Envoy)的健康检查间隔必须小于整个 shutdown 流程耗时,否则会出现“已置 false 但 LB 还没轮到它”的窗口期
srv.Shutdown() 必须异步调用并设 IdleTimeout
http.Server.Shutdown() 本身不阻塞 listener 关闭,但它默认不等活跃连接结束——除非你显式设置 IdleTimeout。
- 必须提前设置
srv.IdleTimeout = 30 * time.Second,否则Shutdown()默认立即返回,正在写的 response 可能被截断 - 不能在主 goroutine 直接调
srv.Shutdown(),否则后续的 Consul 反注册、gRPC 关闭全被卡住 - 正确写法:
go srv.Shutdown(ctx),其中ctx, cancel := context.WithTimeout(context.Background(), 35*time.Second);cancel()不要 defer,留给Shutdown()内部或显式调用 - 检查返回 error:
context.DeadlineExceeded是超时,http.ErrServerClosed是正常,其他 error(如net.OpError)要log.Error
Consul 反注册必须在 Shutdown 返回后、os.Exit 前执行
早于 srv.Shutdown() 返回:Consul 摘流了,但 HTTP 还在收请求,流量直接丢失;晚于 os.Exit(0):进程已死,反注册根本发不出去。
立即学习“go语言免费学习笔记(深入)”;
- 反注册前先校验
client.Agent().ServiceByID(serviceID)是否存在,避免静默 404 掩盖注册失败问题 -
client.Config.HttpClient.Timeout必须设为1500 * time.Millisecond,默认 30s 会卡死主线程 - 最多重试 3 次,每次间隔
200 * time.Millisecond,失败只log.Error,不 panic、不阻塞退出流程 - 反注册成功后建议
time.Sleep(200 * time.Millisecond),给 Consul 集群状态同步留缓冲时间 - 绝不能写在
defer里——panic或kill -9会跳过它,实例永远挂在 Consul 上
gRPC 和后台 goroutine 必须显式关闭且响应 ctx.Done()
http.Server.Shutdown() 对 gRPC、Kafka 消费者、定时任务完全无感。它们各自有关闭语义,且必须与 HTTP shutdown 同步推进。
- gRPC server 必须调
grpcServer.GracefulStop()(不是Stop()),且同样放 goroutine 异步执行 - Kafka 消费者(如 sarama)调
consumer.Close(),并在ConsumePartition循环里监听ctx.Done() - 所有后台 goroutine 启动时
wg.Add(1),退出前wg.Done();shutdown 前调wg.Wait() -
*sql.DB.Close()是阻塞的,但它只等 idle 连接归还,不等正在执行的 query——所以 handler 必须用db.QueryContext(ctx, ...),确保 query 能响应ctx.Done()
最容易被忽略的是 readiness 状态变更和信号捕获的同步性:收到 SIGTERM 后,必须原子置 ready = 0 并立刻触发 srv.Shutdown(),中间不能加 sleep,也不能靠轮询更新状态——任何延迟都会造成“已不健康但还在收请求”的危险窗口。


















