← [ BACK_TO_NOTES ]

Go 健康检查端点

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

#concept #growing #dev #backend #go

[!info] related notes

Go 健康检查端点

这个概念为什么会出现

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

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

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

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

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

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

最小可运行示例

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

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

订单服务部署在 Kubernetes:

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

启动后约 100ms 才标记 ready,模拟预热。此例需 Go 1.22+;DB/Redis 检查只是固定返回成功的占位,未实现真实依赖超时、诊断鉴权或停机处理。部署前需接入这些机制。

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. 示例 /health 的两次检查串行共享 500ms 总预算;生产可在总预算内为各依赖分配独立上限,且驱动必须实际支持取消。
  3. 返回 结构化 JSON,状态码对探针友好(成功 200,失败 503)。
  4. 优雅关闭/启动中应让 ready 变假。

核心概念与准确模型

三类探针语义(K8s 对齐)

语义典型路径失败意味着常见动作
Liveness 存活/healthz进程可能无法自行恢复连续失败达阈值后终止容器,按 restartPolicy 处理
Readiness 就绪/readyz暂时不该接流量标记 NotReady,Service 通常不再将其作为就绪后端;传播非瞬时
Startup 启动专用或本地启动状态启动尚未完成成功前不运行 liveness/readiness;失败达阈值也会终止容器

危险混用:把「数据库抖一下」做成 liveness 失败 → 级联重启风暴。依赖故障是否影响 ready 应由实际可服务能力决定;重启不能修复共享数据库故障。

设计原则

  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检查实现本身错误(应少见)

Kubernetes HTTP 探针对 200–399 视为成功,并有重定向处理规则;应直接返回明确的 200/503,避免登录页或跳转掩盖失败。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启动宽限内失败暂不计数;若期间已成功,后续连续失败开始计数
retries连续失败次数阈值

镜像内必须有 curl 和 shell 才能运行此命令(scratch/distroless 通常没有)。Docker HEALTHCHECK 通过命令退出码更新 healthy/unhealthy,单独标记 unhealthy 不会让普通 Docker 容器自动重启;Compose 的 service_healthy 可用于启动依赖等待,也不等于持续自动恢复策略。

Kubernetes 探针草图

livenessProbe:
  httpGet: { path: /healthz, port: 8080 }
  periodSeconds: 10
readinessProbe:
  httpGet: { path: /readyz, port: 8080 }
  periodSeconds: 5
startupProbe:
  httpGet: { path: /healthz, port: 8080 } # 此例进程可响应即视为本地启动成功
  failureThreshold: 30
  periodSeconds: 2

启动判定若包含本地初始化,应使用相应启动端点;不要无意把外部 DB 可达性绑定到 startup 重启条件。

与 go-graceful-shutdown 联动:进入排空时置 ready=false,给端点/LB 更新留出预算,再用 Shutdown 等待在途请求。探针检查、EndpointSlice 更新与 LB 传播是异步的,不能保证先摘流再停;preStop 与退出等待都要纳入终止宽限期。示例 main 尚未实现这段流程。

鉴权与暴露面

  • 集群内探针通常打 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 也不能证明可写或 schema 兼容;应按业务要求检查只读状态、迁移版本等,并控制成本。
  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 错误:给人看懂了,探针没摘流。
正确:明确返回失败码(通常 503);Kubernetes HTTP 探针把 3xx 也视为成功,不能仅靠「非 2xx」。

[!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。
    代码:若每次从尚未到期的父 ctx 派生、串行且调用遵守取消,约 3s;并行并设置总预算可接近 1s。仅调用 WithTimeout 不保证任意操作按时退出。 工程:在终止宽限内安排 ready=false、端点/LB 传播预算与 Shutdown 排空;观测实际传播时间,不把 ready=false 当成已摘流的确认。

延伸阅读与资料来源

资料类型支撑
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/10/6
Digital Biome
Access

键盘导航

j / k / ↑ / ↓
选择对象
Enter / o
打开选择
/ 或 Ctrl+K
全站搜索
i / f / 1–4
筛选当前列表
g → h / l / p / t / a
首页 / 笔记 / 项目 / 工具 / About
r → 1–9
打开所选对象的关联
u
返回上一页
Esc
关闭浮层 / 取消命令;输入框内清空文本