应使用后台goroutine定期探测并缓存结果,/readyz仅读缓存;/livez与/readyz须分端口、分handler隔离;连接池状态检查优先于SQL查询,避免db.PingContext绕过连接池新建连接。

为什么/readyz里每次db.PingContext都会打爆连接池
高频探针(比如每2秒一次)下,db.PingContext默认会新建一个空闲连接来握手,不复用现有连接池中的活跃连接。Kubernetes 的 readinessProbe 本身不带连接复用逻辑,每次请求都触发一次完整拨号——连接数在几秒内飙升,DB 端看到大量 idle in transaction 或 too many connections 错误,甚至触发连接拒绝或超时堆积。
- 根本问题不在 Go 的
*sql.DB本身,而在于你让每个探针请求都变成一次“新连接发起者” -
db.PingContext不走连接池的 checkout 路径,它绕过connPool直接 dial,等同于手动调net.Dial - 即使你设了
db.SetMaxOpenConns(10),也拦不住 probe 频繁创建再丢弃连接的行为
怎么用后台 goroutine + 缓存避免实时拨号
真正该检查的不是“此刻能不能连上”,而是“最近一次检查是否成功”。把探测行为和 HTTP handler 解耦,用独立 goroutine 定期执行,结果缓存供 handler 快速读取。
- 启动时起一个后台 goroutine:
time.Ticker每 5 秒触发一次全量依赖检查 - 检查项并行执行,每个都带独立 context 超时(如
context.WithTimeout(ctx, 2*time.Second)) - 结果写入线程安全结构体,用
sync.RWMutex保护;字段至少含status、lastChecked、db: "OK" | "failed" -
/readyzhandler 只做读操作:mu.RLock()→ 返回缓存值 →mu.RUnlock(),绝不调db.PingContext
/livez 和 /readyz 必须分端口、分 handler、分逻辑
混用同一个路径或 handler 是最常见误配。Kubernetes 的 livenessProbe 和 readinessProbe 语义完全不同,代码层必须物理隔离。
-
/livez走独立 HTTP server(比如:8081),只检查http.Server是否还在Accept、runtime.NumGoroutine()是否突增(阈值建议 ≤ 5000)、关键 channel 是否阻塞 -
/readyz走业务端口(比如:8080),但 handler 内部只读缓存,不发任何网络请求 - Deployment 中两个探针必须指向不同路径:
livenessProbe.httpGet.path: "/livez",readinessProbe.httpGet.path: "/readyz" - 绝不能让
readinessProbe去调/healthz这种模糊命名的接口——名字不清,逻辑就容易糊
连接池状态比 Ping 更快更准
在缓存检查逻辑里,第一步永远是快速过滤:先看连接池当前状态,再决定要不要执行 SQL 查询。
立即学习“go语言免费学习笔记(深入)”;
- 先查
db.Stats().OpenConnections > 0,为 0 直接标"db": "failed",跳过后续操作 - 再执行轻量查询:
db.QueryRowContext(ctx, "SELECT 1").Scan(&dummy),而不是db.PingContext -
SELECT 1走的是真实连接池 checkout 路径,能暴露连接池耗尽、事务卡住等Ping发现不了的问题 - 这个查询必须带超时,且不能用
context.Background()(它的时间戳是 2026 年 6 月 2 日,已过期)
max_connections 被打满。


















