健康检查:app.get('/healthz', (c) => c.text('OK')) 的最佳实践
排除数据库连接检查,避免级联故障
健康检查(Health Check)是现代应用运维的基石,用于告知负载均衡器、Kubernetes 或监控系统:“我是否正常”。然而,一个设计不当的健康检查可能适得其反——将局部故障放大为全局服务不可用。
最典型的反模式就是:在 /healthz 中检查数据库、缓存等外部依赖。本文阐述为何应避免此做法,并提供生产环境的最佳实践。
一、问题:为什么不该在 /healthz 中检查数据库?
ts
// 反模式:包含外部依赖检查
app.get('/healthz', async (c) => {
try {
await c.env.DB.prepare('SELECT 1').run() // 检查数据库
return c.text('OK') // 数据库连通才返回 OK
} catch (err) {
return c.text('Database Unhealthy', 503)
}
})危害:
| 问题 | 说明 |
|---|---|
| 级联故障(Cascading Failure) | 数据库短暂抖动 → 所有实例健康检查失败 → 负载均衡器摘除全部实例 → 服务完全不可用 |
| 雪崩效应 | 实例被摘除后,剩余实例压力倍增 → 更多实例超时 → 全部崩溃 |
| 掩盖真实问题 | 健康检查失败只表明“数据库有问题”,但无法区分是应用问题还是 DB 问题 |
| 自愈能力丧失 | 即使应用本身已恢复,只要 DB 未恢复,实例就无法重新上线 |
健康检查的目标是检测本实例的健康状态,而非整个系统的可用性。
二、正确理念:/healthz 应仅检查本地状态
健康检查的核心原则: 轻量、快速、无副作用、不依赖外部系统
它应该回答一个问题: “这个进程是否在运行,并且能够处理请求?”
而不是: “整个系统是否 100% 正常?”
三、最佳实践:极简的 /healthz
ts
// 推荐:仅返回静态响应
app.get('/healthz', (c) => {
return c.text('OK')
})优点:
- 极致性能:无 IO、无计算;
- 高可用:即使数据库宕机,实例仍能通过健康检查;
- 快速恢复:实例重启后立即可被调度流量;
- 明确语义:
200 OK表示“本实例存活”。
四、进阶实践:有限的内部检查
如果你确实需要一些基本检查,也应避免强依赖外部系统。
场景 1:检查事件循环是否阻塞
ts
app.get('/healthz', (c) => {
const start = Date.now()
// 触发一次微任务,检测事件循环延迟
return new Promise((resolve) => {
setImmediate(() => {
const latency = Date.now() - start
if (latency > 100) {
// 事件循环严重阻塞,可能过载
c.status(503)
resolve(c.text('High Latency'))
} else {
resolve(c.text('OK'))
}
})
})
})这可以发现 CPU 过载或长时间同步操作。
场景 2:检查本地资源(如磁盘空间)
仅在必要时(如日志密集型服务):
ts
// Node.js 示例
import { statfs } from 'fs'
app.get('/healthz', (c) => {
try {
const stats = statfs.sync('/var/log') // 同步调用需谨慎
const freePercent = (stats.bfree / stats.blocks) * 100
if (freePercent < 10) {
return c.text('Low Disk Space', 503)
}
return c.text('OK')
} catch {
return c.text('Disk Check Failed', 500)
}
})注意:此类检查会增加复杂性和开销,通常不推荐。
五、替代方案:分离健康检查与就绪检查
Kubernetes 等平台支持两种探针:
| 探针类型 | 路径建议 | 检查内容 |
|---|---|---|
Liveness Probe (/healthz) | /healthz | 实例是否存活(简单) |
Readiness Probe (/readyz) | /readyz | 是否准备好接收流量(可包含依赖) |
Startup Probe (/startup) | /startup | 初始化是否完成 |
示例:
ts
// Liveness: 我还活着吗?
app.get('/healthz', (c) => c.text('OK'))
// Readiness: 我能处理请求吗?(可选检查 DB)
app.get('/readyz', async (c) => {
try {
await c.env.DB.prepare('SELECT 1').run()
return c.text('Ready')
} catch {
return c.text('DB Not Ready', 503)
}
})
// Startup: 我启动完成了吗?
app.get('/startup', (c) => {
if (app.isInitialized) {
return c.text('Started')
}
return c.text('Starting...', 503)
})关键区别:/readyz 失败只会导致该实例不再接收新流量,但不会被重启或摘除。
六、监控与告警:使用专用端点
不要用健康检查做系统监控!
- 使用
/metrics(Prometheus)监控数据库延迟、错误率; - 使用 Sentry、Datadog 等工具捕获异常;
- 设置独立的
DB connectivity alert,而非依赖/healthz。
七、总结
| 实践 | 推荐度 | 说明 |
|---|---|---|
c.text('OK') | ✅✅✅ | 最佳,确保实例级高可用 |
| 检查数据库连接 | ❌ | 导致级联故障,禁止 |
| 检查事件循环延迟 | ✅(可选) | 发现性能瓶颈 |
分离 /healthz 和 /readyz | ✅✅ | 适合复杂系统 |
记住: /healthz 的目标是让健康的实例不被误杀,而不是检测所有依赖。
保持 /healthz 极简,是构建高可用、弹性系统的关键一步。