平滑下线需联动健康检查与shutdown:readiness置false后lb摘流,再调用srv.shutdown()并设idletimeout;liveness不依赖外部资源;信号捕获后同步更新状态并触发回调清理。

Go微服务里,光靠 http.Serve() 默认的 shutdown 逻辑不够——它会直接关闭 listener,正在处理的请求可能被粗暴中断。真正能保障平滑下线的,是把健康度检查(liveness/readiness)和 shutdown 流程联动起来,用回调函数主动控制服务状态流转。
健康检查端点必须区分 liveness 和 readiness
很多项目只暴露一个 /health,但 liveness 和 readiness 语义完全不同:liveness 表示进程是否存活(比如 goroutine 是否卡死),readiness 表示能否接收新流量(比如 DB 连接是否就绪、依赖服务是否可用)。下线时,应先让 readiness 变为 false,等负载均衡器摘除实例,再触发 shutdown。
-
readiness端点必须可写入状态开关,例如通过原子变量或 channel 控制返回值 -
liveness端点不应依赖外部资源(如 DB 查询),否则可能误判进程死亡 - Kubernetes 中,
readinessProbe应指向 readiness 端点,livenessProbe指向 liveness 端点
shutdown 前必须等待活跃连接完成
调用 srv.Shutdown() 本身不会等所有 HTTP 连接结束,它只是发信号让 server 停止接受新连接,并给现有连接留出超时时间。如果没设好 srv.IdleTimeout 或没监听 srv.RegisterOnShutdown() 回调,就可能丢请求。
- 务必设置
srv.IdleTimeout(如 30s),否则默认为 0,Shutdown()会立即返回 - 在
srv.RegisterOnShutdown()回调里,可以做清理动作(如关闭数据库连接池、通知消息队列消费者退出) - 不要在
RegisterOnShutdown()里阻塞太久,它运行在单独 goroutine,但长时间阻塞会影响整体 shutdown 流程
readiness 状态变更要与 shutdown 同步触发
readiness 下线不能靠“定时轮询+人工 sleep”,而应在收到 OS 信号(如 SIGTERM)后立刻置为 false,并同步触发 shutdown 流程——否则会出现“已不健康但还在收请求”的窗口期。
- 用
signal.Notify(c, syscall.SIGTERM, syscall.SIGINT)监听信号 - 收到信号后,先将 readiness 状态设为 false(比如原子 store 一个
int32),再调用srv.Shutdown() - readiness handler 中,直接读取该状态变量,避免加锁或 channel 阻塞
- 如果使用了反向代理(如 nginx、envoy),需确保其 health check interval 小于 readiness 状态切换到 shutdown 的总耗时
最易被忽略的是 readiness 状态的可见性延迟——哪怕你秒级关闭了 HTTP server,若上游 LB 缓存了健康检查结果(比如 5 秒缓存),那这 5 秒内仍可能转发请求进来。所以 readiness 端点本身要足够轻量,且 LB 的探针配置必须和你的状态更新节奏对齐。











