Go 健康检查端点

Go 服务健康检查端点应快速、安全、语义清晰:区分存活与就绪,结构化报告依赖状态,并与 Docker HEALTHCHECK、Kubernetes 探针协同。

#type / concept #status / growing #tech / dev #tech / dev / backend #resource / go

[!info] 关联笔记

Go 健康检查端点

这个概念为什么会出现

进程「在监听端口」不等于「能正确服务流量」:

  • 进程活着,但数据库连接池耗尽
  • 配置热更新失败,关键依赖不可达
  • 正在优雅关闭,不应再进新流量
  • 容器已启动,路由/缓存预热未完成

编排系统(Docker、Kubernetes、负载均衡)需要一个廉价、可自动化调用的信号,以决定:

  • 要不要重启容器(存活)
  • 要不要把实例加进负载均衡池(就绪)
  • 启动期失败是否容忍(启动探针)

健康检查端点就是这个信号的 HTTP 形态。它不是给人类看的复杂监控大盘(那是 metrics/tracing),而是机器可读的门禁

[!abstract] 一句话理解 健康检查用稳定路径快速回答「能否承担哪类流量」:存活看进程自洽,就绪看关键依赖与是否接客;返回码与结构化 body 供探针与排障共用,且绝不泄露密钥。

最小可运行示例

先把示例放进业务场景,再看代码:

场景:K8s 探针区分存活 / 就绪,排障看 /health

订单服务部署在 Kubernetes:

  • liveness/healthz:进程卡死才重启
  • readiness/readyz:启动未完成或主库不通 → 摘流量,不杀进程
  • 运维排障可看 /health(含可选依赖,应内网/鉴权)

启动后约 100ms 才标记 ready,模拟预热。

package main

import (
	"context"
	"encoding/json"
	"log"
	"net/http"
	"sync/atomic"
	"time"
)

// depStatus:单个依赖的检查结果(机器可读)。
type depStatus struct {
	Name   string `json:"name"`
	OK     bool   `json:"ok"`
	Detail string `json:"detail,omitempty"`
}

// healthResponse:探针/排障共用的 body 合同。
type healthResponse struct {
	Status string      `json:"status"` // ok | degraded | fail
	Deps   []depStatus `json:"deps,omitempty"`
}

func main() {
	// ready:是否接客(启动中 / 优雅关闭中应为 false)
	var ready atomic.Bool
	ready.Store(false)

	// 模拟预热完成后才就绪
	go func() {
		time.Sleep(100 * time.Millisecond)
		ready.Store(true)
	}()

	mux := http.NewServeMux()

	// 存活:进程能响应即可——勿绑 DB,避免抖动导致重启风暴
	mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
		writeJSON(w, http.StatusOK, healthResponse{Status: "ok"})
	})

	// 就绪:生命周期 + 关键依赖(主库)
	mux.HandleFunc("GET /readyz", func(w http.ResponseWriter, r *http.Request) {
		if !ready.Load() {
			writeJSON(w, http.StatusServiceUnavailable, healthResponse{
				Status: "fail",
				Deps:   []depStatus{{Name: "lifecycle", OK: false, Detail: "starting or draining"}},
			})
			return
		}

		// 依赖检查必须有超时,不能拖死探针
		ctx, cancel := context.WithTimeout(r.Context(), 300*time.Millisecond)
		defer cancel()

		dbOK := pingDB(ctx)
		deps := []depStatus{{Name: "db", OK: dbOK}}
		if !dbOK {
			writeJSON(w, http.StatusServiceUnavailable, healthResponse{Status: "fail", Deps: deps})
			return
		}
		writeJSON(w, http.StatusOK, healthResponse{Status: "ok", Deps: deps})
	})

	// 排障详检:可含非关键依赖;应鉴权或仅内网
	mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
		ctx, cancel := context.WithTimeout(r.Context(), 500*time.Millisecond)
		defer cancel()
		dbOK := pingDB(ctx)
		redisOK := pingRedis(ctx)
		deps := []depStatus{
			{Name: "db", OK: dbOK},
			{Name: "redis", OK: redisOK},
		}
		status := "ok"
		code := http.StatusOK
		if !dbOK {
			status, code = "fail", http.StatusServiceUnavailable
		} else if !redisOK {
			// 缓存可选:标记 degraded,是否仍 200 看产品
			status = "degraded"
		}
		writeJSON(w, code, healthResponse{Status: status, Deps: deps})
	})

	log.Fatal(http.ListenAndServe(":8080", mux))
}

func writeJSON(w http.ResponseWriter, code int, v any) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(code)
	_ = json.NewEncoder(w).Encode(v)
}

