健康检查必须严格区分/livez和/readyz两个端点:/livez仅检查本地进程状态(如goroutine数、内存、时钟漂移)且响应≤1秒,禁用任何外部调用;/readyz须并行检查DB等依赖并缓存结果,失败返回503,路径与探针配置严格一致。

健康检查不是“能返回200就行”,而是让K8s准确判断“这个进程还活着”和“这个服务能接流量”——两个问题必须用两个独立端点回答,否则滚动更新卡住、Pod反复重启都是常态。
为什么/healthz返回503?路由没注册或状态码写错了
最常见的503不是DB连不上,而是http.ServeMux根本没注册该路径,或者handler里只写了w.Write([]byte("ok"))却忘了w.WriteHeader(200)。K8s和Consul只认状态码,不解析响应体。
- 用
http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) })先验证路由通不通 - 别依赖
log.Fatal或panic——探针请求崩溃会导致整个服务不可用 - Consul要求响应体为空或可读,但某些老版本会因缺失
Content-Length头而重试超时
/livez和/readyz必须拆开,不能共用一个handler
混用是生产事故高发区:把DB检查塞进/livez,DB一抖就触发K8s反复杀Pod;把goroutine泄漏检查放进/readyz,又会导致流量被误摘。
-
/livez只做进程级快照:检查runtime.NumGoroutine()是否突增、time.Since(startTime)是否为负(防时钟漂移)、心跳标记是否更新 -
/readyz查真实服务能力:DB连接池db.PingContext、Redisclient.Ping、gRPC客户端conn.GetState(),每项必须设独立超时(如context.WithTimeout(ctx, 800*time.Millisecond)) - 别用
http.DefaultServeMux——它全局唯一,容易被第三方库(比如prometheus/metrics)悄悄覆盖路径;改用http.NewServeMux()并显式绑定
K8s探针参数必须和Go handler耗时对齐
默认timeoutSeconds: 1,但一次db.Ping()在网络抖动时很容易超1秒。handler还没返回,K8s已判定失败并重启——这不是健康检查,是自毁循环。
立即学习“go语言免费学习笔记(深入)”;
-
timeoutSeconds必须 ≥ 你最慢依赖的P99延迟(建议至少设为1.5秒) -
initialDelaySeconds不能为0:新Pod启动后,配置加载、连接池预热需要时间,至少设5秒 - 避免在handler里做同步日志、metric打点、中间件鉴权——这些会拖慢响应,且高频探针会撑爆日志卷
真正难的不是写两个HTTP handler,而是让每个依赖检查都带超时、降级、独立失败标识,并确保K8s探针参数与之严格匹配——差100毫秒,就可能让服务在高峰期陷入CrashLoopBackOff。


















