可靠性评审清单

可靠性评审关注系统在故障情况下的行为——单点故障、降级策略、容错机制、数据一致性、恢复能力。目标不是「永远不出故障」,而是「故障了能快速恢复,影响可控」。

要点

  • 可靠性评审关注系统在故障下的行为:单点故障、降级、容错、恢复
  • 目标不是「永远不出故障」,而是「故障了能快速恢复,影响可控」
  • 重点检查:超时重试、熔断降级、限流背压、数据一致性、备份恢复
  • 每个外部依赖都要问:它挂了怎么办?

1. 适用时机

  • 新系统设计(可靠性要从一开始考虑)
  • 现有系统扩展(新增的组件是否引入新的单点故障?)
  • 重大变更前(评估变更对可靠性的影响)
  • 出现可靠性事故后(防止同类问题再次发生)
  • SLA/SLO 制定或调整时

2. 评审前准备

  • 系统依赖图:本系统依赖哪些外部服务?哪些服务依赖本系统?
  • 故障模式分析:每个依赖挂了会怎样?
  • 当前 SLA/SLO:承诺的可用性是多少?实际是多少?
  • 历史事故清单:过去出过什么可靠性问题?根因是什么?
  • 灾备方案:当前有什么灾备机制?演练过吗?

3. 核心检查项

3.1 单点故障

  • 关键组件无单点(Web 服务、数据库、缓存、消息队列)
  • 单点故障已识别并有应对(主备、集群、多活)
  • 网络无单点(多线路、多区域)
  • 第三方服务无单点(主备供应商、降级方案)

Cloudflare 示例:避免单点故障

// Cloudflare Workers 天然多区域部署,无单点故障
// 但要注意依赖的服务
 
// 坏的依赖:单个数据库实例
const db = new Database('single-instance')
 
// 好的依赖:使用托管服务
// D1 自动多副本
// KV 自动多副本
// R2 自动多副本
 
// 第三方服务要有降级方案
async function fetchWithFallback(url: string, fallbackUrl: string) {
  try {
    const response = await fetch(url, {
      signal: AbortSignal.timeout(5000)
    })
    if (response.ok) return response
  } catch (error) {
    console.error('Primary service failed:', error)
  }
 
  // 降级到备用服务
  return fetch(fallbackUrl, {
    signal: AbortSignal.timeout(5000)
  })
}

3.2 超时与重试

  • 所有外部调用都有超时设置(不能让请求无限等待)
  • 超时时间合理(根据下游 P99 调整)
  • 重试策略明确(哪些接口可以重试、重试几次、间隔多久)
  • 重试使用指数退避(避免重试风暴)
  • 重试只用于幂等操作(非幂等操作的重试需要幂等键)

Hono 示例:超时与重试

import { Hono } from 'hono'
 
const app = new Hono()
 
// 带超时的 fetch
async function fetchWithTimeout(
  url: string,
  options: RequestInit = {},
  timeoutMs = 5000
): Promise<Response> {
  return fetch(url, {
    ...options,
    signal: AbortSignal.timeout(timeoutMs)
  })
}
 
// 带重试的 fetch
async function fetchWithRetry(
  url: string,
  options: RequestInit = {},
  maxRetries = 3
): Promise<Response> {
  for (let i = 0; i < maxRetries; i++) {
    try {
      const response = await fetchWithTimeout(url, options)
 
      // 5xx 错误才重试
      if (response.status >= 500 && i < maxRetries - 1) {
        const delay = Math.pow(2, i) * 1000 + Math.random() * 1000
        await new Promise(resolve => setTimeout(resolve, delay))
        continue
      }
 
      return response
    } catch (error) {
      if (i === maxRetries - 1) {
        throw error
      }
 
      // 指数退避 + 抖动
      const delay = Math.pow(2, i) * 1000 + Math.random() * 1000
      await new Promise(resolve => setTimeout(resolve, delay))
    }
  }
 
  throw new Error('Max retries exceeded')
}
 
