Go 健康检查端点
Go 服务健康检查端点应快速、安全、语义清晰:区分存活与就绪,结构化报告依赖状态,并与 Docker HEALTHCHECK、Kubernetes 探针协同。
[!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)。
结合场景再看四个关注点
/healthz轻、/readyz含关键依赖。- 每个依赖检查带 独立超时。
- 返回 结构化 JSON,状态码对探针友好(成功 200,失败 503)。
- 优雅关闭/启动中应让 ready 变假。
核心概念与准确模型
三类探针语义(K8s 对齐)
| 语义 | 典型路径 | 失败意味着 | 常见动作 |
|---|---|---|---|
| Liveness 存活 | /healthz | 进程可能卡死 | 重启容器 |
| Readiness 就绪 | /readyz | 暂时不该接流量 | 摘除 Endpoints |
| Startup 启动 | 可复用 ready/专用 | 启动慢尚未好 | 推迟 liveness 判定 |
危险混用:把「数据库抖一下」做成 liveness 失败 → 级联重启风暴。数据库抖动更应影响 ready,而不是杀进程(除非有明确「无 DB 进程无意义」的单实例工具场景)。
设计原则
- 快速:个位数到几百毫秒级;依赖检查必须超时。
- 安全:不返回连接串、密码、内部主机名细节(排障字段也要控权)。
- 语义稳定:路径与 JSON 字段变更要当 API 合同管理。
- 区分关键与非关键依赖:
- 关键(如主库)失败 → ready fail / 503
- 非关键(如可选 AI、分析库)失败 →
degraded,是否 200 看产品
- 不抢业务线程池:检查应短;避免在 health 里跑全表扫描或真实用户流程。
检查什么
| 依赖 | 典型方式 | 是否关键 |
|---|---|---|
| 进程自身 | 能进 Handler 即存活 | 存活必需 |
| 主数据库 | PingContext | 通常就绪必需 |
| 缓存 Redis | Ping | 视是否可降级 |
| 消息队列 | 探测连接/元数据 | 视角色 |
| 上游 AI | HTTP /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 做成每分钟的全量深度巡检报告。
设计动机
- 自动化运维的最小合同
- 防止「半死不活」实例继续杀用户请求
- 把启动、运行、排空三阶段表达清楚
- 故障时快速定位是哪一个依赖(结构化 deps)
边界情况与反直觉行为
- 健康检查把依赖打挂
频率过高 + 重查询 → 自致故障;用 ping/轻量命令。 - 共享全局超时过大
一个慢依赖拖满整个/readyz;应每依赖独立短超时,可并行errgroup。 - liveness 依赖外部系统
外部抖动触发重启雪崩。 - 只在主库写路径失败才 503,但 ping 一直成功
ping 不能代表「模式迁移中/只读」等;关键业务可增加轻量SELECT 1或版本检查。 - 关闭中仍 ready=true
LB 继续灌流量,与优雅关闭打架。 - 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 路由挂在鉴权之外。
工程实践
- 路径命名:
/healthz/readyz或/live/ready,团队统一。 - 并行探测依赖,总时间 ≈ 最慢依赖,而非求和。
- 与 DI 一致:health 使用与业务相同的 DB 池,才能反映真实连接问题。
- 测试:表驱动模拟依赖 down/up,断言码与 JSON。
- 发布:迁移窗口可短暂 ready fail,或使用单独 maintenance 信号。
- 多租户/多依赖:body 里 deps 列表,避免只说 no。
- 文档:在 runbook 写「503 + db 意味着什么、如何处理」。
- 本地开发:可用轻量 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 补指标。
自测题
概念题
- 为什么数据库短暂超时更适合让
/readyz失败,而不是/healthz? - Kubelet 是否会解析
{"status":"fail"}来摘流? - 可选 AI 依赖失败时,何种产品语义下仍应 200 ready?
代码推理题
/readyz 串行 ping 三个依赖各 WithTimeout(1s),最坏耗时约多少?如何改为有上界且接近 1s?
工程思考题
滚动发布时旧 pod 收到 SIGTERM:应如何与 ready 探针、LB 摘流顺序配合,才能减少 502?
参考答案
展开
- 避免外部抖动触发重启风暴;摘流即可,进程往往仍健康。
- 默认不会;看 HTTP 状态码。
- 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 后端)
- 本篇状态:已深化
- 建议下一篇:优雅关闭