在 Go 中实现 Kubernetes 的 /healthz 和 /readyz 自定义 handler,需分别暴露轻量级 HTTP 端点:/healthz 仅确认进程存活(无阻塞操作),/readyz 可检查外部依赖但须带超时;响应必须为纯文本且非空,使用 context.WithTimeout 控制耗时,避免空 body 导致探测失败。

如何在 Go 中实现 Kubernetes 的 /healthz 自定义 handler
Kubernetes 的健康检查默认走 /healthz(liveness)和 /readyz(readiness),但原生 kubelet 不会自动调用你的 Go 服务——你得自己暴露 HTTP 端点,并确保它符合 kubelet 的探测逻辑。核心不是“注册到 Kubernetes”,而是“让你的 Go HTTP server 正确响应探测请求”。
常见错误是直接返回 200 OK 但忽略响应体格式或超时控制,导致 kubelet 认为失败(尤其在 initialDelaySeconds 较小、服务启动慢时)。
- 用
http.HandleFunc注册/healthz和/readyz,不要依赖第三方健康检查库(除非明确支持 k8s 语义) - 响应必须是纯文本,且 body 至少含一个换行或非空字符;空 body + 200 会被某些 kubelet 版本视为失败
- handler 内部避免阻塞操作(如 DB 连接池满时的同步 dial);建议用带超时的 context 控制检查耗时,例如
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second) - 若 readiness 依赖外部依赖(如 Redis、ConfigMap 内容),应在 handler 中显式校验,而不是只检查进程存活
为什么 livenessProbe 用 /healthz 而 readinessProbe 用 /readyz
这不是强制约定,但它是 Kubernetes 官方推荐的分离方式:/healthz 表示“进程是否还在跑”,/readyz 表示“能否接收流量”。Go 程序里这两个 endpoint 的实现逻辑往往不同——比如 /healthz 只 return nil,而 /readyz 要检查 gRPC server 是否已 Accept 连接、etcd client 是否可 Get。
容易踩的坑是把两者写成完全一样的 handler,结果 liveness 探测失败后容器被反复重启,即使服务其实已经 ready。
立即学习“go语言免费学习笔记(深入)”;
-
/healthz应极轻量:不查 DB、不发 HTTP 请求、不读文件,仅确认 main goroutine 未 panic、关键 channel 未 close -
/readyz可包含有限度的依赖检查,但每项必须有独立超时(如 Redis ping - 若使用
k8s.io/component-base/healthz包,注意它默认把所有 healthz 注册进同一个 mux,且会自动添加/healthz/log等调试路径,生产环境应禁用或过滤
如何让 probe 失败时不触发误重启
根本问题不在 Go 代码,而在 YAML 配置与 Go handler 的配合。kubelet 判断失败的标准是:HTTP status 非 2xx 或响应超时。但很多人忽略了 failureThreshold 和 periodSeconds 的组合效应。
例如 periodSeconds: 3 + failureThreshold: 1 意味着只要一次探测失败就杀 Pod——而 Go 中一次 DB 延迟抖动就可能触发。
- Go handler 里对临时性依赖(如缓存、下游 API)应返回 200 + body “ready: false”,而非直接 503;然后在 readinessProbe 中用
httpGet.action配合expectedStatusCodes: [200],再靠业务逻辑判断 body 内容(需自定义探针脚本或 sidecar) - 更稳妥的做法:livenessProbe 仅用
/healthz,设failureThreshold: 3;readinessProbe 用/readyz,设initialDelaySeconds: 10(给 Go 初始化留足时间) - 避免在 handler 中 panic;panic 会导致整个 http.Server crash,kubelet 收不到任何响应,等同于 timeout
Go 中处理 probe 请求时要注意的上下文生命周期
HTTP handler 的 http.Request.Context() 在 probe 超时时会被 cancel,但 Go 默认不会自动中断 handler 执行——你得手动监听 ctx.Done() 并退出。
典型现象是:kubelet 发起 /readyz 探测,3 秒超时后放弃,但你的 handler 仍在尝试连 MySQL,持续占用 goroutine 和连接,最终拖垮服务。
- 所有阻塞调用(
db.Query,client.Get,time.Sleep)前必须传入 context,例如db.QueryContext(ctx, "SELECT 1") - 不要在 handler 里启新 goroutine 处理检查逻辑(如 go checkDB()),除非你能保证它能响应
ctx.Done() - 若检查逻辑本身无法接受 context(如旧版第三方 SDK),需用
select { case 做兜底超时
最常被忽略的是 init 阶段的资源预热——比如用 sync.Once 做 lazy init,但第一次 /readyz 请求恰好触发它,而 init 里有没加 context 的网络操作,就会卡住整个 probe 流程。


















