Skip to content

健康检查: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 极简,是构建高可用、弹性系统的关键一步。