/healthz仅检查进程存活(accept、goroutine、内存),/readyz并发检查所有依赖并设超时;二者须独立ServeMux、禁用业务中间件、依赖失败记warn不打error、响应≤3秒、结构化日志精简、暴露Prometheus指标、内置指数退避自愈逻辑。

用 net/http 实现两个独立端点:/healthz 和 /readyz
必须拆开,不能共用一个 handler。Kubernetes 的 livenessProbe 和 readinessProbe 语义完全不同,混用会导致误杀——比如 DB 临时超时,/readyz 返回 503 是合理的,但若 /healthz 也查 DB,就会触发重启,加剧雪崩。
-
/healthz只检查进程是否卡死:比如 HTTP server 是否还在 accept 连接、关键 goroutine channel 是否阻塞、内存是否接近 OOM(可用runtime.ReadMemStats粗略判断) -
/readyz才检查依赖:DB、Redis、下游 gRPC 服务等,任一失败就返回http.StatusServiceUnavailable(503),且必须设超时 - 两个端点建议用独立的
http.ServeMux,和主业务路由隔离,防止业务 panic 或中间件崩溃导致健康探针失效
依赖检查必须带 context.WithTimeout,且并发执行
常见错误是串行调用 db.Ping() → redis.Ping() → client.CheckHealth(),一个慢就拖垮整个响应。生产环境要求 /readyz 响应 ≤ 3 秒,否则 K8s 会判定失败。
- 每个依赖检查函数都封装成
func() (string, error)类型,例如DBChecker(db *sql.DB)内部调用db.PingContext(ctx),超时设为 500ms - 用
errgroup.Group并发执行所有 checker,避免单点延迟放大 - 不要在健康检查里做重操作:不刷新缓存、不重连连接池、不 reload 配置——它只反映「此刻是否可响应」,不是「是否完全就绪」
别在 handler 里打 error 日志,也别用 zap.Error 记依赖失败
健康检查被 K8s 每秒轮询多次,如果每次 DB 不通都打一条 error 日志,日志系统直接被刷爆。这不是 bug,是高频探测下的正常现象。
- 依赖失败统一记
warn级,带上组件名和耗时,例如zap.Warn("redis check failed", zap.String("component", "redis"), zap.Float64("latency_ms", 1200)) - 避免结构化日志字段过多(如把整个 error stack 写进日志),健康检查 handler 要轻量,CPU 和 GC 开销都要压到最低
- 如果用了 Prometheus,建议暴露
health_check_failed_total{component="mysql"}这类指标,比日志更适合聚合分析抖动根因
恢复动作不能靠外部平台兜底,得在 Go 里主动做状态驱动协调
健康检查只是“感知”,真正的高可用在于“恢复”。K8s 重启容器是兜底手段,但代价高、有中断;Go 协程更适合做轻量、可控的自愈。
立即学习“go语言免费学习笔记(深入)”;
- 启动一个 goroutine 定期调
/readyz,连续 3 次失败就触发恢复:比如调srv.Shutdown()拒绝新请求、调db.Close()+sql.Open()重建连接、重新加载配置文件 - 对临时性错误(如网络抖动),用指数退避重试,而不是立即 panic ——
backoff.Retry或简单 for-loop +time.Sleep就够用 - 若 5 分钟内重试 10 次仍失败,才
os.Exit(1),把最终决策权交还给容器平台;这时退出是“优雅放弃”,不是“静默崩溃”
最常被忽略的一点:/healthz 和 /readyz 的响应体结构要一致,但语义必须严格分离——前者不许有任何外部 IO,后者必须覆盖所有流量路径依赖。写错一个超时或混用一个 handler,就可能让整套探针机制失效。










