Table of contents
Open Table of contents
TL;DR
高可用 = 冗余(让故障不致命)+ 自动故障转移(让恢复足够快)。核心公式 Availability = MTBF / (MTBF + MTTR),降低 MTTR 的 ROI 远高于提高 MTBF,因为故障不可避免。每多一个 9,成本和复杂度指数级上升——大多数业务在 99.95%-99.99% 找到平衡点。
SLI → SLO → SLA 体系
三个概念层层递进,不要混用:
- SLI(Service Level Indicator):实际测量值。如”成功请求数/总请求数”
- SLO(Service Level Objective):内部目标,比 SLA 更严格,留 error budget
- SLA(Service Level Agreement):对外合同,违反有赔偿
”几个 9” 的真实含义
| 可用性 | 年停机 | 月停机 | 核心要求 |
|---|---|---|---|
| 99%(2 个 9) | 3.65 天 | 7.3 小时 | 基本不可接受 |
| 99.9%(3 个 9) | 8.76 小时 | 43.8 分钟 | 多实例 + 手动故障转移 |
| 99.95% | 4.38 小时 | 21.9 分钟 | 自动故障转移 + 跨 AZ |
| 99.99%(4 个 9) | 52.6 分钟 | 4.38 分钟 | 零停机部署 + 混沌工程 + 7×24 on-call |
| 99.999%(5 个 9) | 5.26 分钟 | 25.9 秒 | 多 Region 多活 + 秒级自动恢复 |
从 99.9% 到 99.99% 不是提升 0.09%,而是停机时间缩短 10 倍。成本和复杂度从 3-5x 跳到 5-10x。
核心机制
冗余
- Active-Active:所有节点同时工作,无切换延迟,但数据一致性复杂。适合无状态服务
- Active-Passive:主工作备待命,切换有延迟但一致性易保证。适合有状态服务(数据库)
- N+1:N 个节点承载负载 + 1 个冗余。注意峰值时每个节点的余量是否足够
冗余的反直觉陷阱:增加组件数量增加单个组件故障概率,但降低全部同时故障的概率。前提是故障必须独立——所有副本放同一机架,断电全完(correlated failure)。
故障检测
Health Check 两种类型必须区分:
- Liveness:进程还活着吗?不活就重启
- Readiness:能接受流量吗?不能就摘流量
混淆是常见事故原因——数据库连接池满了应该摘流量(readiness),不应该重启 Pod(liveness),重启反而制造连接风暴。
Phi Accrual Failure Detector(Cassandra 使用):不是二元的活/死判断,而是基于历史心跳间隔的统计分布输出”怀疑程度”,比固定超时更适应网络抖动。
自动故障转移
故障转移时间的构成:
总时间 = 检测时间(5-30s) + 确认时间(1-5s) + 切换时间(1-10s) + 客户端发现时间(0-60s)
客户端发现时间常被忽视——DNS TTL 可能 60s,连接池缓存旧连接。这就是为什么”秒级故障转移”实际恢复时间可能是分钟级。
无状态 vs 有状态的 HA 差异
这是高可用设计中最根本的分水岭:
| 无状态服务 | 有状态服务 | |
|---|---|---|
| HA 难度 | 简单:多实例 + LB + 健康检查 | 极复杂:数据复制、切换时数据安全、脑裂 |
| 故障影响 | 任意实例挂,用户无感知 | 需要共识算法或外部协调 |
核心原则:尽一切可能把服务做成无状态,把状态推到专门的有状态组件(数据库、缓存),然后用成熟方案让这些组件高可用。
各层的高可用方案
应用层
部署策略对可用性的影响:
| 策略 | 可用性影响 | 回滚速度 | 核心挑战 |
|---|---|---|---|
| 滚动更新 | 过程中容量降低 | 慢(反向滚动) | 新旧版本并存的兼容性 |
| 蓝绿部署 | 零停机 | 秒级 | DB schema 必须向后兼容 |
| 金丝雀部署 | 影响面可控 | 秒级 | 需要流量分配能力 |
优雅关闭(Graceful Shutdown)的竞态:Kubernetes 中 Pod 被删除时,SIGTERM 和从 Service 摘除是并行的——Pod 可能正在关闭但还在接收新请求。解法:preStop hook 中 sleep 2-5s。
数据层
同步复制 vs 异步复制:
| 维度 | 同步复制 | 异步复制 |
|---|---|---|
| 数据安全 | RPO=0 | 可能丢失最近写入 |
| 写入延迟 | 受最慢副本影响 | 不受副本影响 |
| 可用性 | 副本挂了可能写不进 | 副本挂不影响主库 |
PostgreSQL 的实际做法:synchronous_standby_names = 'FIRST 1 (replica1, replica2)'——至少一个副本确认即可,平衡数据安全和可用性。
网络层
- 多 AZ 部署:AZ 间延迟 < 2ms,物理隔离(独立电力、冷却),同步复制可行
- 多 Region 部署:Region 间延迟高(几十到几百 ms),同步复制不可行,通常 Active-Passive 或 Active-Active(极复杂)
关键设计模式
Circuit Breaker(熔断器)
防止下游故障导致上游资源耗尽,形成级联故障:
CLOSED (正常通行) ──失败率>阈值──▶ OPEN (快速失败)
▲ │
│ 试探成功 超时后试探
└─────── HALF-OPEN (放行少量) ◀──────┘
Retry with Backoff + Jitter
等待时间 = min(base_delay × 2^attempt + random_jitter, max_delay)
- Jitter 必须加:没有 Jitter,所有客户端在相同时间点重试,形成周期性流量尖刺(惊群效应)
- 重试预算:Google SRE 推荐全局最多 10% 的请求可以是重试请求
哪些可以重试:网络超时、503(临时性故障)。不能重试:400/401/404(业务错误)。危险:非幂等操作(除非有 idempotency key)。
超时分层递减
Client 10s → Gateway 8s → Service A 5s → Service B 3s
下游超时必须严格小于上游,否则上游线程全部阻塞在等待下游。
高可用与 CAP
CAP 不是”三选二”——网络分区不可选,实际选择只有分区发生时选 C 还是选 A。没有分区时 C 和 A 可以同时满足。
Error Budget 模型(Google SRE):SLO 99.95% = 每月约 22 分钟的 error budget。Budget 充足可以激进发版,快用完就冻结变更。优雅地解决了”发布速度 vs 稳定性”的矛盾。
Pitfalls
- 加了冗余但没测故障转移 — 数据库配了主从但从未做切换演练,真故障时发现从库复制延迟了 3 小时。对策:至少季度级故障演练
- 主从切换后应用不重连 — 连接池缓存旧主库连接。对策:用连接代理(PgBouncer),或设
target_session_attrs=read-write - Health check 检查的不是真正的健康 — 只返回 200 但数据库已断连。对策:检查关键依赖,但依赖检查放 readiness 不放 liveness
- 脑裂 — 两个节点都认为自己是主。对策:Patroni + etcd 做共识,STONITH 强制关旧主
- 隐性共同依赖 — 消除了明显 SPOF 但所有服务共用一个 DNS 解析器、一个配置中心。对策:画完整依赖拓扑图
- 灰色故障 — 系统没完全挂但丢包 1%、IO 偶尔飙高。Health check 返回 200 但用户体验已经很差。对策:基于 SLI 的告警比硬件指标告警更有效
生产 Checklist
部署:
- 每个服务至少 2 实例,跨至少 2 个 AZ
- 数据库有至少 1 个跨 AZ 同步复制从库
- 零停机部署(滚动/蓝绿),PodDisruptionBudget 已配置
故障检测与恢复:
- 所有服务暴露
/health,区分 liveness 和 readiness - 数据库故障转移自动化(Patroni / RDS Multi-AZ),RTO 已实测
- 应用能自动重连数据库/缓存
容错模式:
- 所有外部调用有超时(从外到内递减)
- 关键调用路径有 Circuit Breaker
- 重试使用 Exponential Backoff + Jitter
- 不同下游的连接池隔离(Bulkhead)
监控与演练:
- 基于 SLI 的告警 + 明确的响应 runbook
- 至少季度级故障演练
- 依赖拓扑图已绘制,隐性单点已识别