21.09-健康检查接口

要点

  • 健康检查接口用于让监控系统自动检测服务是否可用
  • 区分 Liveness Probe(是否活着)和 Readiness Probe(是否准备好接收流量)
  • 检查依赖服务的连通性:数据库、缓存、第三方 API
  • 返回详细的状态信息,方便排查问题

内容

1. 为什么需要健康检查

在以下场景中,健康检查是必不可少的:

场景用途
负载均衡自动剔除不健康的实例
KubernetesLiveness Probe 重启挂掉的 Pod,Readiness Probe 控制流量
监控系统检测服务是否可用,触发告警
部署系统部署后自动检查是否启动成功

没有健康检查,你只能靠用户反馈才能发现问题。有了健康检查,监控系统会在你发现问题之前主动告警。

2. 基础健康检查

最简单的健康检查接口:

// src/index.ts
import { Hono } from 'hono'
 
const app = new Hono()
 
app.get('/health', (c) => {
  return c.json({ status: 'ok' })
})

这个接口返回 {"status": "ok"},状态码 200。监控系统定期请求这个接口,如果返回 5xx 或超时,就认为服务不可用。

但这个接口太简单了,它只告诉你「进程还在」,不告诉你「服务是否能正常工作」。

3. 深度健康检查

更完善的健康检查应该验证依赖服务:

// src/routes/health.ts
app.get('/health', async (c) => {
  const checks = {
    status: 'ok',
    timestamp: new Date().toISOString(),
    version: process.env.APP_VERSION || 'unknown',
    checks: {} as Record<string, { status: string; latency?: number; error?: string }>,
  }
 
  // 1. 检查数据库
  const dbStart = Date.now()
  try {
    await c.env.DB.prepare('SELECT 1').first()
    checks.checks.database = {
      status: 'ok',
      latency: Date.now() - dbStart,
    }
  } catch (err) {
    checks.checks.database = {
      status: 'error',
      error: err.message,
    }
    checks.status = 'degraded'
  }
 
  // 2. 检查缓存
  const cacheStart = Date.now()
  try {
    await c.env.CACHE.get('health:check')
    checks.checks.cache = {
      status: 'ok',
      latency: Date.now() - cacheStart,
    }
  } catch (err) {
    checks.checks.cache = {
      status: 'error',
      error: err.message,
    }
    checks.status = 'degraded'
  }
 
  // 3. 检查 LLM API(可选,可能比较慢)
  if (c.req.query('deep') === 'true') {
    const llmStart = Date.now()
    try {
      await fetch('https://api.openai.com/v1/models', {
        headers: { 'Authorization': `Bearer ${c.env.OPENAI_API_KEY}` },
      })
      checks.checks.llm = {
        status: 'ok',
        latency: Date.now() - llmStart,
      }
    } catch (err) {
      checks.checks.llm = {
        status: 'error',
        error: err.message,
      }
      checks.status = 'degraded'
    }
  }
 
  const statusCode = checks.status === 'ok' ? 200 : 503
  return c.json(checks, statusCode)
})

返回示例:

{
  "status": "ok",
  "timestamp": "2026-06-21T10:30:00.000Z",
  "version": "1.2.3",
  "checks": {
    "database": {
      "status": "ok",
      "latency": 5
    },
    "cache": {
      "status": "ok",
      "latency": 2
    },
    "llm": {
      "status": "ok",
      "latency": 150
    }
  }
}

4. Liveness 和 Readiness

Kubernetes 区分两种健康检查:

4.1 Liveness Probe(存活探针)

问:「进程还活着吗?需要重启吗?」

// 只检查进程是否还在运行
app.get('/health/live', (c) => {
  return c.json({ status: 'ok' })
})

如果 Liveness Probe 失败,Kubernetes 会重启 Pod。

4.2 Readiness Probe(就绪探针)

问:「服务准备好接收流量了吗?」

// 检查所有依赖服务
app.get('/health/ready', async (c) => {
  const checks = await checkDependencies(c.env)
 
  const isReady = Object.values(checks).every(c => c.status === 'ok')
  const statusCode = isReady ? 200 : 503
 
  return c.json({
    status: isReady ? 'ready' : 'not_ready',
    checks,
  }, statusCode)
})

