超时、重试、熔断、限流与降级:把故障限制在局部
一次下游变慢会沿着调用树放大成整站故障,本页解释四道防线各管什么:截止时间决定"还做不做",熔断决定"该不该发起",限流与隔离决定"同时允许多少",降级与恢复决定"坏了之后怎么体面退出"。Dubbo源码固定 dubbo-3.2.15;Dubbo代理、目录与重试的调用链在Dubbo调用链与服务治理,DNS与TCP细节在网络主题。
deadline是端到端预算:逐层扣除与提前拒绝
一次调用消耗DNS、连接池等待、TCP/TLS建立、发送、provider排队、执行、返回与解码时间。connect timeout只管连接阶段;read timeout限制单次等待,不等于整个请求预算。若入口800ms,每层重新得到800ms,三层串行调用就可能远超用户期限。应传剩余预算或可校准的deadline,每次排队、尝试、退避后重新计算。
在单进程内用单调时钟计算耗时,不用会跳变的墙钟作精确持续时间。跨机器直接传绝对时间戳要考虑时钟偏差;可传剩余预算并在接收时扣除本地耗时,预留网络误差。请求到provider时若预算已不足,应在有副作用前拒绝,不能排队到几秒后执行一个调用者已放弃的写请求。
例:总预算800ms,入口解析与鉴权60ms,关键数据库预算250ms,RPC每次最多150ms,返回预留40ms,剩余300ms。允许一次额外RPC尝试前还要扣第一次耗时和退避;若第一次耗时150、退避80,剩余170,可以尝试但不能再给300。重试预算应用全程实际耗时更新,不把每个上限都当必然相加,也不把未用完预算随意重置。
Dubbo发送前计算timeout,结果不超过0时以TIMEOUT_TERMINATE返回,不再发起调用(原码及倒计时条件见Dubbo调用链)。这不保证剩余时间足够完成一次尝试;下文完整尝试预算是独立模型,而非该框架检查的行为。
超时在本地结束等待,不回滚对端事务
FailoverClusterInvoker重试与协议层超时各自独立工作:集群层决定换不换provider,协议层决定本地等多久。DubboInvoker把请求交给DefaultFuture并传入timeout;到期后由定时任务完成本地future,而不是让网络层挂着。
原码:到期检查完成本地future
原码连续摘录(仅统一公共缩进,截取方法局部):DefaultFuture.java,dubbo-3.2.15,L333–L341。
private void notifyTimeout(DefaultFuture future) {
// create exception response.
Response timeoutResponse = new Response(future.getId());
// set timeout status.
timeoutResponse.setStatus(future.isSent() ? Response.SERVER_TIMEOUT : Response.CLIENT_TIMEOUT);
timeoutResponse.setErrorMessage(future.getTimeoutMessage(true));
// handle response.
DefaultFuture.received(future.getChannel(), timeoutResponse, true);
}notifyTimeout构造Response,按是否已发送设置SERVER_TIMEOUT或CLIENT_TIMEOUT,再交DefaultFuture.received结束本地等待。这是本地超时协议,不是远端事务回滚协议;sent状态也不能证明服务端究竟处理到哪一步——响应可能已在网络中,provider事务可能已提交。上层重试必须保留业务身份、检查剩余deadline,并能查询已经发生的副作用。
取消同理要成为业务协议:带operation_id,服务端在效果提交点原子检查取消/版本;若扣款已成功,取消只能发起退款等后续动作。把TimeoutException当"服务端绝对没执行"会在自动重试时重复扣款,应保存UNKNOWN并查单。UNKNOWN恢复与外部副作用回查业务幂等与跨资源事务。
重试放大按调用树计算,预算集中一层管理
网关、应用、Dubbo各最多3次尝试,单次上游请求最坏可能产生27次底层调用,而不是9次。若再扇出4个独立分支,最坏规模还要乘4;并非所有请求都达到最坏,但容量保护必须看到同步失败时的峰值。基础失败概率p且每次独立的假设下,最多a次尝试的期望次数为1+p+...+p^(a-1);真实过载失败高度相关,不能依赖独立概率估计低风险。
重试条件应同时满足:操作幂等或可查证、错误可恢复、剩余deadline足够、次数与速率预算尚有余量。用全局重试令牌限制重试占正常流量比例,避免每个客户端各自三次在大规模故障时叠加。退避加抖动减少同步,尊重服务端的等待提示;对UNKNOWN写请求先查单,不把每种5xx都立即重做。
重试应由拥有业务语义与总预算的一层负责,其他层明确关闭或只处理不会重复副作用的连接建立阶段。读操作也不是无限安全:高成本查询、带计费统计或时序敏感读取可能产生资源与业务影响。hedged request会同时产生副本工作,更需只读/幂等与并发预算,不能当作通用降延迟开关。
熔断状态机与错误分类
Closed收集窗口内调用结果;达到最小样本与失败/慢调用阈值后Open,快速拒绝;冷却后Half-open允许有限探测,达到恢复标准才Closed,否则回Open。状态机如下,它是方法模型,不绑定未固定版本的熔断库源码:
Closed ──失败/慢调用达阈值且样本足够──▶ Open
Open ──冷却期到──▶ Half-open(仅有限探测)
Half-open ──探测达标──▶ Closed;探测失败──▶ Open必须定义窗口按调用开始还是结束计数、超时算哪类错误、本地限流拒绝是否计入,避免把所有4xx都当依赖故障。滚动窗口最小样本不足时不能用一次失败就判"100%失败";窗口太长又可能把恢复拖慢。慢调用阈值保护的是资源占用,错误率低但每次挂10秒同样可耗尽线程。半开如果瞬间放行所有客户端,就把恢复的provider再次压垮;应限制全局或分层探测并发、随机化探测时间,并在恢复后逐步升量。
熔断不是限流:前者根据依赖健康决定是否发起,后者根据预算与公平分配流量。断路打开后若调用者立即反复重试本地拒绝,仍会消耗入口CPU并产生海量日志,应让拒绝传播为可理解的快速失败或受控降级。熔断指标必须与真实下游错误分开,防止"拒绝很多所以依赖很坏"的自我强化。
限流、并发上限与隔离舱
令牌桶以每秒r补充、最多b个令牌,允许突发b;漏桶按近似固定速率排出并约束队列;并发上限限制同时占用资源的请求数。固定窗口边界可能形成短时双倍突发,滑动窗口提高平滑性但增加状态成本。计数器的原子性、时钟和多实例分摊决定实际语义,不能给每个实例完整配额后声称实现了租户全局配额。
输入稳定完成率500/s、平均服务时间80ms,Little定律下平均在途约40;服务时间涨到800ms而速率仍500,平均在途约400。只设500 QPS不限制并发会让慢依赖耗尽连接;增加线程到1000只会把排队移向数据库。要以连接池、线程池、有界队列与deadline共同保护,明确拒绝策略,禁止无限排队。
隔离舱按依赖/业务优先级划分资源,A的慢调用不应占满B的全部线程。共享事件循环仍要保持短回调,阻塞调用移交有界worker;共享连接池若没有租户配额,也可能被低优先级批任务占满。限流应在昂贵工作前执行,执行一半再拒绝不仅浪费资源,还可能产生UNKNOWN副作用。
降级也要维护业务不变量,恢复要有顺序
商品推荐可以用缓存默认值,余额裁决不能用过期余额假装成功;写操作可以接受为PENDING并返回操作查询入口,不能返回成功后静默丢弃。降级响应应区分陈旧、部分结果、已受理、明确失败。缓存兜底要有最大年龄、失效策略与刷新合并,避免断路期间缓存过期触发全量回源。
恢复比打开开关更难:暂停后台回放,少量探测,再按容量增加在线流量,最后释放重试和积压。新流量、重试、补偿、对账需要共享下游预算。若同一时刻全部客户端恢复、线程池队列排空又把积压全速放出,会制造第二轮故障。验收要观察恢复期间p99、在途、拒绝、UNKNOWN余额与积压年龄。
验证:确定性预算模型
先运行完整实验脚本,仅需Python标准库:
python3 labs/middleware-senior/reliability_lab.py budget --json
python3 labs/middleware-senior/reliability_lab.py budget --first-attempt-ms 600 --json默认输入总800ms、固定成本100ms、每次尝试150ms、退避80ms、最多3次。模型在每次尝试前检查"退避+完整尝试"能否放入剩余预算:三次尝试依次剩550/320/90ms,stop=ATTEMPT_LIMIT;放大倍数按三层三次算27。第一次耗时改为600ms后,第二次尝试因剩余100ms装不下"退避80+尝试150"被拒绝,stop=INSUFFICIENT_DEADLINE;对照的"只数次数不看预算"策略会把3次全部发出并超支360ms,这就是发送前检查要防的结果。输出model=deterministic-deadline-admission-not-network。
本轮公开CLI及断言已运行,结果符合以上预期;模型用确定性步骤表达预算,不模拟真实网络超时精度与时钟。熔断/限流为方法模型,未绑定具体库源码;以下真实环境实验未运行,不伪造压测曲线。
隔离mock provider实验设计:provider处理先提交operation_id,再sleep超过consumer timeout,预期consumer超时但业务存在;以同operation_id重试应返回原结果,不产生第二次效果。再把provider设为慢且不报错,对比只有QPS限流与并发上限时的在途数。
保留原综合案例:broker报错但可能已追加、注册中心分区、消费者持有已下线provider列表、写RPC超时。先用event_id判断消息身份,再用目录版本区分发现与调用,再按operation_id查业务效果。恢复顺序为稳定下游、限制尝试、核对UNKNOWN、限速重放与对账;盲目重启全部客户端会同时制造重平衡、连接风暴和重复写。
架构师变化条件
当服务从单依赖变为十路扇出,整体成功率与尾延迟受最慢分支影响,应定义必需/可选分支、每路预算和总并发;不能把十路各自timeout都设成入口timeout再无限等待。跨地域延迟增加时,可以异步化可延迟副作用或本地读模型,但资金裁决仍需明确权威。
当业务加入高优先级支付与低优先级回放,共用500/s总配额可能导致在线请求饿死;需要保留在线配额、限制回放并允许闲时借用。熔断器按单实例、整个服务或租户隔离粒度不同,故障样本与恢复代价也不同。最终方案应写出保护对象、拒绝语义、恢复顺序、UNKNOWN查证责任和压测停止阈值,避免只给一组库默认值。
闭卷验收
本页 7 道闭卷题收进了下方自测面板:先回忆,再展开参考答案与评分点,最后给三档自评。