Go HTTP服务应暴露/healthz端点供Kubernetes livenessProbe使用,仅检查进程自身状态(如内存标志),不依赖外部组件,响应须≤1秒、无日志、绕过中间件,并配置initialDelaySeconds避免启动时误判。

Go HTTP 服务怎么暴露 /healthz 端点
Kubernetes 的 livenessProbe 默认靠 HTTP GET 请求判断容器是否存活,所以你的 Go 服务得提供一个稳定、轻量、不依赖外部组件的健康检查接口。别用 /health 这种泛名——K8s 社区约定用 /healthz(带 z 是为了和 /health 区分,避免误命中其他路由)。
关键不是“能不能返回 200”,而是“返回 200 时是否真代表进程可服务”。常见错误是直接写个空 handler:http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) })——这完全没意义,进程卡死、goroutine 泄漏、数据库连接断开都检测不到。
- 只检查内存中状态(如
atomic.LoadInt64(&healthy)),不碰磁盘、网络、数据库等外部依赖 - 响应必须在 1 秒内完成,否则 K8s 可能超时重试或重启
- 不要记录日志(除非打到 stderr 且级别为 error),高频探针会撑爆日志卷
- 如果用了 Gin/Echo,记得用
ctx.AbortWithStatus(200)而非ctx.String(200, "ok"),避免中间件干扰
为什么不能在 /healthz 里查数据库连接
查数据库 = 引入网络 I/O + 超时风险 + 连接池竞争。一旦 DB 暂时不可用,livenessProbe 就失败,K8s 会杀掉 Pod 并重建——但新 Pod 启动后依然连不上 DB,结果就是无限重启循环(CrashLoopBackOff)。
这不是健康检查,这是自毁开关。真正的健康检查只回答一个问题:“这个进程自己还能跑吗?” DB 连通性属于 readinessProbe 范畴,它决定“要不要把流量转给我”,而不是“我该不该被干掉”。
立即学习“go语言免费学习笔记(深入)”;
-
livenessProbe失败 → K8s 杀进程 → 重新启动容器 -
readinessProbe失败 → K8s 摘掉 Service endpoint → 流量不进来,但进程继续运行 - 若你硬要在
/healthz里加 DB 检查,请用极短超时(context.WithTimeout(ctx, 100 * time.Millisecond))并忽略错误,只当它“不可用但不影响进程存活”
如何让探针不被中间件拖垮
很多 Go Web 框架默认给所有路由套上日志、鉴权、CORS、panic recover 中间件。这些对业务请求合理,但对每秒一次的 /healthz 是纯负担:recover 中间件捕获 panic 是好意,可健康端点本就不该 panic;日志中间件每秒记一条,日志量翻倍;鉴权中间件还可能触发 JWT 解析或远程校验。
最稳妥的做法是绕过整个路由中间件栈,用独立的 http.ServeMux 或直接注册到 http.DefaultServeMux 上:
go
// 单独起一个轻量 mux,不走主服务中间件
healthMux := http.NewServeMux()
healthMux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(200)
w.Write([]byte("ok"))
})
go http.ListenAndServe(":8081", healthMux) // 单独端口
- 不要复用主服务的
Router实例(比如 Gin 的r := gin.Default()) - 如果必须共用端口,用
http.Handle("/healthz", ...)直接注册,确保它在中间件链最外层 - 避免在 handler 里调用
time.Now()、runtime.NumGoroutine()等可能触发 GC 或锁竞争的操作
Pod 启动后探针立即失败怎么办
K8s 默认 initialDelaySeconds: 0,意味着容器一启动就发第一个探针。而 Go 服务从 main() 执行到 http.ListenAndServe 启动监听,中间有初始化 DB 连接池、加载配置、预热缓存等耗时操作。此时端口还没 bind,探针必然 connection refused,连续失败几次就被判“不健康”。
解决办法不是调大 failureThreshold,而是让 K8s 知道“我需要一点时间”。设 initialDelaySeconds: 5(根据你服务冷启动实测时间定),再配合 periodSeconds: 10 和 timeoutSeconds: 1 —— 这样既不会太激进,也不会太迟钝。
- 别信“我的服务启动只要 200ms”,加上 GC、调度延迟、容器网络就绪时间,保守按 3–5 秒起步
- 用
kubectl logs <pod> -c <container>看启动日志,确认Server listening on :8080出现时刻 - 如果服务启用了 graceful shutdown,确保探针端点不被
Shutdown()提前关闭
实际部署时最容易被忽略的,是探针路径和端口在 livenessProbe 配置里写错——比如服务监听 :8080,但 probe 写成 port: 8081;或者路径写了 /health,代码里却是 /healthz。这种低级错误不会报语法错,只会让 Pod 一直 restart,排查起来反而最费时间。










