根本原因是/healthz虽返回200,但存在超时、重定向、中间件拦截、外部依赖或未设HTTP超时等行为,导致kubelet判定失败;liveness与readiness共用同一端点更会引发语义冲突和雪崩。

为什么/healthz返回200还会被Kubernetes反复重启
根本原因不是探针没调通,而是探针响应内容或行为和Kubernetes预期不匹配。kubelet只认HTTP状态码,http.StatusOK必须在超时内稳定返回,且不能有重定向、中间件拦截、日志打点或鉴权逻辑干扰。
常见误报现象包括:
-
livenessProbe失败但curl -v http://pod-ip:port/healthz手动测试成功——说明探针路径被中间件吞掉或延迟了 - Pod频繁处于
CrashLoopBackOff但进程日志里没panic——大概率是http.Server没设ReadTimeout/WriteTimeout,某次慢请求卡住整个HTTP handler,导致后续探针超时 - 刚启动就触发liveness失败——
initialDelaySeconds设太小,Go程序还没加载完配置、连上DB就开查
实操建议:
- 健康端点必须用独立
http.ServeMux,不要挂到Gin/echo主路由下,避免中间件污染 - 用
http.ListenAndServe启动的必须换成http.Server实例,并显式设置ReadTimeout、WriteTimeout、IdleTimeout - 检查逻辑里所有外部调用(如
db.PingContext)必须带context.WithTimeout,超时时间 ≤timeoutSeconds - 1
livenessProbe和readinessProbe共用一个/health接口有多危险
共用同一个handler是生产环境最常踩的坑。liveness判定“该不该重启”,readiness判定“该不该转发流量”,语义冲突直接导致滚动更新卡死或服务雪崩。
典型翻车场景:
- 数据库短暂抖动2秒,
readinessProbe失败→Endpoint被摘→流量切走;但livenessProbe也同时失败→Pod被杀→新Pod启动又撞上DB抖动,无限循环 - 收到
SIGTERM后,程序关闭了readiness但liveness仍返回200,Kubernetes认为“还能活”,继续往将死实例发请求
实操建议:
- 拆成两个端点:
/healthz(只检查进程存活,用atomic.LoadInt32(&healthy))、/readyz(检查DB/Redis等依赖) -
livenessProbe只配httpGet+/healthz,readinessProbe配/readyz,两者timeoutSeconds和periodSeconds分开调 - 在
/readyzhandler里主动读原子变量isReady,收到SIGTERM后立刻置0,让Endpoint Controller自动摘除
Gin框架里写健康检查最容易漏掉的三件事
Gin默认行为和Kubernetes健康检查要求天然相斥。不显式干预,90%的Gin服务在生产环境都会出问题。
必须补上的动作:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 禁用调试模式:
gin.SetMode(gin.ReleaseMode),否则/healthz可能被重定向到/debug/...或输出堆栈 - 关掉
router.Run(),改用http.Server启动,并设置IdleTimeout(建议60秒),防止长连接耗尽文件描述符 - 健康端点不用
c.JSON,直接w.WriteHeader(http.StatusOK)+w.Write([]byte("ok")),绕过Gin中间件链和JSON序列化开销
示例片段:
probeMux := http.NewServeMux()
probeMux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
})
go http.ListenAndServe(":8081", probeMux)
注意:这个probeMux必须监听独立端口,Deployment里要声明两个containerPort。
Go panic没被捕获导致健康检查静默失败
容器里panic不等于日志里有堆栈。很多Go服务崩溃后日志为空,是因为panic输出到了os.Stderr,而Docker日志采集器默认只收os.Stdout;或者程序启动时重定向了标准流。
后果是:kubelet连续调/healthz失败,触发重启,但你完全看不到panic源头。
实操建议:
- 所有goroutine入口包一层
recover,尤其是http.Server.Serve、定时任务、消息消费协程 - 别用
log.Fatal退出,它直接os.Exit(1),跳过Shutdown钩子,Kubernetes记为非优雅终止 - 监听
SIGTERM后,先置isReady = 0,再调srv.Shutdown(),确保drain完成才退出
最隐蔽的问题往往不在探针配置里,而在Go运行时对错误的沉默处理——panic不recover、stderr不采集、shutdown不等待,这三者叠加,会让健康检查变成盲区。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










