12 分布式可靠性模式
概述#
分布式系统不能靠"希望不出问题"来保证可靠性。本章展开五种核心可靠性模式——冗余降级、熔断、限流、重试退避、故障隔离——它们共同构成服务韧性的基础层,是 SRE 在实际系统设计中最大的技术价值输出。
分布式系统的故障是必然的——网络会抖、节点会挂、依赖会慢。可靠性设计不是"防止故障",而是"让故障的影响可控"。这五种模式就是控制故障影响的工程手段。
一、冗余与降级:可靠性设计的起点#
在进入具体模式之前,先明确两个基础概念:
| 概念 | 含义 | 案例 |
|---|---|---|
| 冗余(Redundancy) | 多副本/多活部署,单个节点故障不影响整体 | K8s Deployment replicas≥2、多 AZ 部署 |
| 降级(Degradation) | 部分功能不可用时,核心流程仍可走通 | 推荐系统挂了,首页仍能展示 |
降级的核心是优雅地部分失败(Fail Partial),而不是全部崩溃。比如支付不可用时购物车仍然可用,只是结账时告之稍后重试。
冗余的层次#
| 层次 | 做法 | 防护的故障 |
|---|---|---|
| Pod 级 | replicas ≥ 2 | 单 Pod crash |
| 节点级 | PodAntiAffinity 跨节点 | 单节点故障 |
| AZ 级 | 跨可用区部署 | 单 AZ 故障 |
| Region 级 | 多 Region 异步复制 | 单 Region 灾难 |
每多一层冗余,成本翻倍但可用性提升一个数量级。要根据服务的 SLO 决定冗余层次——内部工具 Pod 级就够,核心交易要 AZ 级,全球服务要 Region 级。
降级的设计原则#
降级不是"挂了就返回错误",是"挂了就返回次优响应":
- 推荐服务挂 → 返回热门商品列表(缓存)
- 搜索服务慢 → 返回简化结果(少聚合)
- 用户画像挂 → 返回默认画像
- 评论服务挂 → 商品页不显示评论但能下单
降级需要预先设计——不是事故时临时决定"要不要降级",而是代码里就写好了 fallback 逻辑,依赖不可用时自动触发。
二、熔断(Circuit Breaker)#
问题#
A 服务调用 B 服务,B 服务响应变慢。A 的线程池被一堆等待 B 响应的请求占满,然后 A 也无法响应上游。雪崩由此开始——一个下游的慢最终拖垮整个调用链上游。
B 慢 → A 线程池被 B 的请求占满 → A 无法响应其他请求
→ A 的上游也卡住 → 整个调用链雪崩这是分布式系统最常见的故障传播模式——不是"挂了传播",而是"慢了传播"。慢比挂更危险,因为挂会快速失败,慢会持续占用资源。
解决方案:三状态熔断器#
┌──────────┐
│ CLOSED │ ← 正常状态,请求正常通过
└────┬─────┘
失败次数达阈值│
▼
┌──────────┐
│ OPEN │ ← 熔断打开,请求直接拒绝(快速失败)
└────┬─────┘
冷却时间到 │
▼
┌──────────┐
│ HALF-OPEN│ ← 试探性放行少量请求
└────┬─────┘
成功 → CLOSED │ 失败 → OPEN三状态详解#
| 状态 | 行为 | 目的 |
|---|---|---|
| CLOSED(闭合) | 请求正常通过,统计失败次数 | 正常工作 |
| OPEN(断开) | 请求立即返回错误,不实际调用下游 | 保护下游不被持续打挂,让线程池不阻塞 |
| HALF-OPEN(半开) | 放行少量探测请求,成功则恢复 CLOSED | 探测下游是否已恢复 |
关键参数#
| 参数 | 建议值 | 含义 |
|---|---|---|
| 失败阈值 | 5-10 次(1 分钟窗口内) | 触发 OPEN |
| 熔断时长 | 30s-60s | OPEN 持续多久后进 HALF-OPEN |
| HALF-OPEN 探测数 | 1-3 个请求 | 试探多少请求 |
| 失败定义 | 超时 + 5xx + 连接错误 | 什么算"失败" |
| 慢调用阈值 | 延迟 > 2s 算失败 | 防止"慢但不报错"拖垮系统 |
实际案例#
摘自 A 旗舰发布场景:某次发布中,payment-api 因数据库慢查询导致调用下游 signal-api 超时。未配熔断时,payment-api 的 gRPC 连接池被所有等待 signal-api 超时的请求占满,导致 payment-api 无法响应任何请求。加熔断后,signal-api 异常时 payment-api 直接快速失败返回 fallback 响应,主流程不受影响。
熔断器实现注意事项#
- 区分不同错误类型(超时 vs 5xx 可能需要不同的熔断阈值)
- 熔断器本身需要监控(OPEN 次数、HALF-OPEN 成功率)
- 在网关层做粗粒度熔断,在服务调用层做细粒度熔断
- 熔断 OPEN 时要有 fallback 逻辑(返回缓存、默认值、降级响应)
- 熔断器状态变化要发告警——OPEN 是异常状态,需要关注
常见实现#
- Hystrix(Java,停止维护,但概念经典)
- Resilience4j(Java,Hystrix 的现代替代)
- Sentinel(阿里开源,功能丰富)
- Istio OutlierDetection(服务网格层,语言无关)
- Envoy(代理层熔断)
服务网格层熔断(Istio/Envoy)的好处是语言无关、配置统一;应用层熔断(Resilience4j/Sentinel)的好处是细粒度、可结合业务逻辑。两者通常组合使用。
三、限流(Rate Limiting)#
令牌桶(Token Bucket)#
最常用的限流算法:
令牌生成速率 = R 个/秒
桶容量 = B 个令牌(允许的突发量)
请求到达 → 取令牌 → 有 → 放行
→ 无 → 拒绝(或排队)| 参数 | 含义 | 典型值 |
|---|---|---|
| Rate(R) | 令牌生成速率 | 如 1000 QPS |
| Burst(B) | 桶容量,允许的突发请求数 | 如 R × 2 |
令牌桶的特点是允许突发——桶里有令牌时可以瞬间处理多个请求,适合"平时低流量、偶尔突发"的场景。
漏桶(Leaky Bucket)#
以恒定速率处理请求,超出排队的丢弃:
请求流入(任意速率)→ [漏桶队列] → 固定速率流出处理漏桶的特点是强制平滑——不管请求多突发,处理速率恒定。适合"必须匀速处理"的场景,如消息队列消费。
两种算法对比#
| 令牌桶 | 漏桶 | |
|---|---|---|
| 突发处理 | 允许突发 | 拒绝突发 |
| 实现复杂度 | 中 | 低 |
| 适用场景 | API 网关、用户请求 | 消息队列、后台任务 |
| 用户体验 | 好(偶尔突发不被拒) | 一致(但突发会被拒) |
限流层次#
| 层级 | 粒度 | 工具 |
|---|---|---|
| 入口网关 | 按 IP / API Key | Istio RateLimit / NGINX / Envoy |
| 服务间调用 | 按 caller service | Sidecar 限流、Sentinel |
| 业务层 | 按用户 / 租户 | 自定义中间件 |
| 数据库 | 按连接数 | 连接池限流 |
多层限流是必要的——入口限流防 DDoS,服务间限流防雪崩,业务层限流防单用户滥用,数据库限流防连接耗尽。
限流 vs 熔断 的区别#
| 熔断 | 限流 | |
|---|---|---|
| 触发条件 | 下游错误率高/响应慢 | 请求量超阈值 |
| 目的 | 保护自己不被慢下游拖垮 | 保护下游不被过量请求打挂 |
| 粒度 | 按"下游健康"动态切换 | 按 QPS/并发数硬限制 |
| 动态性 | 自适应(根据错误率自动开/关) | 通常是静态配置 |
| 方向 | 阻断"自己→下游"的调用 | 阻断"上游→自己"的请求 |
两者互补:限流防过载,熔断防雪崩。一个完整的可靠性方案两者都需要。
限流的响应策略#
被限流时怎么响应?四种选择:
- 拒绝:返回 429 Too Many Requests(最简单)
- 排队:请求进入队列等待(适合可延迟任务)
- 降级:返回缓存或简化结果(用户体验好)
- ** Shed Load**:丢弃部分请求保核心(极端过载时)
选择取决于业务——用户请求适合拒绝或降级,后台任务适合排队。
四、重试退避(Retry with Backoff)#
为什么要重试#
分布式系统中,网络抖动、服务临时过载、Pod 重启导致的瞬时不可用是常态。适当的重试能把这些瞬时故障从"用户可见的错误"变成"透明恢复"。
统计显示,分布式系统中 90%+ 的故障是瞬时的(< 1 秒)。不重试 = 把 90% 的可恢复故障变成用户可见错误。
重试的三大风险#
- 重试风暴:下游已经够慢了,上游再重试 3 次 → 请求量变成 3 倍 → 雪崩加速
- 非幂等操作重试:扣款请求被重试 → 用户被扣两次
- 无限制重试:永远重试 → 线程池耗尽
正确的重试策略#
| 策略 | 做法 | 适用场景 |
|---|---|---|
| 指数退避(Exponential Backoff) | 重试间隔 100ms → 200ms → 400ms → 800ms | 通用 |
| 指数退避 + Jitter | 退避间隔加随机抖动,避免"惊群效应" | 大规模集群 |
| 最大重试次数 | 3 次,超过则失败 | 通用 |
| 总超时限制 | 所有重试总时间不超过 X 秒 | 用户体验敏感 |
| 仅重试可重试错误 | 429/503/超时可重试,400/401 不重试 | 按错误类型 |
| circuit breaker 联动 | 熔断 OPEN 时不重试 | 防止重试打挂下游 |
Jitter(抖动)的必要性#
假设 100 个调用方同时因下游故障触发重试。如果都用固定间隔(1s),它们会在同一时刻同时重试 → 下游从"挂掉"变成"被 100 个请求瞬间打死"。加上随机 Jitter 后,重试分散在 1s-2s 的时间窗口内 → 下游有喘息空间。
无 Jitter: T+1s → 100 个请求同时到达 ← 惊群
有 Jitter: T+1s~2s → 100 个请求散布在 1s 窗口内Jitter 的实现:
import random
import time
def retry_with_jitter(func, max_retries=3):
for attempt in range(max_retries):
try:
return func()
except RetryableError:
if attempt == max_retries - 1:
raise
# 指数退避 + Jitter
base = 0.1 * (2 ** attempt) # 100ms, 200ms, 400ms
jitter = random.uniform(0, base / 2) # 0 ~ base/2 的随机
time.sleep(base + jitter)幂等性是重试的前提#
重试必须保证幂等——同样的请求执行多次和一次效果相同。
| 操作类型 | 幂等性 | 重试安全性 |
|---|---|---|
| GET / PUT | 幂等 | 安全重试 |
| DELETE | 幂等(删除已删除的返回 404 但效果相同) | 安全重试 |
| POST(创建) | 非幂等 | 需要幂等键 |
| 支付/扣款 | 非幂等 | 必须幂等键 |
非幂等操作的重试必须用幂等键(idempotency key)——客户端生成唯一 ID,服务端去重。这是支付系统的标配。
五、故障隔离(Fault Isolation)#
舱壁模式(Bulkhead)#
从船舶设计借用的概念:船体被分隔成多个水密舱,一个舱进水不会让整艘船沉没。
在软件架构中:
| 隔离维度 | 做法 | 效果 |
|---|---|---|
| 线程池隔离 | 不同下游使用独立线程池 | 慢下游只占满自己的池,不影响其他下游 |
| 连接池隔离 | 不同数据库使用独立连接池 | 一个 DB 连接满不影响另一个 DB |
| 部署隔离 | 核心服务和非核心服务分离部署 | 非核心 OOM 不影响核心 |
| 租户隔离 | 大客户单独资源池 | 一个大客户的突发流量不影响其他客户 |
| 资源隔离 | CPU/内存 limits | 一个 Pod 不吃光节点资源 |
线程池隔离的工程实现#
调用方服务
├── 线程池 A(10 线程)→ 下游 A(慢)
├── 线程池 B(10 线程)→ 下游 B(正常)
└── 线程池 C(5 线程) → 下游 C
→ 下游 A 变慢,占满 10 线程,但 B 和 C 不受影响如果不做隔离:所有请求共享一个线程池,下游 A 慢 → 线程池被占满 → B 和 C 也无法调用。
K8s 层面的隔离#
- PodDisruptionBudget(PDB):限制同时不可用的 Pod 数量
- PodAntiAffinity:确保同服务 Pod 不打到同一个节点
- Resource Requests/Limits:防止一个 Pod 吃光节点资源
- NetworkPolicy:限制不必要的网络访问
- PriorityClass:高优先级 Pod 优先调度
- NodePool 专用:核心服务用专用节点池
租户隔离的层次#
SaaS 系统的租户隔离有多个层次:
| 层次 | 隔离强度 | 成本 | 适用 |
|---|---|---|---|
| 共享应用 + 逻辑隔离 | 弱 | 低 | 小客户 |
| 独立 Pod + 共享 DB | 中 | 中 | 中客户 |
| 独立 namespace + 独立 DB | 强 | 高 | 大客户 |
| 独立集群 | 最强 | 最高 | 企业客户 |
隔离层次选择取决于客户规模和付费水平——小客户共享资源摊低成本,大客户独立资源保证性能。
六、降级(Degradation)#
降级是故障发生时的"应急计划"——核心功能保留,非核心功能关闭。
降级的类型#
| 类型 | 做法 | 例子 |
|---|---|---|
| 功能降级 | 关闭非核心功能 | 关闭评论、推荐 |
| 数据降级 | 返回旧数据或缓存 | DB 挂了返回缓存 |
| 体验降级 | 降低精度但保可用 | 搜索返回少量结果 |
| 流量降级 | 拒绝部分请求 | 拒绝新用户注册,保老用户 |
降级的触发#
降级可以是:
- 手动触发:运维人员判断后开启(响应慢但可控)
- 自动触发:基于指标自动开启(响应快但可能误判)
- 混合模式:自动触发 + 人工确认(折中)
自动触发的指标:
- 错误率 > 5% → 降级非核心功能
- P99 延迟 > 2s → 简化响应
- CPU > 90% → 拒绝低优先级请求
- DB 连接池 > 90% → 返回缓存
降级的测试#
降级逻辑必须定期演练——不演练的降级 = 没有降级。常见问题:
- 降级开关本身挂了
- 降级后的 fallback 数据过期
- 降级逻辑有 bug(平时不跑,关键时刻出错)
- 降级后核心功能也不可用(依赖没断干净)
混沌工程(见 Ch15)是测试降级的最佳方式。
七、总结对比表#
| 模式 | 解决的问题 | 核心机制 | 触发条件 | 实现层 |
|---|---|---|---|---|
| 冗余 | 单点故障 | 多副本部署 | 常驻 | 架构层 |
| 降级 | 部分故障整体可用 | Fallback 逻辑 | 依赖不可用时 | 应用层 |
| 熔断 | 下游慢拖垮上游 | 三状态自动切换 | 错误率/延迟超阈值 | 应用/网格层 |
| 限流 | 过量请求打挂下游 | 令牌桶/漏桶 | 请求量超配额 | 网关/应用层 |
| 重试退避 | 瞬时故障导致用户可见错误 | 指数退避+Jitter | 特定错误类型 | 应用层 |
| 舱壁隔离 | 故障扩散 | 线程池/连接池/部署隔离 | 常驻 | 架构层 |
实战要点#
- 熔断+限流+重试必须组合使用,单用任何一个都有盲区。
- 重试只在幂等操作上做。扣款/下单等非幂等操作要么不重试,要么用幂等键保证安全。
- Jitter 是防重试风暴的关键。不加 Jitter 的退避基本等于定时炸弹。
- 舱壁是最容易被忽视的隔离手段——它不像熔断那样"可见"(没有告警),但缺少它时故障的传播速度是瞬时的。
- 五种模式都需要监控:熔断 OPEN 次数、限流拒绝数、重试成功率、线程池队列深度、降级触发次数——这些都是 SLO 的前置指标。
- 降级要定期演练。不演练的降级在真正需要时大概率失效。
- 限流要分层。单一层次的限流总有盲区——入口、服务间、业务层、数据库层都要有。
- 熔断 OPEN 是异常状态。频繁 OPEN 说明下游有结构性问题,需要根因排查。
小结#
分布式可靠性模式的本质是设计失败(Design for Failure)——不是让系统不出错,而是出错时影响可控。冗余防单点、降级保核心、熔断保护上游、限流保护下游、重试兜底瞬时故障、舱壁阻断故障传播。六个模式组成一张安全网,让系统在部分组件失败时仍能对外提供可接受的服务。
判断一个系统的可靠性设计是否成熟,看三件事:核心调用链有熔断+限流、非核心依赖有降级方案、故障不会跨服务扩散(舱壁隔离)。三件事都做到是成熟系统;只做到一两件是"在改进中";一件都没做,系统就是"脆"的——任何时候一个组件抖动都可能引发雪崩。
下一章讲容量规划——可靠性模式的"预防性"补充,提前准备容量避免过载。