Chapter 08
高可用与容错
第 02 到 07 章解决了系统能扩、能存对、能扛流量。但那些设计都默认每个零件正常工作。本章面对现实——机器一定会挂、依赖一定会变慢,网络一定会切断。容错不是"避免故障"(做不到),而是把局部故障关在原地,不让它顺着调用链蔓延成全站雪崩。这是设计里唯一一处目标是"优雅地坏掉"而不是"正确地运行"的领域。
本章建立的 schema
- 恢复速度由故障检测延迟决定,不是由副本数量决定;冗余只是检测之后的兜底。
- 熔断、限流、降级三件套的本质是保护调用方自己的资源(线程、连接、内存),不是修复下游。
- 重试必须三件套配齐:退避(backoff)+ 抖动(jitter)+ 幂等令牌——少任意一个都会把"减轻过载"变成"加剧过载"。
- 脑裂(split-brain)的正确防御是多数派 + fencing token;带 TTL 的分布式锁本身不安全。
容错领域有一条贯穿全章的反直觉主线:很多在单机时代用来"提高可靠性"的本能动作,放到分布式系统里会反过来制造故障。遇错就重试、超时就拉长、依赖慢了就多开线程顶上——这些在单机里无害的反应,在多机协作下会把一次局部抖动放大成全局崩溃。本章逐个机制拆开它们为什么会失控、正确的版本长什么样、代价是什么。
把第 01 章那把尺子拿过来回扣一下:无状态层的容错相对简单——副本可互换,挂一个换一个即可。真正棘手的容错全集中在有状态层:谁是主、数据是否还在多数派手里、分区两侧会不会都自认为是主。所以本章后半段(脑裂 + fencing)才是有状态系统最硬的容错问题,它直接接上第 04 章留下的"脑裂的完整防御在 08 章"那个伏笔。
8.1冗余与故障检测:恢复速度由检测决定
跑多个副本只是必要条件;系统多快恢复,取决于多快确信某个副本已经死了——而"确信"本身在分布式里是个无解的难题。
高可用的第一直觉是冗余:跑 N 个副本,挂一个还有 N-1 个。这没错,但它只回答了"故障发生后还有没有备件",没回答更关键的问题——系统多久才知道要切换。一个典型的故障转移(failover)链路是:故障检测器持续探活(heartbeat / health check)→ 判定某副本已死 → 控制器把流量切到健康副本。这条链路里,副本数量决定切到哪,但检测延迟决定多久才切。一个有 5 个副本、却要 30 秒才判定主节点死亡的系统,恢复时间是 30 秒;一个只有 2 个副本、200 毫秒就检测到的系统,恢复时间是 200 毫秒。冗余多寡在这里几乎不影响恢复速度。
分布式系统里无法区分"节点死了"和"节点慢/网络慢"——两种情况下,你都只是收不到心跳。这是第 01 章八谬误里"网络可靠""延迟为零"的直接后果。把超时阈值调短,慢节点会被误判为死(false positive),触发不必要的切换;调长,真死的节点迟迟不被发现,恢复变慢。检测就是在"误判"和"迟判"之间权衡,没有两全。
固定超时的两难,与 phi 累积检测器
最朴素的检测是固定超时:超过 T 毫秒没收到心跳就判死。问题是 T 是个写死的常数,而真实网络的延迟会波动——午高峰 GC、跨机房抖动都会让心跳偶尔迟到。固定 T 要么为了不误判而设得很大(恢复慢),要么为了快恢复而设得很小(频繁误判)。
phi 累积故障检测器(phi accrual failure detector)换了个思路:不输出"死/活"这个布尔值,而是输出一个连续的怀疑度。它持续记录心跳到达的间隔,拟合成一个正态分布;当前这次心跳"迟到"了多久,就换算成一个概率 P(在历史分布下,间隔超过当前等待时长的概率),再取 phi = -log10(P)。phi 越大代表越可疑。调用方自己定阈值(如 phi > 8 才判死)。它的好处是自适应:网络整体变慢时,历史分布的方差变大,同样的迟到换算出的 phi 反而变小——阈值自动放宽,误判减少。Cassandra 和 Akka 用的就是这套。
- 代价:要维护每个节点的心跳间隔历史窗口(额外内存与计算),且冷启动时样本不足、估计不准。
- 何时失效:当延迟分布根本不是正态(如双峰:要么 1ms 要么 1s),正态拟合给出的概率会偏,phi 失真。
关键陷阱 · 检测错误会直接造成脑裂
故障检测的误判不是"白切一次流量"这么轻——在有状态系统里,一个错误的"主已死"判定,会让备节点也升为主,于是两个主同时收写,这就是脑裂。所以检测器的阈值不是性能调参,而是正确性参数:它和 8.6 节的 fencing 必须配套设计,光靠"检测得准"挡不住脑裂,因为再准的检测器在网络分区下也会判错。
一个主从架构,主节点没死,只是发生了一次 5 秒的 Full GC(stop-the-world),期间停发心跳。固定超时设为 2 秒。这 5 秒里系统会发生什么?
展开答案
检测器在 2 秒后判定主"已死",触发故障转移,备升主。但原主 GC 结束后"活过来",仍以为自己是主——现在有两个主。如果客户端还连着旧主写入,就出现双写与数据分叉。这正是 8.6 节要解决的:单纯靠"检测 + 升主"必然在 GC/暂停场景下脑裂,必须用 fencing token 让存储端拒绝旧主的写。GC 暂停超过超时阈值是生产中脑裂最常见的触发源之一。
8.2熔断器:保护的是调用方,不是下游
熔断器(circuit breaker)真正的价值不是"检测到下游故障",而是在下游变慢时立刻停止向它发请求,把调用方自己被占用的线程和连接腾出来。
最危险的下游故障不是"死了",而是"慢了但没死"。一个死掉的依赖会立刻拒绝连接,调用方很快得到错误、释放线程。但一个慢依赖会让每个调用方线程都挂在那里等响应——等到超时为止。在高 QPS 下,线程池会被这些"挂着等"的请求迅速占满,于是调用方自己也无法服务任何请求,哪怕它本身完全健康。慢依赖通过耗尽调用方资源,把故障传染给了上游。这就是级联失败的核心传播路径。
熔断器是一个三态状态机,挡在调用方和下游之间:
三态的运行逻辑:
- CLOSED(闭合):正常放行所有请求,同时在滑动窗口里统计错误率/慢调用率。一旦超过阈值,跳到 OPEN。
- OPEN(打开):直接本地快速失败,请求根本不发给下游。这一步腾出了调用方的线程与连接——这是熔断的全部意义所在。停留一个冷却期。
- HALF-OPEN(半开):冷却到期后,放少量探针请求试探下游是否恢复。探针成功→回 CLOSED;探针失败→退回 OPEN 再冷却。
深一层 · 熔断不是"检测故障"
很多人把熔断器理解成"自动发现下游挂了"。检测只是手段。它真正交付的是资源保护:让一个慢(而非死)的依赖,不至于把上游线程池占满、拖垮整个进程。这个区别决定了配置方向——熔断阈值要盯慢调用比例和并发占用,而不只是错误码,因为"慢"才是最毒的,而慢调用根本不返回错误码。
代价与失效场景:熔断打开期间,所有请求快速失败,这对下游是"放弃"而非"挽救"——必须配降级(fallback:返回缓存值、默认值或友好错误)才不会把熔断变成对用户的全量报错。阈值设太敏感会在下游正常抖动时误熔断(可用性反降);窗口设太长则恢复迟钝。熔断器是进程内的,它保护不了"下游已经被打垮"这件事本身——那需要下游侧的限流(8.3)。
8.3限流:四种算法与它们各自漏掉的东西
限流(rate limiting)是在被保护资源的入口设闸:超过承载能力的请求宁可早早拒绝(返回 429/503),也不让它进来把系统拖垮。早拒绝是止血,晚崩溃是失血。
限流和熔断分工不同:熔断是调用方保护自己不被下游拖垮,限流是被调用方保护自己不被调用方打爆。四种主流算法,各有各的取舍:
| 算法 | 行为 | 容许突发? | 代价 / 漏掉的东西 |
|---|---|---|---|
| 令牌桶 | 按速率 R 补令牌、桶容量 B;有令牌才放行 | 是(最多 B 的突发) | 需要兜得住 B 大小的瞬时峰值;最常用,因为真实流量本就有突发 |
| 漏桶 | 请求入队、固定速率 R 流出 | 否(强制平滑) | 突发流量被排队推迟甚至丢弃;队列本身要有界,否则又变成无界队列 |
| 滑动窗口日志 | 记录每个请求的精确时间戳 | —(精确) | 内存 O(请求数),高 QPS 下昂贵 |
| 滑动窗口计数 | 按重叠比例加权前一个固定窗口的计数 | —(近似平滑) | 是近似值(略有误差),但廉价,且修掉了固定窗口的 2× 边界漏洞 |
选型逻辑:真实流量天然有突发,所以多数 API 网关默认令牌桶——它在限定平均速率的同时容许短促尖峰。需要严格平滑输出(如对一个脆弱的下游匀速喂请求)才用漏桶。需要精确到每个请求时间但 QPS 不高,用滑动窗口日志;高 QPS 想又快又基本准,用滑动窗口计数。
8.4重试风暴:退避、抖动与幂等,缺一不可
重试在偶发抖动下能救命,在系统性过载下会致命——区别在于重试是否配齐了三件套:退避拉开间隔、抖动打散同步、幂等令牌保证重复安全。
重试是最容易被滥用的容错手段,因为它在单机和低负载下几乎总是"对"的。它在两个场景里会变成纵火犯:
重试风暴:跨层放大
当下游因过载而开始失败,每一层都重试,重试次数会沿调用链相乘放大。这是退避之外更隐蔽的危险:
正确的重试 = 退避 + 抖动 + 幂等
三件套缺一不可,每一件堵一个独立的失败模式:
- 指数退避(exponential backoff):每次重试间隔翻倍(如 1s、2s、4s),给下游喘息时间,避免失败后立刻贴脸重发。但光有退避不够——它解决"太快",没解决"太齐"。
- 抖动(jitter,随机化):纯指数退避有个隐患——大量客户端在同一时刻一起失败,会算出同一个退避时间,于是又在同一时刻一起重试(thundering herd,惊群)。full jitter 把退避改成在
[0, cap]区间均匀随机取值,把这群同步的重试摊开到时间轴上。这里随机性让系统更健壮,是反直觉的——在重试调度上,确定性才是缺陷。 - 幂等令牌(idempotency token):重试意味着同一个操作可能执行多次。只有当操作带幂等键、服务端能识别"这是重复请求"并返回首次结果时,重试才是安全的。否则重试一笔"扣款"就是重复扣款。
关键陷阱 · 重试预算与重试令牌桶
三件套之外还需要全局闸门:客户端侧给重试单独配一个令牌桶(retry budget),规定"重试流量不超过正常流量的 X%"。这样即使下游大面积失败,重试总量也被钉在一个上限内,不会无限放大。AWS 的实践是把重试当成一种需要预算的稀缺资源,而不是"失败就无脑重发"。重试令牌桶呼应了 8.3 的限流——本质上是给重试这一类流量单独限流。
一个下游服务因为容量不足开始返回 503。所有上游客户端都配了"失败立即重试 2 次"(没有退避、没有抖动)。下游会陷入什么状态?加了重试反而让可用性更低,为什么?
展开答案
瞬间,下游收到的请求量变成原来的 3 倍(原始 1 次 + 重试 2 次),而它本就因容量不足才返回 503——重试把它彻底压垮,进入 brownout(部分功能瘫痪)。这就是"加重试反而降低可用性":在容量性故障(而非偶发抖动)下,重试增加的是负载而不是成功率。正确做法是退避拉开间隔、抖动打散同步、重试预算封顶总量,并在下游侧用限流早早拒绝(429/503 + 重试头),让客户端知道不该再来。
8.5超时预算与舱壁:给故障划定边界
超时预算(deadline)让请求永远不会无限期挂住线程;舱壁(bulkhead)把不同依赖的资源隔开,让一个依赖被打满时只饿死自己那一格,不拖垮整条船。
超时预算沿调用链传播
单点超时(每个调用各设一个固定 timeout)有个问题:它们不协调。一个用户请求穿过 5 个服务,如果每层各设 3 秒超时,最坏情况用户要等 15 秒——而用户可能 2 秒就已经放弃刷新了,后面这 13 秒的工作全是浪费,还白白占着每一层的线程。
超时预算(deadline propagation)把"剩余可用时间"作为一个值随请求往下传。入口设定总预算(如 2 秒),每经过一跳就减去已耗时间,把剩余预算传给下一跳。某一跳发现剩余预算已不够完成自己的工作,就立刻放弃、不再往下发——因为即便做完,上游也早已超时丢弃了结果。这保证了:没有任何一个请求会占用线程超过它的全局预算,慢请求被及时砍掉,线程被回收。gRPC 的 deadline、很多 RPC 框架的 timeout 透传就是这个机制。
舱壁:隔离资源,限制故障爆炸半径
舱壁这个词来自船体设计——船舱被隔板分成多个互不连通的水密舱,一个破洞进水只淹一个舱,船不沉。在系统里,舱壁指给每个下游依赖分配独立的资源池(独立线程池或独立连接池),而不是所有依赖共用一个大池子。
舱壁有两种实现:
- 线程池隔离:每个依赖一个独立线程池,调用在该池的线程上执行。隔离最彻底(连慢调用都被关在自己的线程里),但线程切换有开销,线程数多时成本高。这是 Hystrix 的经典模型。
- 信号量隔离:用一个计数器限制某依赖的并发数,调用在调用方线程上直接执行。开销小,但因为不另起线程,挡不住调用方线程本身被慢调用挂住——它只限并发数,不做线程隔离。适合本身极快的本地调用。
超时、熔断、舱壁、降级是配套使用的一套止血组合:超时保证单个请求不会无限挂住,熔断保证大面积失败时不再徒劳发请求,舱壁保证一个依赖的故障不外溢,降级保证用户拿到一个兜底响应而非报错。第 07 章的缓存击穿和雪崩在这里接上——缓存层崩溃造成回源洪峰时,正是靠这套组合(熔断回源 + 限流 + 降级返回旧值)止血。
8.6脑裂与 fencing token:带 TTL 的锁本身不安全
网络分区会让两个节点都自认是主、都接受写,造成数据损坏(脑裂)。正确防御是多数派选主 + fencing token;任何基于"锁有 TTL 就安全"的方案都会在进程暂停时破功。
这是本章最硬、也是有状态系统最致命的容错问题,它直接接续第 04 章。脑裂(split-brain)的成因:网络分区把集群切成两半,每半都收不到对方心跳,于是两半各自选出/认定一个主,两个主都开始接受写入。等分区恢复,两份分叉的数据无法合并,数据已损坏。8.1 节那个 GC 暂停的例子是脑裂的另一条触发路径——主没死只是停顿,备却已升主。
第一层防御:多数派才能当主
第一道闸是多数派(quorum):规定只有拿到 ⌊N/2⌋+1 票的节点才能成为主。这直接调用了第 05 章的多数派 overlap——任意两个多数派必然相交,所以不可能同时存在两个都被多数派认可的主。被分区切到少数派一侧的节点凑不齐多数票,只能停摆(拒绝服务),而不是自立为主。这把"两个主都活跃"挡掉了大半。
第二层防御:fencing token,因为多数派也不够
但多数派挡不住一种情况:一个曾经合法的主,在被取代后仍以为自己是主(GC 暂停、长 STW、VM 被挂起后恢复)。它手里可能还攥着一个"看起来有效"的锁或租约,于是带着陈旧身份去写存储。这时需要第二层——fencing token(栅栏令牌)。
机制:每次有节点获得 leadership/锁,协调者发给它一个单调递增的令牌号(1, 2, 3…)。节点每次写存储都必须带上自己的令牌号。存储端记住它见过的最大令牌号,主动拒绝任何令牌号更小的写。这样,旧主即使复活、即使还攥着旧锁,它的令牌号(比如 33)已经小于新主的令牌号(34),存储端会直接拒绝它的写——栅栏把陈旧的持锁者挡在数据之外。
关键陷阱 · 带 TTL 的分布式锁不是安全的互斥
常见的错误认知是:"给分布式锁加个 TTL,到期自动释放,就不会有两个客户端同时持锁了。"这是错的。客户端 A 拿到锁、TTL 设 10 秒,然后发生一次 15 秒的 GC 暂停——在第 10 秒锁自动过期,客户端 B 拿到了同一把锁。现在 A 的 GC 结束、它不知道自己的锁早没了,照样去写存储,于是 A 和 B 同时在写。TTL 解决的是"持锁者永久消失后锁怎么释放",它解决不了"持锁者暂停又复活"。唯一正确的修复是 fencing token:让存储端校验单调令牌号,把陈旧的 A 挡在外面。这就是为什么 Redis 单实例锁、ZooKeeper 临时节点都不能单独保证互斥的写安全,必须配存储端的 fencing 校验。
深一层 · 为什么 fencing 不能靠时间戳
有人想用时间戳代替令牌号:写请求带本地时间戳,存储端取最大。这在时钟漂移下不安全——第 01 章八谬误里"拓扑不变""只有一个时钟"的反面。两台机器的物理时钟会偏差,一个时钟快的旧主可能盖过新主的时间戳。fencing token 必须是由单一协调者发放的单调递增整数(逻辑序号),不依赖任何物理时钟,才能保证全局唯一且严格递增。第 09 章的分布式 SQL 用 TrueTime/HLC 处理跨节点时序,正是因为裸物理时钟不可信。
8.7混沌工程:在受控条件下逼出潜伏的故障
混沌工程(chaos engineering)不是"随机搞破坏",而是一套科学方法:先定义可测的稳态假设,再从最小爆炸半径注入故障,验证假设是否仍成立——把故障切换和降级逻辑里的潜伏 bug,在白天受控地暴露出来,而不是在凌晨三点被它们叫醒。
前面六节讲的所有机制——熔断、退避、舱壁、fencing——都有一个共同的残酷事实:它们只在真正发生故障时才会被执行,而那恰恰是最不该出错的时刻。一段从没被触发过的故障转移代码,等同于一段没测过的代码;它很可能在第一次真实故障时才暴露 bug,而那时整个系统正在燃烧。混沌工程的存在就是为了反转这个时序:主动、可控地制造故障,让这些路径在可观测、可回滚的条件下先跑一遍。
它的方法论有四步,关键在于不是乱来:
- 定义稳态假设:用可测量的指标描述"系统正常"是什么样(如"下单成功率 ≥ 99.5%、p99 延迟 < 800ms")。没有可测稳态,注入故障后无从判断有没有出问题。
- 最小爆炸半径起步:从影响最小的范围开始(单个实例、单个可用区的一小部分流量),而不是上来就拔整个机房。爆炸半径可控是混沌工程区别于"作死"的根本。
- 注入真实故障:杀进程、注入延迟、丢包、模拟依赖超时——复现生产里真会发生的故障类型。
- 验证假设是否仍成立:注入后,稳态指标是否还在阈值内?如果熔断/降级生效,用户应几乎无感;如果指标崩了,就找到了一个潜伏 bug——这正是实验的目的。
深一层 · 混沌工程是对前六节机制的"集成测试"
前六节的每个机制都是单独配置的,但故障时它们必须协同工作:熔断打开后降级是否返回了合理兜底?舱壁是否真的把故障关住了?fencing 在主切换时是否真的拒绝了旧主?这些跨机制的交互,单元测试覆盖不到,只有在真实注入故障时才看得出来。混沌工程因此是容错设计的验收环节,不是可选项——没跑过混沌实验的高可用方案,准确说只是"声称高可用"。
8.8失败模式清单与多层防御
本章的机制各自针对特定失败模式。把它们汇成一张对照表,便于在评审里快速定位"这个故障该用哪些防御组合"。多数严重故障需要多层叠加,单一机制挡不住:
| 失败模式 | 触发条件 | 防御组合 |
|---|---|---|
| 重试风暴 / 惊群 | 大面积失败后所有客户端同步重试 | full jitter + 重试预算(重试令牌桶)+ 下游限流早拒 |
| 级联失败 | 慢依赖耗尽上游线程池,逐层传染 | 超时预算 + 熔断 + 舱壁 + 降级(四件套) |
| 脑裂 | 网络分区,两侧各自认主 | 多数派选主 + fencing token(存储端校验) |
| 陈旧持锁者 | GC / VM 暂停超过锁 TTL 后复活 | 单调 fencing token(不能只靠 TTL) |
| 热 shard | 单个热 key / 明星用户集中在一个分区(回扣 03 章) | key 加盐 / 拆分 + 前置缓存(分区与复制都救不了热 key) |
| 缓存雪崩 / 击穿 | 大量 key 同时过期或缓存节点挂(回扣 07 章) | TTL 加随机抖动 + singleflight + 回源熔断 + 缓存高可用 |
| 时钟漂移 | 多机物理时钟偏差(回扣 01 章八谬误) | 用逻辑时钟 / 单调序号排序,fencing 不靠时间戳 |
| 无界队列 | 消费跟不上、队列无限增长直至 OOM(回扣 07 章背压) | 有界队列 + 背压 + 早拒(429 / 503) |
这张表里一半的失败模式都来自前面章节的"状态切分/复制"决策:热 shard 是分区(03)的下游,缓存雪崩是缓存(07)的下游,脑裂是复制(04)的下游。容错不是一个独立模块,而是贯穿全程的横切层——切分和复制每引入一种状态分布方式,就引入一组对应的失败模式,本章提供的是拦截这些失败模式不让它们蔓延的工具箱。
自测
先合上教程写答案,再展开对照。能用自己的话说清"为什么",比记住名词重要。
- 熔断器真正保护的是什么?为什么说它的核心价值是资源保护而不是故障检测?一个"慢但没死"的依赖比一个"已死"的依赖更危险,为什么?
- 为什么带 TTL 的分布式锁本身不能保证互斥写安全?fencing token 凭什么能?为什么 fencing 必须用单调递增整数而不能用时间戳?
- 为什么纯指数退避还要再加 jitter(抖动)?在重试调度这件事上,为什么说"确定性反而是 bug"?
- 恢复时间主要由副本数量决定,还是由故障检测延迟决定?为什么一个错误的"已死"判定在有状态系统里特别危险?
展开参考答案
1. 熔断保护的是调用方自己的资源(线程、连接、内存)。慢依赖会让每个调用线程挂着等到超时,高 QPS 下迅速占满线程池,使健康的调用方也无法服务——这是级联失败的传播路径。死依赖会立刻拒连、快速释放线程,反而没那么毒。熔断在 OPEN 态直接本地快速失败、请求不发出,把线程腾出来,这就是它的全部意义。检测只是手段。
2. TTL 解决的是"持锁者永久消失后锁怎么自动释放",解决不了"持锁者暂停又复活"。客户端 A 持锁、TTL 10s、发生 15s GC,第 10s 锁过期、B 拿锁,A 复活后不知锁已失效仍去写,于是 A、B 同时写。fencing token:每次授权发一个单调递增令牌,写时携带,存储端记住已见最大值、拒绝更小的令牌,旧主令牌偏小被挡在数据外。必须用单调整数而非时间戳,因为多机物理时钟会漂移,时钟快的旧主可能盖过新主——逻辑序号由单一协调者发放,全局唯一严格递增,不依赖物理时钟。
3. 纯指数退避只解决"重试太快",没解决"重试太齐":大量客户端在同一时刻失败会算出同一个退避时间,于是又在同一时刻一起重试(惊群),再次打垮系统。full jitter 在 [0,cap] 随机取延迟,把这群同步重试摊到时间轴上。在重试调度上,所有客户端算出同一个时间正是 bug 之源,所以随机性(抖动)让系统更健壮——确定性在这里是缺陷。
4. 由故障检测延迟决定。冗余只决定"故障后还有没有备件、切到哪",而"多久才知道要切"取决于检测延迟——一个 5 副本但 30s 才判死的系统恢复时间是 30s。错误的"已死"判定在有状态系统里特别危险,因为它会触发备升主,造成两个主同时收写(脑裂),数据分叉损坏;而无状态层误判最多白切一次流量。所以检测阈值是正确性参数,且必须和 fencing 配套。
一个慢下游引发的全站雪崩,给出从单个请求到全局的多层防御
场景:服务 A 同步调用下游服务 B。某天 B 没有崩溃,只是因为一次慢查询,响应时间从 50ms 涨到 8s。很快 A 的整个线程池被"挂着等 B"的请求占满,A 自己也对外不可用了——而 A 还服务着另外两个和 B 完全无关的接口,它们也一起瘫痪了。请设计一套防御,从单个请求级别一直到全局级别,逐层说明每层挡住了什么、代价是什么。
展开思路(先自己写完整方案再看)
请求级:给调用 B 设超时预算(如 300ms),并让 deadline 沿调用链传播——单个请求不会再挂 8s,到点就放弃。代价:B 偶发慢时这些请求会失败(需配降级)。
依赖级:给 B 配独立舱壁(独立线程池/信号量),即使打满也只饿死这一格,A 调用其他下游的两个无关接口照常服务。代价:资源利用率下降,每格要预留余量。
聚合级:给 B 加熔断器。当慢调用比例超阈,熔断跳 OPEN,A 不再向 B 发请求、直接快速失败并降级(返回缓存值/默认值/友好错误),把线程彻底腾出。HALF-OPEN 探针确认 B 恢复后再放行。代价:熔断期间 B 相关功能对用户是降级态。
下游级:B 侧加限流(令牌桶),超过容量的请求早早返回 429/503 + 重试头,避免 A 的重试把 B 彻底压垮。A 侧重试必须配 full jitter + 重试预算,否则重试反而加剧 B 过载。
验证级:用混沌工程主动给 B 注入延迟,在受控条件下确认上面四层真的协同生效——熔断是否打开、舱壁是否隔住、降级是否返回了合理兜底、A 的无关接口是否始终可用。没验证过的防御等于没有防御。