如果 Readiness Probe 失败,Kubernetes 会把 Pod 从负载均衡中剔除,但不会重启。

5. 依赖检查的实现

// src/lib/health-checks.ts
export interface HealthCheckResult {
  status: 'ok' | 'error' | 'degraded'
  latency?: number
  error?: string
  details?: Record<string, unknown>
}
 
export async function checkDatabase(env: Env): Promise<HealthCheckResult> {
  const start = Date.now()
  try {
    const result = await env.DB.prepare('SELECT 1 as value').first()
    return {
      status: 'ok',
      latency: Date.now() - start,
      details: { value: result?.value },
    }
  } catch (err) {
    return {
      status: 'error',
      latency: Date.now() - start,
      error: err.message,
    }
  }
}
 
export async function checkCache(env: Env): Promise<HealthCheckResult> {
  const start = Date.now()
  try {
    const key = 'health:check'
    const value = await env.CACHE.get(key)
 
    if (!value) {
      // 缓存未命中,写入一个测试值
      await env.CACHE.put(key, 'ok', { expirationTtl: 60 })
    }
 
    return {
      status: 'ok',
      latency: Date.now() - start,
    }
  } catch (err) {
    return {
      status: 'error',
      latency: Date.now() - start,
      error: err.message,
    }
  }
}
 
export async function checkLLMAPI(env: Env): Promise<HealthCheckResult> {
  const start = Date.now()
  try {
    const response = await fetch('https://api.openai.com/v1/models', {
      headers: { 'Authorization': `Bearer ${env.OPENAI_API_KEY}` },
    })
 
    if (!response.ok) {
      return {
        status: 'error',
        latency: Date.now() - start,
        error: `HTTP ${response.status}`,
      }
    }
 
    return {
      status: 'ok',
      latency: Date.now() - start,
    }
  } catch (err) {
    return {
      status: 'error',
      latency: Date.now() - start,
      error: err.message,
    }
  }
}
 
export async function checkKV(env: Env): Promise<HealthCheckResult> {
  const start = Date.now()
  try {
    const key = 'health:check'
    await env.KV.put(key, 'ok')
    const value = await env.KV.get(key)
 
    if (value !== 'ok') {
      return {
        status: 'error',
        latency: Date.now() - start,
        error: 'KV write/read mismatch',
      }
    }
 
    return {
      status: 'ok',
      latency: Date.now() - start,
    }
  } catch (err) {
    return {
      status: 'error',
      latency: Date.now() - start,
      error: err.message,
    }
  }
}
 
export async function checkR2(env: Env): Promise<HealthCheckResult> {
  const start = Date.now()
  try {
    const key = 'health/check.txt'
    await env.R2.put(key, 'ok')
    const obj = await env.R2.get(key)
 
    if (!obj) {
      return {
        status: 'error',
        latency: Date.now() - start,
        error: 'R2 write/read mismatch',
      }
    }
 
    return {
      status: 'ok',
      latency: Date.now() - start,
    }
  } catch (err) {
    return {
      status: 'error',
      latency: Date.now() - start,
      error: err.message,
    }
  }
}
// src/routes/health.ts
import {
  checkDatabase,
  checkCache,
  checkLLMAPI,
  checkKV,
  checkR2,
} from '../lib/health-checks'
 
app.get('/health', async (c) => {
  const checks = {
    database: await checkDatabase(c.env),
    cache: await checkCache(c.env),
    kv: await checkKV(c.env),
    r2: await checkR2(c.env),
  }
 
  // 深度检查(可选)
  if (c.req.query('deep') === 'true') {
    checks.llm = await checkLLMAPI(c.env)
  }
 
  const hasError = Object.values(checks).some(c => c.status === 'error')
  const allDegraded = Object.values(checks).every(c => c.status !== 'ok')
 
  let status: 'ok' | 'degraded' | 'error'
  if (allDegraded) {
    status = 'error'
  } else if (hasError) {
    status = 'degraded'
  } else {
    status = 'ok'
  }
 
  const statusCode = status === 'ok' ? 200 : 503
 
  return c.json({
    status,
    timestamp: new Date().toISOString(),
    version: process.env.APP_VERSION || 'unknown',
    checks,
  }, statusCode)
})

