Skip to content

可靠性评审清单

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

要点

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

1. 适用时机

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

2. 评审前准备

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

3. 核心检查项

3.1 单点故障

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

Cloudflare 示例:避免单点故障

typescript
// 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 示例:超时与重试

typescript
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 示例:熔断器

typescript
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)
  }
})

降级策略示例

typescript
// 降级策略矩阵
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 示例:限流中间件

typescript
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 示例:最终一致性与补偿

typescript
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 示例

typescript
// 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 调整时,同步调整相关检查项
  • 每半年复审一次,删除过时的检查项

基于 MIT 协议开源