// 使用示例
app.get('/data', async (c) => {
  try {
    const response = await fetchWithRetry('https://api.example.com/data')
    return c.json(await response.json())
  } catch (error) {
    console.error('Failed to fetch data:', error)
    return c.json({ error: '数据获取失败' }, 503)
  }
})

3.3 熔断与降级

  • 关键外部依赖有熔断机制(连续失败 N 次就停止调用)
  • 熔断恢复策略明确(半开状态、探测频率)
  • 降级方案明确(依赖挂了,提供什么兜底?)
  • 降级对用户体验的影响已沟通
  • 降级开关可动态调整(不要硬编码)

Hono 示例:熔断器

import { Hono } from 'hono'
 
const app = new Hono()
 
// 简单的熔断器实现
class CircuitBreaker {
  private failureCount = 0
  private lastFailureTime = 0
  private state: 'closed' | 'open' | 'half-open' = 'closed'
 
  private readonly failureThreshold = 5
  private readonly recoveryTimeout = 60000 // 1 分钟
 
  async execute<T>(fn: () => Promise<T>): Promise<T> {
    if (this.state === 'open') {
      if (Date.now() - this.lastFailureTime > this.recoveryTimeout) {
        this.state = 'half-open'
      } else {
        throw new Error('Circuit breaker is open')
      }
    }
 
    try {
      const result = await fn()
      this.onSuccess()
      return result
    } catch (error) {
      this.onFailure()
      throw error
    }
  }
 
  private onSuccess() {
    this.failureCount = 0
    this.state = 'closed'
  }
 
  private onFailure() {
    this.failureCount++
    this.lastFailureTime = Date.now()
 
    if (this.failureCount >= this.failureThreshold) {
      this.state = 'open'
    }
  }
}
 
// 使用熔断器
const externalApiBreaker = new CircuitBreaker()
 
app.get('/external-data', async (c) => {
  try {
    const data = await externalApiBreaker.execute(async () => {
      const response = await fetch('https://external-api.com/data')
      return response.json()
    })
 
    return c.json(data)
  } catch (error) {
    console.error('External API failed:', error)
 
    // 降级:返回缓存数据
    const cached = await c.env.KV.get('external-data:cache', 'json')
    if (cached) {
      c.header('X-Cache', 'HIT')
      return c.json(cached)
    }
 
    return c.json({ error: '服务暂时不可用' }, 503)
  }
})

降级策略示例

// 降级策略矩阵
const degradationStrategies = {
  // 推荐服务挂了:返回热门推荐
  'recommendation-service': async (c) => {
    const fallback = await c.env.KV.get('recommendations:fallback', 'json')
    return c.json(fallback || [])
  },
 
  // 搜索服务挂了:返回空结果 + 提示
  'search-service': async (c) => {
    return c.json({
      results: [],
      message: '搜索服务暂时不可用,请稍后再试'
    })
  },
 
  // 支付服务挂了:返回错误,不降级
  'payment-service': async (c) => {
    return c.json({ error: '支付服务暂时不可用' }, 503)
  }
}
 
// 使用降级策略
app.get('/api/recommendations', async (c) => {
  try {
    const response = await fetchWithTimeout(
      'https://recommendation-service.internal/api/recommend',
      {},
      3000
    )
 
    if (!response.ok) throw new Error(`HTTP ${response.status}`)
 
    return c.json(await response.json())
  } catch (error) {
    console.error('Recommendation service failed:', error)
    return degradationStrategies['recommendation-service'](c)
  }
})

3.4 限流与背压

  • 入口限流(防止突发流量压垮系统)
  • 下游限流(防止打垮依赖服务)
  • 限流策略明确(每用户、每 IP、全局)
  • 限流后的用户体验(友好提示、排队机制)
  • 背压机制(上游过载时,下游能告诉上游「慢一点」)

Hono 示例:限流中间件

import { Hono } from 'hono'
 
const app = new Hono()
 