6. 健康检查的频率和超时

不同场景对健康检查的要求不同:

场景频率超时
负载均衡每 5-10 秒2 秒
Kubernetes Liveness每 10 秒5 秒
Kubernetes Readiness每 10 秒5 秒
外部监控(UptimeRobot)每 1-5 分钟30 秒

注意:

  • 健康检查不要太频繁,否则会成为性能负担
  • 超时时间要小于监控系统的检查间隔
  • 深度检查(如 LLM API)可能很慢,只在需要时启用

7. 安全考虑

健康检查接口不应该暴露敏感信息:

// ❌ 错误做法:暴露数据库连接字符串
app.get('/health', (c) => {
  return c.json({
    database: c.env.DATABASE_URL,  // 泄露敏感信息
  })
})
 
// ✅ 正确做法:只返回状态
app.get('/health', (c) => {
  return c.json({
    status: 'ok',
    checks: {
      database: { status: 'ok' },  // 只返回状态
    },
  })
})

如果健康检查接口需要鉴权:

// 只对内部监控系统开放
app.get('/health', basicAuth({
  username: 'monitor',
  password: process.env.HEALTH_CHECK_PASSWORD,
}), async (c) => {
  // ...
})

或者只对特定 IP 开放:

app.get('/health', async (c, next) => {
  const ip = c.req.header('CF-Connecting-IP')
  const allowedIPs = ['10.0.0.0/8', '172.16.0.0/12']
 
  if (!isIPInRanges(ip, allowedIPs)) {
    return c.json({ error: 'Forbidden' }, 403)
  }
 
  await next()
})

8. 实战:完整的健康检查系统

// src/routes/health.ts
import { Hono } from 'hono'
import {
  checkDatabase,
  checkCache,
  checkLLMAPI,
  checkKV,
  checkR2,
} from '../lib/health-checks'
 
const health = new Hono()
 
// 1. Liveness Probe:只检查进程是否活着
health.get('/live', (c) => {
  return c.json({ status: 'ok' })
})
 
// 2. Readiness Probe:检查依赖服务
health.get('/ready', async (c) => {
  const checks = {
    database: await checkDatabase(c.env),
    cache: await checkCache(c.env),
    kv: await checkKV(c.env),
    r2: await checkR2(c.env),
  }
 
  const isReady = Object.values(checks).every(c => c.status === 'ok')
 
  return c.json({
    status: isReady ? 'ready' : 'not_ready',
    checks,
  }, isReady ? 200 : 503)
})
 
// 3. 完整健康检查:包含深度检查
health.get('/', async (c) => {
  const checks = {
    database: await checkDatabase(c.env),
    cache: await checkCache(c.env),
    kv: await checkKV(c.env),
    r2: await checkR2(c.env),
  }
 
  if (c.req.query('deep') === 'true') {
    checks.llm = await checkLLMAPI(c.env)
  }
 
  const hasError = Object.values(checks).some(c => c.status === 'error')
  const allDegraded = Object.values(checks).every(c => c.status !== 'ok')
 
  let status: 'ok' | 'degraded' | 'error'
  if (allDegraded) {
    status = 'error'
  } else if (hasError) {
    status = 'degraded'
  } else {
    status = 'ok'
  }
 
  return c.json({
    status,
    timestamp: new Date().toISOString(),
    version: process.env.APP_VERSION || 'unknown',
    uptime: process.uptime(),
    checks,
  }, status === 'ok' ? 200 : 503)
})
 
export default health
// src/index.ts
import health from './routes/health'
 
app.route('/health', health)

9. 小结

健康检查的关键点:

  1. 基础检查:返回 &#123;"status": "ok"&#125;,用于负载均衡和进程存活检测
  2. 深度检查:验证数据库、缓存、第三方 API 的连通性
  3. Liveness vs Readiness:前者用于重启,后者用于流量控制
  4. 性能考虑:健康检查不要太频繁,深度检查只在需要时启用
  5. 安全考虑:不要暴露敏感信息,可以加鉴权或 IP 白名单

健康检查是运维自动化的基础。没有它,你就只能靠人工巡检。有了它,监控系统会自动发现问题并告警。