Kubernetes标记Unhealthy的常见原因是健康检查响应体为空、缺失Content-Type头或被中间件劫持,导致Kubelet静默判定失败;livenessProbe与readinessProbe不可混用同一路径,应分离为/healthz和/readyz并独立实现;probe配置需匹配应用启动节奏,避免initialDelaySeconds过小或阻塞主服务。

为什么/health返回200但Kubernetes仍标记Unhealthy
常见原因是响应体为空、缺失Content-Type头,或被中间件劫持。Kubelet 对空响应或text/plain默认体静默判为失败,不是“没收到”,而是“不符合健康协议”。
- 必须显式设置
w.Header().Set("Content-Type", "application/json") - 响应体不能是空字符串,至少写
{"status":"ok"},推荐加timestamp字段便于时序排查 - 若用 Gin/Echo,别注册在
r.Use()之后——JWT 或日志中间件会拦截/health并返回 401 或 500 - 检查是否误配了
http.Redirect或http.Error(w, ..., 302):K8s 探针不跟随重定向
livenessProbe和readinessProbe指向同一路径的后果
这是最隐蔽也最致命的配置错误。两个探针语义完全不同,混用会导致雪崩式故障。
-
livenessProbe失败 → K8s 杀容器 → 重启;它只该检查进程是否卡死(如 goroutine 数暴增、atomic.LoadInt32(&healthy)为 0) -
readinessProbe失败 → K8s 摘流量;它才该同步探测 DB/Redis/gRPC,任一失败就返回503并带具体错误项(如{"redis": "timeout"}) - 若都指向
/health,DB 挂了就会触发无限重启(CrashLoopBackOff),新 Pod 启动后依然连不上 DB - 正确做法:
livenessProbe走/healthz,readinessProbe走/readyz,且 handler 完全分离
健康检查阻塞主HTTP server的典型原因
一个慢健康检查可能拖垮整个服务,尤其在高并发下。这不是性能问题,而是架构误用。
- 在 handler 里调用
db.Ping()或redis.Client.Ping()未设 context 超时:K8s 默认timeoutSeconds: 1,超时即杀 probe 连接,积压请求占满 goroutine 池 - 每次 probe 都新建
sql.DB或redis.Client:fd 快速耗尽,后续所有请求无法建连接 - 用了
time.Sleep或阻塞日志(如log.Printf):高频 probe 下日志卷撑爆,或直接卡住 handler - 正确解法:依赖检查必须复用全局 client,并用
context.WithTimeout(ctx, 1500*time.Millisecond)硬性截断;非强依赖项改异步更新+缓存快照
probe配置与Go代码启动节奏不匹配
容器已启动,但 Go 服务还没真正 ready,probe 却已开始请求,结果必然失败。
立即学习“go语言免费学习笔记(深入)”;
-
initialDelaySeconds设太小:比如 Go 服务要花 8 秒初始化 gRPC client 和 DB 连接池,但 readinessProbe 的initialDelaySeconds: 5,第 6 秒就开始探测,必失败 - 没暴露独立 probe 端口:把
/healthz挂在主http.Server上,一旦业务路由因锁竞争或 GC STW 卡住,probe 也跟着超时,K8s 误判进程死亡 - 正确姿势:用
http.NewServeMux()起独立http.Server监听:8081,完全隔离业务逻辑;并在代码中用sync.WaitGroup或 channel 显式通知“启动完成” - 务必验证:
curl -v http://localhost:8081/healthz和curl -v http://localhost:8081/readyz是否秒回,且 body 非空、header 正确


