// 简单的限流中间件
async function rateLimiter(c: any, next: any) {
  const key = `ratelimit:${c.req.header('CF-Connecting-IP')}`
  const limit = 100 // 每分钟 100 次
  const window = 60 // 60 秒窗口
 
  // 获取当前计数
  const current = await c.env.KV.get(key, 'json') || { count: 0, resetAt: Date.now() + window * 1000 }
 
  // 检查是否超限
  if (Date.now() > current.resetAt) {
    // 窗口过期,重置
    current.count = 0
    current.resetAt = Date.now() + window * 1000
  }
 
  if (current.count >= limit) {
    return c.json({
      error: '请求过于频繁,请稍后再试',
      retryAfter: Math.ceil((current.resetAt - Date.now()) / 1000)
    }, 429)
  }
 
  // 增加计数
  current.count++
  await c.env.KV.put(key, JSON.stringify(current), {
    expirationTtl: window
  })
 
  // 设置响应头
  c.header('X-RateLimit-Limit', limit.toString())
  c.header('X-RateLimit-Remaining', (limit - current.count).toString())
  c.header('X-RateLimit-Reset', new Date(current.resetAt).toISOString())
 
  return next()
}
 
app.use('/api/*', rateLimiter)
 
app.get('/api/data', (c) => {
  return c.json({ data: 'ok' })
})

3.5 数据一致性

  • 关键业务数据一致性保证(事务、补偿、对账)
  • 最终一致场景有补偿机制(重试、对账、告警)
  • 分布式事务方案明确(Saga、TCC、本地消息表)
  • 数据冲突处理策略(乐观锁、版本号、最后写入胜出)

Hono 示例:最终一致性与补偿

import { Hono } from 'hono'
 
const app = new Hono()
 
// 分布式事务:Saga 模式
app.post('/orders', async (c) => {
  const body = await c.req.json()
  const orderId = crypto.randomUUID()
 
  try {
    // Step 1: 创建订单
    await c.env.DB.prepare('INSERT INTO orders (id, status) VALUES (?, ?)')
      .bind(orderId, 'pending')
      .run()
 
    // Step 2: 扣减库存
    const inventoryResult = await deductInventory(c.env, body.items)
    if (!inventoryResult.success) {
      throw new Error('库存不足')
    }
 
    // Step 3: 扣款
    const paymentResult = await processPayment(c.env, body.userId, body.amount)
    if (!paymentResult.success) {
      // 补偿:恢复库存
      await restoreInventory(c.env, body.items)
      throw new Error('支付失败')
    }
 
    // Step 4: 更新订单状态
    await c.env.DB.prepare('UPDATE orders SET status = ? WHERE id = ?')
      .bind('completed', orderId)
      .run()
 
    return c.json({ orderId, status: 'completed' }, 201)
  } catch (error) {
    // 补偿:删除订单
    await c.env.DB.prepare('DELETE FROM orders WHERE id = ?')
      .bind(orderId)
      .run()
 
    console.error('Order creation failed:', error)
    return c.json({ error: error.message }, 400)
  }
})
 
// 对账任务:定时检查数据一致性
app.get('/internal/reconcile', async (c) => {
  // 检查订单和支付记录是否一致
  const orders = await c.env.DB.prepare(
    'SELECT * FROM orders WHERE status = ? AND created_at > ?'
  )
    .bind('completed', Date.now() - 86400000) // 最近 24 小时
    .all()
 
  const issues = []
 
  for (const order of orders.results) {
    const payment = await c.env.DB.prepare(
      'SELECT * FROM payments WHERE order_id = ?'
    )
      .bind(order.id)
      .first()
 
    if (!payment) {
      issues.push({ orderId: order.id, issue: 'missing_payment' })
    } else if (payment.amount !== order.amount) {
      issues.push({ orderId: order.id, issue: 'amount_mismatch' })
    }
  }
 
  if (issues.length > 0) {
    // 发送告警
    console.error('Reconciliation issues:', issues)
  }
 
  return c.json({ checked: orders.results.length, issues: issues.length })
})

