可靠性评审清单
可靠性评审关注系统在故障情况下的行为——单点故障、降级策略、容错机制、数据一致性、恢复能力。目标不是「永远不出故障」,而是「故障了能快速恢复,影响可控」。
要点
- 可靠性评审关注系统在故障下的行为:单点故障、降级、容错、恢复
- 目标不是「永远不出故障」,而是「故障了能快速恢复,影响可控」
- 重点检查:超时重试、熔断降级、限流背压、数据一致性、备份恢复
- 每个外部依赖都要问:它挂了怎么办?
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. 评审会上容易漏的点
- 超时设置:问「下游挂了,这个请求会等多久才失败?」
- 重试风暴:问「重试策略会不会让问题更严重?」
- 降级体验:问「降级后用户看到什么?客服知道吗?」
- 数据一致性:问「转账转到一半失败了,钱在哪?」
- 故障演练:问「上次演练是什么时候?发现了什么?」
5. 通过标准
- 单点故障已识别并有应对
- 超时、重试、熔断、降级策略完整
- 数据一致性方案明确
- 备份与恢复策略到位
- 监控告警覆盖
- 故障演练计划
6. 反模式
- 乐观主义:「下游不会挂」「网络不会抖动」
- 重试一切:所有错误都重试,包括非幂等操作
- 无限等待:不设超时,请求一直挂着
- 硬编码降级:降级逻辑写死,无法动态调整
- 备份不复原:备份了但从没测试过恢复
7. 维护建议
可靠性评审清单需要随系统演进更新:
- 新增依赖时,重新评估单点故障和降级策略
- 出现可靠性事故后,把根因对应的检查项加进来
- SLA/SLO 调整时,同步调整相关检查项
- 每半年复审一次,删除过时的检查项