/livez和/readyz必须分开且不共享逻辑,因/livez仅检查进程是否存活(如HTTP服务是否Accept、goroutine数是否异常),响应须≤200ms;/readyz则需缓存依赖状态(DB/Redis等)供快速读取,避免高频探测压垮下游。

/livez 和 /readyz 必须分开,且不能共享逻辑——这是最稳定的前提。用同一个 handler 会把“进程是否卡死”和“能不能收流量”混为一谈,Kubernetes 滚动更新时大概率卡住,或触发无限重启。
为什么 /livez 里绝对不能查数据库或 Redis
查 DB 或 Redis 就是给 livenessProbe 装自毁开关:一次网络抖动、连接池满、DNS 解析慢,/livez 就超时失败,K8s 立即杀 Pod。新 Pod 启动后照样连不上,结果就是 CrashLoopBackOff。
真正该做的只有三件事:
-
http.Server是否还在 Accept 连接(比如检查srv.Serve()goroutine 是否存活) - 关键原子变量是否被置为 0(如
atomic.LoadInt32(&isHealthy)) -
runtime.NumGoroutine()是否突增(比如 > 5000),作为死锁/泄漏的快照信号
响应必须在 200ms 内完成,不设 context 超时,不记日志,不走任何中间件。
/readyz 怎么避免每次请求都打爆下游
高频探针(默认每 2–5 秒一次)如果每次都调 db.PingContext() 或 redis.Ping(),会在几秒内建起数百个空闲连接,压垮 DB 或 Redis 连接池。
立即学习“go语言免费学习笔记(深入)”;
正确做法是用后台 goroutine 缓存探测结果:
- 启动时启一个
time.Ticker,每 5 秒执行一次全量依赖探测 - 探测项并发执行:
db.PingContext(ctx, 2*time.Second)、redis.Ping(ctx).Err()、grpcConn.WaitForStateChange(ctx, connectivity.Ready) - 结果写入线程安全结构体(
sync.RWMutex保护),含各组件状态、时间戳、整体status("UP"/"DEGRADED"/"DOWN") -
/readyzhandler 只读缓存,不做任何新 I/O
Gin/Echo 框架里注册健康端点的两个硬坑
框架中间件是健康检查最大干扰源,尤其 JWT 验证、日志、panic recover —— 它们会让 /livez 响应变慢、返回 401 或吞掉错误。
必须避开:
- Gin:健康路由注册在
r.Use()之前;或用r.NoRoute()显式拦截路径,跳过所有中间件 - Echo:写成
e.GET("/livez", livezHandler).SkipMiddleware = true(不是.Use().Skip) - 别把
/metrics和/livez注册到同一组路由下,否则 promhttp 中间件可能污染 header 或加日志
响应头必须显式设:w.Header().Set("Content-Type", "application/json; charset=utf-8"),否则 Traefik 等 Ingress 可能截断 JSON 导致探针失败。
独立 probe 端口比共用端口更稳
业务端口跑 Gin/Echo + 中间件 + TLS,probe 端口只跑裸 net/http.ServeMux,完全隔离。
示例:
probeMux := http.NewServeMux()
probeMux.HandleFunc("/livez", livezHandler)
probeMux.HandleFunc("/readyz", readyzHandler)
go http.ListenAndServe(":8081", probeMux) // 单独监听 8081
Deployment 中配两个容器端口,livenessProbe 和 readinessProbe 全部指向 :8081。这样即使业务 HTTP server panic 或被中间件阻塞,probe 依然可用。
最易被忽略的一点:Kubernetes 不会“自动修复”,它只按配置杀或摘流量;真正的稳定性来自 Go 代码里对状态的主动控制——比如收到 SIGTERM 时立刻将 isReady 置为 0,让 /readyz 返回 503,等 drain 完再退出。


