3.6 备份与恢复

  • 数据备份策略(全量 + 增量、频率、保留时长)
  • 备份恢复演练(定期测试,不是「备份了但从没试过」)
  • RPO(恢复点目标)明确(能容忍丢多少数据?)
  • RTO(恢复时间目标)明确(能容忍停服多久?)
  • 跨地域灾备(核心系统)

3.7 监控与告警

  • 关键业务指标监控(订单量、注册数、成功率)
  • 系统指标监控(CPU、内存、磁盘、网络)
  • 依赖服务监控(下游服务的健康状态)
  • 告警规则合理(不漏报、不乱报)
  • 告警升级路径明确(初级 on-call → 高级 on-call → 负责人)

3.8 故障演练

  • 定期故障演练(Chaos Engineering、红蓝对抗)
  • 演练覆盖常见故障场景(网络延迟、服务宕机、数据库故障)
  • 演练结果记录和改进跟踪
  • 新系统上线前做过故障演练

3.9 可观测性

  • 日志完整(关键操作、错误、上下文)
  • 链路追踪接入(能看到请求在所有服务间的流转)
  • 指标聚合和查询(快速定位问题)
  • 故障时的排查手册(Runbook)

3.10 SLA / SLO / Error Budget

  • SLA(服务等级协议)明确(承诺给用户的可用性)
  • SLO(服务等级目标)明确(内部目标,比 SLA 严格)
  • Error Budget 明确(允许多少不可用时间)
  • Error Budget 消耗监控(预算快用完时要减速发布)

SLO 示例

// SLO 定义
const SLO = {
  availability: 0.999,  // 99.9% 可用性
  latency: {
    p50: 100,   // P50 < 100ms
    p95: 500,   // P95 < 500ms
    p99: 1000   // P99 < 1000ms
  },
  errorRate: 0.001  // 错误率 < 0.1%
}
 
// Error Budget 计算
const totalMinutesInMonth = 30 * 24 * 60
const allowedDowntime = totalMinutesInMonth * (1 - SLO.availability)
// 99.9% 可用性 = 允许 43.2 分钟停机时间
 
// 监控 Error Budget 消耗
app.get('/internal/slo-status', async (c) => {
  const now = new Date()
  const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1)
 
  // 查询本月错误率
  const errorRate = await calculateErrorRate(c.env, startOfMonth, now)
 
  const budgetConsumed = errorRate / SLO.errorRate
  const budgetRemaining = 1 - budgetConsumed
 
  return c.json({
    slo: SLO,
    current: {
      errorRate,
      budgetConsumed: `${(budgetConsumed * 100).toFixed(2)}%`,
      budgetRemaining: `${(budgetRemaining * 100).toFixed(2)}%`
    },
    status: budgetRemaining > 0.5 ? 'healthy' : budgetRemaining > 0 ? 'warning' : 'critical'
  })
})

4. 评审会上容易漏的点

  1. 超时设置:问「下游挂了,这个请求会等多久才失败?」
  2. 重试风暴:问「重试策略会不会让问题更严重?」
  3. 降级体验:问「降级后用户看到什么?客服知道吗?」
  4. 数据一致性:问「转账转到一半失败了,钱在哪?」
  5. 故障演练:问「上次演练是什么时候?发现了什么?」

5. 通过标准

  • 单点故障已识别并有应对
  • 超时、重试、熔断、降级策略完整
  • 数据一致性方案明确
  • 备份与恢复策略到位
  • 监控告警覆盖
  • 故障演练计划

6. 反模式

  • 乐观主义:「下游不会挂」「网络不会抖动」
  • 重试一切:所有错误都重试,包括非幂等操作
  • 无限等待:不设超时,请求一直挂着
  • 硬编码降级:降级逻辑写死,无法动态调整
  • 备份不复原:备份了但从没测试过恢复

7. 维护建议

可靠性评审清单需要随系统演进更新:

  • 新增依赖时,重新评估单点故障和降级策略
  • 出现可靠性事故后,把根因对应的检查项加进来
  • SLA/SLO 调整时,同步调整相关检查项
  • 每半年复审一次,删除过时的检查项