func pingDB(ctx context.Context) bool {
	// 生产:return db.PingContext(ctx) == nil
	_ = ctx
	return true
}

func pingRedis(ctx context.Context) bool {
	_ = ctx
	return true
}

建议运行:

go run .
# 终端 2:
curl -s http://localhost:8080/healthz
curl -s http://localhost:8080/readyz
curl -s http://localhost:8080/health

期望输出类似:

{"status":"ok"}
{"status":"ok","deps":[{"name":"db","ok":true}]}
{"status":"ok","deps":[{"name":"db","ok":true},{"name":"redis","ok":true}]}

启动后立刻打 /readyz 可能短暂 503(lifecycle 未 ready)。

结合场景再看四个关注点

  1. /healthz/readyz 含关键依赖。
  2. 每个依赖检查带 独立超时
  3. 返回 结构化 JSON,状态码对探针友好(成功 200,失败 503)。
  4. 优雅关闭/启动中应让 ready 变假。

核心概念与准确模型

三类探针语义(K8s 对齐)

语义典型路径失败意味着常见动作
Liveness 存活/healthz进程可能卡死重启容器
Readiness 就绪/readyz暂时不该接流量摘除 Endpoints
Startup 启动可复用 ready/专用启动慢尚未好推迟 liveness 判定

危险混用:把「数据库抖一下」做成 liveness 失败 → 级联重启风暴。数据库抖动更应影响 ready,而不是杀进程(除非有明确「无 DB 进程无意义」的单实例工具场景)。

设计原则

  1. 快速:个位数到几百毫秒级;依赖检查必须超时。
  2. 安全:不返回连接串、密码、内部主机名细节(排障字段也要控权)。
  3. 语义稳定:路径与 JSON 字段变更要当 API 合同管理。
  4. 区分关键与非关键依赖
    • 关键(如主库)失败 → ready fail / 503
    • 非关键(如可选 AI、分析库)失败 → degraded,是否 200 看产品
  5. 不抢业务线程池:检查应短;避免在 health 里跑全表扫描或真实用户流程。

检查什么

依赖典型方式是否关键
进程自身能进 Handler 即存活存活必需
主数据库PingContext通常就绪必需
缓存 RedisPing视是否可降级
消息队列探测连接/元数据视角色
上游 AIHTTP /health常为可选
磁盘空间可选特定服务

AI 类依赖常标为可选:模型服务挂了,登录与非 AI API 仍应 ready(若产品如此定义)。

状态码约定

HTTP含义(约定)
200通过该探针
503未通过;编排应摘流或重试
500检查实现本身错误(应少见)

探针主要看 状态码;body 给人对账与日志。不要只返回 200 却在 JSON 里写 "status":"fail" 却期望 K8s 摘流——默认 kubelet 不解析你的业务 JSON

Docker HEALTHCHECK

HEALTHCHECK --interval=30s --timeout=3s --start-period=30s --retries=3 \
  CMD curl -fsS http://127.0.x.x:8080/readyz || exit 1
参数含义
interval检查周期
timeout单次检查超时
start-period启动宽限,失败不计入 unhealthy
retries连续失败次数阈值

选择 ready 还是 live 作为 Docker health,取决于你希望 Docker 把「不健康」理解成什么;在 Compose 依赖顺序里,常用 ready 语义。

Kubernetes 探针草图

livenessProbe:
  httpGet: { path: /healthz, port: 8080 }
  periodSeconds: 10
readinessProbe:
  httpGet: { path: /readyz, port: 8080 }
  periodSeconds: 5
startupProbe:
  httpGet: { path: /readyz, port: 8080 }
  failureThreshold: 30
  periodSeconds: 2

go-graceful-shutdown 联动:收到 SIGTERM → 先 ready=false(或 Shutdown 不再接新连接)→ 等待 in-flight → 退出。这样 ready 探针会先摘流,再停进程。

鉴权与暴露面

  • 集群内探针通常打 pod IP + containerPort,可不上公网 Ingress。
  • 若必须经网关:确保 不强制用户 JWT(见 go-routing-patterns 分组)。
  • 详细依赖诊断接口应 内网或 mTLS/admin auth,避免信息泄露。

与可观测性的分工

机制职责
Health 端点门禁:接不接流量、要不要重启
Metrics量化:错误率、延迟、池利用率
Logs / Traces解释:为何失败

不要把 health 做成每分钟的全量深度巡检报告。

设计动机

  1. 自动化运维的最小合同
  2. 防止「半死不活」实例继续杀用户请求
  3. 把启动、运行、排空三阶段表达清楚
  4. 故障时快速定位是哪一个依赖(结构化 deps)

