从限流到熔断前两篇都在管进来的流量第 1 篇定了分层策略和记账维度第 2 篇把四种算法跑在同一条流量上。但限流器有个共同盲区——它假设被放行的请求都能正常完成。现实里更凶的事故形态是依赖烂掉地图 ETA 服务 RT 从 80 毫秒涨到 8 秒派单线程全部挂在等待上连接池、线程池相继耗尽一个下游的慢把整条网约车链路拖进 ICU。这时候需要的不是少放点流量进来而是对坏依赖直接快速失败——熔断器。本篇用网约车派单链路的例子把 CLOSED/OPEN/HALF_OPEN 状态机逐帧回放再做一轮参数扫描看每组旋钮各自保护了谁、误伤了谁。状态机与两本账熔断器的本质是一个按依赖对象维护的状态机加两本账。三态CLOSED 正常放行并记账OPEN 拒绝一切真实调用、当场返回HALF_OPEN 放少量探测请求试探依赖是否恢复。两本账分别是触发账和恢复账触发账决定 CLOSED→OPEN——常见口径有失败率最近 N 次或最近 T 秒内失败占比、连续失败次数、慢调用比例RT 超阈值的占比对付没报错但越来越慢这种更阴的故障恢复账决定 HALF_OPEN→CLOSED——要求连续 k 次探测成功且探测期间必须限并发否则半开瞬间涌入的恢复流量会把刚喘过气的依赖再踩死。这里有个常被混淆的分工限流管总量熔断管质量。限流器看到的失败不改变它的放行决策熔断器只关心我调的东西还活着吗。两者正交生产上一定同时存在而且熔断的 fast-fail 发生在上游省下的恰恰是第 1 篇里说的陪跑成本。实验一逐帧回放一次故障生命周期写一个次数滑窗 失败率阈值的熔断器喂一个确定性故障剧本虚拟时钟每秒一次调用t20~44 依赖硬故障t45 起恢复OPEN 持续 5 秒后转半开半开需连续 2 次探测成功才闭合fromcollectionsimportdeque# 网约车派单链路依赖地图 ETA 服务; 熔断器按次数滑窗 失败率决策# 虚拟时钟每秒一次调用; 故障剧本: [20,45) 硬故障, t60/63/66 三次偶发毛刺classBreaker:def__init__(self,win20,min_calls10,rate0.5,open_s5,probes3,ok_need2):self.win,self.min_calls,self.ratewin,min_calls,rate self.open_s,self.probes,self.ok_needopen_s,probes,ok_need self.resultsdeque(maxlenwin)# CLOSED 期的最近 N 次结果self.stateCLOSEDself.opened_atNoneself.probe_ok0deffast_fail(self,now):returnself.stateOPENandnowself.opened_atself.open_sdefrecord(self,now,ok):ifself.stateOPENandnowself.opened_atself.open_s:self.state,self.probe_okHALF_OPEN,0print( t%3d OPEN - HALF_OPEN (开始探测)%now)ifself.stateHALF_OPEN:ifok:self.probe_ok1ifself.probe_okself.ok_need:self.stateCLOSEDself.results.clear()print( t%3d HALF_OPEN - CLOSED (探测成功, 恢复放行)%now)else:self.state,self.opened_atOPEN,nowprint( t%3d HALF_OPEN - OPEN (探测失败, 继续快速失败)%now)returnifself.stateCLOSED:self.results.append(1ifokelse0)nlen(self.results)ifnself.min_callsand(n-sum(self.results))/nself.rate:self.state,self.opened_atOPEN,nowprint( t%3d CLOSED - OPEN (窗口失败率 %d/%d %.0f%%)%(now,n-sum(self.results),n,self.rate*100))defcall(self,now,service_ok_fn):ifself.fast_fail(now):returnFAST_FAILokservice_ok_fn(now)self.record(now,ok)returnOKifokelseFAILdefservice_ok(now):# 故障剧本, 完全确定性if20now45:returnFalsereturnnownotin(60,63,66)brBreaker()stat{OK:0,FAIL:0,FAST_FAIL:0}print( 熔断状态机回放 (每秒 1 次调用, 共 90 秒) )fortinrange(90):stat[br.call(t,service_ok)]1print(放行且成功 %d | 真实失败 %d | 熔断器快速失败 %d%(stat[OK],stat[FAIL],stat[FAST_FAIL]))print(快速失败占比 %.0f%%: 这些请求没有一秒的超时等待, 全部当场返回%(100.0*stat[FAST_FAIL]/90))运行输出 熔断状态机回放 (每秒 1 次调用, 共 90 秒) t 29 CLOSED - OPEN (窗口失败率 10/20 50%) t 34 OPEN - HALF_OPEN (开始探测) t 34 HALF_OPEN - OPEN (探测失败, 继续快速失败) t 39 OPEN - HALF_OPEN (开始探测) t 39 HALF_OPEN - OPEN (探测失败, 继续快速失败) t 44 OPEN - HALF_OPEN (开始探测) t 44 HALF_OPEN - OPEN (探测失败, 继续快速失败) t 49 OPEN - HALF_OPEN (开始探测) t 50 HALF_OPEN - CLOSED (探测成功, 恢复放行) 放行且成功 58 | 真实失败 16 | 熔断器快速失败 16 快速失败占比 18%: 这些请求没有一秒的超时等待, 全部当场返回回放里能看到三个关键机制。其一触发账是滑动的故障从 t20 开始但直到 t29 窗口内失败 10/20 才熔断——前 9 秒系统确实在明知不太行地继续调用这是失败率口径相对连续失败口径必须付的检出延迟。其二半开探测是脉冲式的每次 OPEN 满 5 秒放一次探测失败即重新开闸并重新计时opened_at 被探测失败刷新所以 t34/39/44 形成整齐的重试脉冲故障期内总共只多打了 3 次真实请求换来 16 秒的完全保护。其三t49 那次探测成功但状态还没闭合t50 凑满ok_need2才 CLOSED——单点成功不算恢复这条规则防的是依赖回光返照式的间歇性可用。另外注意 t60/63/66 的三次毛刺没有触发熔断窗口失败率 3/2015% 远低于阈值健康系统的抖动本来就不该让闸刀落下。实验二参数扫描——没有免费的保护把同一个剧本分别喂给灵敏/中庸/保守三组参数统计四件事硬故障检出延迟、误熔断次数短暂抖动被当成故障、抖动期误伤秒数被 fast-fail 的其实没坏的请求、恢复时刻fromcollectionsimportdeque# 同一故障剧本喂三组参数: 硬故障 [20,45), 短暂抖动 [60,62] (3 次失败后自愈)# 评估四个指标: 硬故障检出延迟 / 抖动期误熔断次数 / 误伤期快速失败秒数 / 恢复时刻defservice_ok(now):if20now45:returnFalsereturnnot(60now62)defrun(name,win,min_calls,rate,open_s,ok_need,span80):results,state,opened_at,probe_okdeque(maxlenwin),CLOSED,None,0trips,detect_at,recover_at,ff_blip0,None,None,0fornowinrange(span):ifstateOPEN:ifnowopened_atopen_s:if60now66:ff_blip1# 抖动期的快速失败 误伤continuestateHALF_OPENokservice_ok(now)ifstateHALF_OPEN:ifok:probe_ok1ifprobe_okok_need:state,results,probe_okCLOSED,deque(maxlenwin),0ifrecover_atisNone:recover_atnowelse:state,opened_atOPEN,nowcontinueresults.append(1ifokelse0)nlen(results)ifnmin_callsand(n-sum(results))/nrate:state,opened_at,tripsOPEN,now,trips1ifdetect_atisNone:detect_atnow-20print(%-8s 误熔断%d次 | 硬故障检出延迟%s秒 | 恢复于t%s | 抖动期误伤%d秒%(name,trips-(1ifdetect_atisnotNoneelse0)iftripselse0,detect_at,recover_at,ff_blip))print(剧本: 硬故障 t20~44, 短暂抖动 t60~62 (共 3 次失败))run(灵敏,win5,min_calls3,rate0.4,open_s5,ok_need1)run(中庸,win10,min_calls5,rate0.5,open_s5,ok_need2)run(保守,win20,min_calls10,rate0.5,open_s5,ok_need2)运行输出剧本: 硬故障 t20~44, 短暂抖动 t60~62 (共 3 次失败) 灵敏 误熔断1次 | 硬故障检出延迟1秒 | 恢复于t46 | 抖动期误伤4秒 中庸 误熔断0次 | 硬故障检出延迟4秒 | 恢复于t50 | 抖动期误伤0秒 保守 误熔断0次 | 硬故障检出延迟9秒 | 恢复于t50 | 抖动期误伤0秒检出延迟和误伤之间是硬交换灵敏组win5意味着两次失败就能凑够 40% 失败率一次普通网络抖动就够触发保守组窗口 20 次、最少 10 次才参与判定抖动完全免疫但真故障要白挨 9 秒。工程上没有最优参数只有按依赖的故障画像选交换比对秒级雪崩敏感的强依赖支付、库存宁可灵敏让误伤换取生存对短暂抖动普遍的弱依赖营销标签宁可保守。另外两个实践口径min_calls在低峰期可能永远凑不满每小时 100 请求的接口按 20 次滑窗要 12 分钟才能判定所以时间窗和次数窗通常组合用OPEN 时长不宜小于依赖的已知恢复手段耗时如连接池自重举要 10 秒你就设 15 秒否则探测脉冲会反过来给依赖添乱。落地清单每个依赖一个独立熔断器实例绝不共享状态——地图挂了不该让短信服务跟着开闸度量至少两本账错误失败率 慢调用比例只配错误率防不住不报错的慢HALF_OPEN 必须限并发示例里是 1 秒 1 探测真实网关要允许少量并行探测但设上限熔断事件进告警和大盘熔断次数、平均 OPEN 时长、fast-fail 计数熔而不觉是最危险的fast-fail 的返回要有语义让调用方知道这是保护性拒绝可走降级数据下一篇就讲降级怎么接住它熔断解决了坏依赖别再拖死我但快速失败之后用户总得看到点什么订单页不能白屏派单还得给出兜底距离。什么时候可以拒绝、什么时候必须给出替代答案、替代答案由谁准备——下一篇《高并发流量治理实战4降级的艺术核心链路与非核心链路的取舍清单》把失败之后的部分补齐。参考来源Martin Fowler: CircuitBreaker (Bliki): https://martinfowler.com/bliki/CircuitBreaker.htmlWikipedia: Circuit breaker design pattern: https://en.wikipedia.org/wiki/Circuit_breaker_design_patternResilience4j 文档: Circuit Breaker: https://resilience4j.readme.io/docs/circuitbreakerNetflix Hystrix Wiki (GitHub 归档仓库): https://github.com/Netflix/Hystrix/wikiEnvoy 文档: Circuit breaking and outlier detection: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/upstream/circuit_breaking本系列已结集为免费专栏高并发流量治理实战从限流到全链路压测进阶推荐付费专栏提示词工程实战从入门到生产级 Prompt 设计限时 ¥19.9首篇免费试读