健康检查接口必须返回200且不带body;就绪探针需验证下游依赖连通性并避免耗时操作;路由注册顺序和中间件可能影响probe可达性。

健康检查接口必须返回 200 且不带 body
Go 的 http.HandleFunc 或 http.ServeMux 注册的健康检查端点,最常踩的坑是返回非 200 状态码或意外写入响应体。Kubernetes 的 liveness probe 默认只看 HTTP 状态码,只要不是 2xx 就触发重启;而 readiness probe 同样只依赖状态码判断是否转发流量。所以:
- 不要返回 http.StatusServiceUnavailable(503)来“表示不健康”,这会让 kubelet 认为服务挂了
- 不要调用 json.NewEncoder(w).Encode(...),哪怕内容是 {"status":"ok"} —— 只要写了 body,某些 Ingress(如 nginx-ingress)可能缓存响应并污染后续请求
- 最简实现就是:
http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
就绪探针需要检查依赖服务连通性
就绪(readiness)不是“进程活着”,而是“能正常处理请求”。这意味着你得主动验证下游依赖:数据库连接、Redis、gRPC 服务等。常见错误是把就绪和健康混为一谈,只检查自身 goroutine 是否卡死。
- 用 context.WithTimeout 包裹每个依赖检查,超时时间建议 ≤ 2s(避免 probe 超时被 kubelet 判定失败)
- 检查 DB 连接时,别只调 db.Ping(),它可能走本地连接池缓存;应执行一条轻量 SQL,比如 SELECT 1
- 如果依赖不可达,直接 return,**不要**写入 response body 或设置非 200 状态码 —— 就绪探针失败只需让 handler 不写任何内容(或仅 w.WriteHeader(http.StatusOK)),kubelet 会因无响应/超时/非 2xx 视为未就绪
- 示例片段:
http.HandleFunc("/readyz", func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 1500*time.Millisecond)
defer cancel()
if err := db.QueryRowContext(ctx, "SELECT 1").Scan(&dummy); err != nil {
return // 不写任何响应,kubelet 自动标记 NotReady
}
w.WriteHeader(http.StatusOK)
})
避免在 probe handler 中做耗时或阻塞操作
probe 请求由 kubelet 频繁发起(默认每 10s 一次),如果 handler 里做了锁竞争、长循环、同步日志写入,会导致整个 HTTP server 线程阻塞,进而拖垮业务请求。
- 不要在 /healthz 或 /readyz 里调用 log.Printf 或 fmt.Println —— 日志库底层可能加锁或刷盘
- 不要读取大文件、解析配置、加载模板 —— 这些该在服务启动时完成
- 不要调用 time.Sleep 模拟检查(调试除外),它会让 probe 超时
- 如果必须做异步检查(如定期刷新缓存状态),用全局变量 + 原子操作维护一个 atomic.Bool,probe handler 只读这个标志位
HTTP 路由注册顺序影响 probe 可达性
如果你用的是 gorilla/mux 或自定义中间件,probe 路径可能被中间件拦截或路由匹配失败。例如:
- 中间件对所有路径做 JWT 解析,但 probe 请求没带 token → 返回 401 → kubelet 当作不健康
- http.ServeMux 是前缀匹配,若先注册了 http.HandleFunc("/api/", ...),再注册 http.HandleFunc("/healthz", ...),没问题;但如果反过来,/healthz 被 / 的兜底 handler 拦截了,就永远 404
- 使用 gorilla/mux 时,probe 路由必须显式注册,不能依赖 r.PathPrefix("/").Handler(...) 的 fallback
- 验证方式:用 curl -v http://localhost:8080/healthz 直连服务,确认返回 200 且无 body、无重定向、无额外 header(如 Content-Length: 0 是 OK 的)
实际部署中,最容易被忽略的是就绪探针的检查粒度 —— 它不是“DB 能连上就行”,而是“当前实例能否在 SLA 内完成核心业务链路”。比如订单服务的 /readyz 应该模拟一次库存扣减+消息投递,而不是只 ping MySQL。这点没法靠通用库解决,得按业务语义定制。


