边界情况与反直觉行为

  1. 健康检查把依赖打挂
    频率过高 + 重查询 → 自致故障;用 ping/轻量命令。
  2. 共享全局超时过大
    一个慢依赖拖满整个 /readyz;应每依赖独立短超时,可并行 errgroup
  3. liveness 依赖外部系统
    外部抖动触发重启雪崩。
  4. 只在主库写路径失败才 503,但 ping 一直成功
    ping 不能代表「模式迁移中/只读」等;关键业务可增加轻量 SELECT 1 或版本检查。
  5. 关闭中仍 ready=true
    LB 继续灌流量,与优雅关闭打架。
  6. IPv6/localhost 差异
    HEALTHCHECK 用 127.0.x.x 与监听 0.0.x.x 需一致可达。

常见误区

[!warning] 常见误区:一个 /health 包打天下 错误:存活、就绪、深度诊断混一个端点且都查全依赖。
正确:至少区分 live/ready;诊断可另开并控权。

[!warning] 常见误区:JSON 写 fail 但 HTTP 200 错误:给人看懂了,探针没摘流。
正确:失败必须非 2xx(通常 503)。

[!warning] 常见误区:检查里打印连接串或错误原文到公网 错误:detail: "postgres://user:pass@..."
正确:枚举化 unreachable / timeout,细节进服务端日志。

[!warning] 常见误区:健康检查走完整中间件链(强制登录) 错误:探针无 Token → 401 → 实例永不就绪。
正确:health 路由挂在鉴权之外。

工程实践

  1. 路径命名/healthz /readyz/live /ready,团队统一。
  2. 并行探测依赖,总时间 ≈ 最慢依赖,而非求和。
  3. 与 DI 一致:health 使用与业务相同的 DB 池,才能反映真实连接问题。
  4. 测试:表驱动模拟依赖 down/up,断言码与 JSON。
  5. 发布:迁移窗口可短暂 ready fail,或使用单独 maintenance 信号。
  6. 多租户/多依赖:body 里 deps 列表,避免只说 no。
  7. 文档:在 runbook 写「503 + db 意味着什么、如何处理」。
  8. 本地开发:可用轻量 always-ready,但勿让生产配置漂移未测。

标准库注册与中间件

root := http.NewServeMux()
root.HandleFunc("GET /healthz", live)
root.HandleFunc("GET /readyz", ready)

api := auth(apiMux)
root.Handle("/api/", api)

handler := chain(root, recoverMW, requestID) // 不要把 auth 包在最外层全局

本节总结

  • 健康检查是 编排门禁,不是监控替代品。
  • 存活轻、就绪准、启动宽容
  • 状态码给机器,结构化 deps 给人;失败用 503。
  • 超时、鉴权旁路、优雅摘流 是工程三大纪律。
  • 下一步go-graceful-shutdown 与部署探针联调;go-observability 补指标。

自测题

概念题

  1. 为什么数据库短暂超时更适合让 /readyz 失败,而不是 /healthz
  2. Kubelet 是否会解析 {"status":"fail"} 来摘流?
  3. 可选 AI 依赖失败时,何种产品语义下仍应 200 ready?

代码推理题

/readyz 串行 ping 三个依赖各 WithTimeout(1s),最坏耗时约多少?如何改为有上界且接近 1s?

工程思考题

滚动发布时旧 pod 收到 SIGTERM:应如何与 ready 探针、LB 摘流顺序配合,才能减少 502?

参考答案

展开
  1. 避免外部抖动触发重启风暴;摘流即可,进程往往仍健康。
  2. 默认不会;看 HTTP 状态码。
  3. AI 非关键路径,核心 CRUD 仍可服务时,可 degraded/仍 ready。
    代码:最坏约 3s;并行探测 + 各 1s 超时,总上界约 1s 级。
    工程:SIGTERM → ready=false 或停止注册 → 等探针/LB 摘流 → Shutdown 等待 in-flight → 退出。

延伸阅读与资料来源

资料类型支撑
Kubernetes — Configure Probes文档live/ready/startup 语义
Dockerfile HEALTHCHECK文档容器健康检查参数
Package net/http标准库端点实现
Package context标准库检查超时
Package database/sql — PingContext标准库DB 探测

笔记元信息

  • 建议文件名:go-health-check-endpoint.md
  • 所属阶段:阶段七(服务工程 / Web 后端)
  • 本篇状态:已深化
  • 建议下一篇:优雅关闭
创建于 2026/6/25 更新于 2026/7/15