Kratos的app.Run()默认不处理优雅退出,因其设计上将控制权交还开发者,监听SIGINT/SIGTERM后直接os.Exit(0),跳过资源清理;必须显式使用app.WithBeforeStop注册registry.Deregister和http.Server.Shutdown等逻辑,否则导致连接中断、请求丢失、注册中心残留。

Kratos 本身不提供开箱即用的“优雅重启”二进制能力,它的优雅退出(graceful shutdown)需开发者显式编排生命周期,否则 SIGTERM 一来就直接 kill,连接中断、请求丢失、注册中心残留全都会发生。
为什么 app.Run() 默认不处理优雅退出
Kratos 的 app.Run() 是一个阻塞调用,它内部监听 SIGINT/SIGTERM 后直接调用 os.Exit(0),跳过了所有资源清理逻辑。这不是 bug,而是设计取舍:框架把控制权交还给使用者,避免在复杂场景下做错误假设。
- 如果你没注册
app.WithBeforeStop,http.Server.Shutdown()和registry.Deregister()根本不会被执行 -
app.WithAfterStop也一样,不显式声明,日志 flush、DB 连接池 Close 等操作就被跳过 - 某些 contrib 组件(如
contrib/registry/nacos)的Deregister是异步或带重试的,必须等它返回成功才能继续 shutdown 流程
http.Server.Shutdown() 超时后仍丢请求?检查 Read/Write Timeout
Go 标准库的 http.Server.Shutdown() 只保证不再接受新连接,但对已建立的长连接(如 HTTP/1.1 keep-alive、gRPC streaming)没有强制 drain 行为——它会等所有活跃连接自然关闭,超时后才强制断开。这导致部分请求被截断。
- 务必为
http.Server显式设置ReadTimeout和WriteTimeout(例如 5–10 秒),防止慢请求拖垮整个退出流程 - 不要依赖
IdleTimeout单独控制;它只管空闲连接,不干预正在读写的请求 - 若业务含大量长轮询或 SSE,建议在
preStop阶段主动通知客户端断连,而非等超时强杀
反注册失败就停不下来?别跳过 registry.Deregister() 重试
常见错误是 registry.Deregister() 调用后不检查返回值,或者失败就直接往下走。结果是旧实例还在注册中心挂着,K8s Service 或客户端 resolver 仍在往它转发流量,直到心跳超时(可能长达 30 秒)。
- 必须在
app.WithBeforeStop中同步执行registry.Deregister(),并判断 error 是否为临时性错误(如网络抖动、etcd 临时不可达) - 对临时错误,最多重试 3 次,每次间隔指数退避(如 100ms → 200ms → 400ms),并记录 warn 日志
- 若最终失败,不应 panic 或 exit,而应允许
http.Server.Shutdown()继续,但要打 error 日志提醒运维人工介入
滚动更新时 Pod 还在收请求?就绪探针和 preStop 必须配平
Kubernetes 的滚动更新能否平滑,不取决于 Kratos,而取决于你是否让 K8s 准确感知“这个 Pod 已准备好”和“这个 Pod 真正准备好了离开”。两者错位,就会出现流量打到正在 shutdown 的实例上。
-
readinessProbe路径(如/ready)必须调用health.Checker.Check(),且检查项包含 DB 连通、Redis 可写、gRPC 客户端 ready —— 不能只返回{"status":"ok"} -
lifecycle.preStop应触发同一套健康检查的“反向逻辑”,比如关闭写入能力、拒绝新任务,但保持读服务短暂可用 -
terminationGracePeriodSeconds必须 ≥preStop最大耗时 +http.Server.Shutdown()超时时间,否则 K8s 会在你还没 deregister 完就发 SIGKILL
最容易被忽略的一点:Kratos 的 health.Checker 默认不感知资源初始化状态,你得自己在 Check() 方法里聚合所有 data 层 client 的 readiness 结果。漏掉一个 Redis 连接池的 ping 检查,/ready 就会提前变绿,流量就早于 DB 初始化到达,报错就来了。











