Dubbo 集群容错与负载均衡详解
定位:Dubbo 目录第 04 篇(治理能力篇),讲透集群层的容错策略、负载均衡算法、路由规则、降级 Mock 与超时重试------这是面试考察最密集的一篇
适用版本:Dubbo 3.x(JDK 8+/17)
目录
一、集群层定位
1.1 在调用链中的位置
回看 02 篇调用链,集群层是消费方进入网络前的最后一道"逻辑层":
代理 → 【集群层(容错 + 路由 + 选址)】 → Filter → 协议层 → 网络
它对上层(代理)隐藏"后面有 N 台机器"的事实,对下层(协议)只发起"对一台机器的单次调用"。
1.2 三个职责、四个抽象
| 抽象 | 职责 | 本篇章节 |
|---|---|---|
Cluster |
失败后怎么办:重试/快速失败/忽略 | 第二节 |
Directory |
候选地址从哪来(接注册中心推送,06 篇) | --- |
Router |
候选集先按规则过滤(灰度/泳道) | 第四节 |
LoadBalance |
过滤后的候选集里选哪一个 | 第三节 |
一次调用的选址流水线:
Directory 全量候选
→ Router 链过滤(标签/条件/脚本)
→ LoadBalance 选 1 个
→ 发起调用
→ 失败?交给 Cluster 策略决定下一步
四个抽象全部是 SPI 扩展点(03 篇),都可自定义替换------这是治理能力的扩展性基础。
二、六大容错策略
容错策略通过 cluster 参数配置(默认 failover),全部是 Cluster SPI 的实现:
2.1 Failover(失败自动切换,默认)
调用节点 A → 失败 → 换节点 B 重试 → 失败 → 换节点 C
总次数 = 1 + retries(默认 retries=2,共调 3 次)
- 语义:假设失败是"那台机器的问题",换一台大概率能成;
- 适用:读操作、幂等写操作;
- 风险:非幂等场景重试会重复执行;重试会放大下游压力(故障时重试风暴,见 6.4)。
2.2 Failfast(快速失败)
调用节点 A → 失败 → 立即抛异常,不重试
- 适用:非幂等写操作(下单、扣款)、明确只允许执行一次的逻辑;
- 与重试的关系 :Failfast 下
retries配置无效------"不重试"正是它的目的。
2.3 Failsafe(失败安全)
调用 → 失败 → 吞掉异常,返回空结果,仅记日志
- 适用:旁路逻辑------写审计日志、埋点上报。这些调用失败不应影响主流程;
- 注意:它不是"安全地失败",而是"失败了也当没发生",只可用于允许静默丢失的调用。
2.4 Failback(失败自动恢复)
调用 → 失败 → 把请求记入内存队列 → 后台定时任务重发
- 适用:消息通知类、最终一致即可的操作;
- 注意 :重发队列在内存中,进程重启会丢失;积压过多会占内存,有上限保护(超限丢弃并告警)。
2.5 Forking(并行调用)
同时并行调 N 台(forks 参数,如 2)→ 任一台先成功即返回,其余取消
- 适用:实时性要求极高、可承受资源浪费的场景;
- 代价:一次调用消耗 N 倍下游资源,是"用钱买延迟"。
2.6 Broadcast(广播调用)
逐台调用所有节点 → 任一台失败即整体失败
- 适用:通知所有节点做本地动作------刷新本地缓存、更新本地配置;
- 语义:要求"全部成功",一台失败即失败(不做部分成功的语义,需自定义)。
2.7 选型速查
| 场景 | 策略 | 原因 |
|---|---|---|
| 查询接口 | Failover | 幂等,重试安全 |
| 下单/扣款 | Failfast + retries=0 | 非幂等,重复执行有资损风险 |
| 审计日志 | Failsafe | 失败可静默 |
| 消息通知 | Failback | 最终一致即可 |
| 毫秒级实时 | Forking | 延迟优先 |
| 缓存刷新通知 | Broadcast | 需要全部节点执行 |
自定义策略 :实现 Cluster 接口(SPI),如"先试主集群、失败切备集群"的多机房容错。
三、负载均衡算法
loadbalance 参数,默认 random,全部是 LoadBalance SPI 实现。
3.1 Random(加权随机,默认)
按权重随机选择。权重来自注册信息(weight 参数),默认 100。
节点 A weight=100, B weight=300
→ 随机落点中 3/4 概率选中 B
特点:单次有随机性,大样本下调用量趋近权重比例;实现简单、无状态。
3.2 RoundRobin(加权轮询)
按权重比例轮询分配。权重 100:300 时,每 4 次调用中 1 次给 A、3 次给 B。
实现细节(平滑加权轮询):
每个节点维护 currentWeight,每轮所有节点 currentWeight += weight,
选 currentWeight 最大者,选中后 currentWeight -= totalWeight
→ 分配序列均匀打散(不会连续 3 次都打 B),避免瞬时集中
特点:分配精确、序列平滑;对慢节点无感知------慢节点照样分到同样多的请求。
3.3 LeastActive(最少活跃数)
"活跃数"= 已发起、未返回的调用数量。每次选活跃数最小的节点。
直觉:节点处理得越快,手里的在途请求越少 → 活跃数小
结果:慢节点自然分到更少流量(自适应)
特点:对性能差异自适应,无需人工调权重;活跃数相同则退化为加权随机。注意统计是消费方本地视角,多消费方各自统计。
3.4 ConsistentHash(一致性哈希)
相同调用参数路由到同一节点:
① 把每个节点映射到哈希环上,并按权重生成虚拟节点(默认 160 个/实例)
② 对调用参数(默认取第一个参数,可配 hash.arguments)做哈希
③ 顺时针找第一个虚拟节点 → 定位到物理节点
关键性质:
- 会话粘滞:同一用户/参数的请求稳定落在同一节点,利于该节点做本地缓存;
- 增删节点影响小:只影响环上相邻的一小段,不会全局洗牌(虚拟节点让分布更均匀);
- 虚拟节点的作用:物理节点少时直接映射分布不均,虚拟节点放大采样点使负载均衡。
3.5 ShortestResponse(最短响应,3.x 新增)
估算每个节点的"平均响应时间"(滑动窗口内的平均耗时),选估算最快的,同分加权随机。思想与 gRPC 的 P2C(Power of Two Choices)相近:用响应速度直接做选址依据。
与 LeastActive 的区别:LeastActive 看"在途数量"(间接指标),ShortestResponse 直接看历史耗时(直接指标),对"慢但并发低"的节点识别更准。
3.6 算法对比速查
| 算法 | 状态 | 对慢节点感知 | 典型场景 |
|---|---|---|---|
| Random | 无 | 无 | 通用默认 |
| RoundRobin | 每节点游标 | 无 | 机器同质、求精确分配 |
| LeastActive | 活跃计数 | 有 | 机器异构/性能不均 |
| ConsistentHash | 哈希环 | 弱 | 参数级粘滞、本地缓存 |
| ShortestResponse | 耗时窗口 | 强 | 追求低尾延迟 |
四、路由规则
路由在负载均衡之前 执行,职责是把候选集按规则缩小。规则通常由配置中心动态下发,运行时生效、无需重启。
4.1 条件路由(Condition)
基于 URL 参数的 if-then 表达式:
# 含义:来源是 10.20.153.10 的请求,只允许调往 10.20.153.11
host = 10.20.153.10 => host = 10.20.153.11
# 常见用途:白名单测试机
=> host != 172.22.3.91 # 除测试机外都可见(生产流量绕开测试机)
典型用途:测试机隔离、机房就近路由的粗粒度版本、应急摘除某些节点(=> 空 即全拒)。
4.2 标签路由(Tag)
给实例打标签(如 tag = gray),请求携带标签则只路由到同标签实例:
提供方:dubbo.provider.tag = gray (灰度机器组)
消费方:请求上下文设置 attachment tag = gray
→ 灰度流量只落在灰度机器;无标签流量不落到灰度机器(隔离)
这是全链路灰度/泳道的基础设施:发布时给新版本机器打标签,验证流量定向打过去,出问题只影响标签内。
4.3 脚本路由(Script)
用 Groovy/JavaScript 等脚本写任意过滤逻辑:
groovy
// 只保留非 10.x 段的提供者(示意)
invokers.findAll { !it.url.host.startsWith("10.") }
表达力最强,但脚本执行有开销且需谨慎审核内容(配置中心下发的脚本等同生产代码)。
4.4 三种路由的取舍
| 类型 | 表达力 | 维护成本 | 适用 |
|---|---|---|---|
| 条件路由 | 低 | 低 | 简单白名单/黑名单/摘机 |
| 标签路由 | 中 | 低 | 灰度、泳道(推荐) |
| 脚本路由 | 高 | 高 | 复杂规则,少用 |
五、服务降级与 Mock
5.1 Mock 的两种模式
java
// 失败才降级(自动):正常调用,异常时返回 Mock 结果
@DubboReference(mock = "fail:return null")
// 强制降级(手动):不调远端,直接返回 Mock 结果(应急开关)
@DubboReference(mock = "force:return empty")
| 模式 | 行为 | 用途 |
|---|---|---|
fail: |
调用失败后返回 Mock | 自动兜底:弱依赖挂了不拖垮主流程 |
force: |
根本不发起调用 | 手动熔断开关:故障期人为切断某依赖 |
5.2 自定义 Mock 类
复杂返回(非字面量)用实现同接口的 Mock 类:
java
public class GreetingServiceMock implements GreetingService {
@Override
public String sayHello(String name) {
return "服务暂不可用"; // 降级返回
}
}
java
@DubboReference(mock = "com.demo.GreetingServiceMock")
private GreetingService greetingService;
Mock 类必须有无参构造;它的实例在消费方本地创建执行,不经过网络。
5.3 降级与容错的关系
容错(Cluster):在"服务还活着"的前提下处理单次失败
降级(Mock): 在"服务不值得调/调不通"时给出替代结果
两者叠加:fail: mock 本质是容错链兜底的最后一环
六、超时与重试机制
6.1 超时配置在谁身上生效
一个高频误解:超时不是"提供方执行超过就中断",而是消费方的等待上限。
消费方发起调用,启动计时(默认 1000ms)
→ 期间提供方还在执行,但消费方到点即抛 TimeoutException
→ 提供方是否继续执行?默认继续跑完(除非业务自行检查)
推论:
- 超时只节省消费方线程,不自动节省提供方资源;
- 超时 + 重试组合在非幂等场景 = 重复执行风险(6.4)。
6.2 配置优先级链
消费方方法级 > 消费方接口级 > 消费方全局
> 提供方方法级 > 提供方接口级 > 提供方全局 > 默认 1000ms
规则两条:
- 消费方优先:等多久由等待方说了算;
- 小范围优先:方法级覆盖接口级,接口级覆盖全局。
提供方配置超时的意义:作为"服务方自知的合理耗时建议",在消费方没配时兜底。
6.3 重试的计数语义
java
@DubboService(retries = 2) // 首次 + 2 次重试 = 最多调 3 次
注意三个细节:
retries是额外重试次数,不是总次数;- 重试默认换节点(配合 Failover),不是打同一台;
- 只对
RpcException中的超时/网络类失败重试,业务异常不重试(否则错误被放大)。
6.4 重试风暴与防护
故障场景下的恶性循环:
提供方变慢 → 消费方超时 → Failover 重试 → 提供方请求量 ×3
→ 更慢 → 更多超时 → 更多重试 → 雪崩
防护手段:
- 非幂等/写多场景调低
retries = 0; - 提供方限流(08 篇讲线程池/并发控制);
- 超时值合理:显著大于 P99 耗时但不无限大;
- 配合路由摘除故障节点(比无脑重试更优)。
七、总结
- 集群层三职责:容错(失败怎么办)、路由(候选集过滤)、选址(选哪台),对应 Cluster / Router / LoadBalance 三个 SPI,Directory 提供候选。
- 六大容错:Failover 默认(读/幂等)、Failfast(非幂等写)、Failsafe(旁路)、Failback(通知类,内存队列重启丢失)、Forking(实时,资源换延迟)、Broadcast(全节点执行)。
- 负载均衡:Random 加权随机(默认)、RoundRobin 平滑加权、LeastActive 自适应慢节点、ConsistentHash 参数粘滞 + 虚拟节点、ShortestResponse 按历史耗时选址。
- 路由:条件(白黑名单/摘机)、标签(灰度泳道,推荐)、脚本(高表达力,慎用);规则经配置中心动态下发。
- 降级 :
fail:失败后兜底、force:手动切断;自定义 Mock 类处理复杂返回值。 - 超时重试纪律 :超时是消费方等待上限;消费方配置优先;非幂等写必须
retries=0+ Failfast;警惕重试风暴放大故障。
八、常见高频面试题
1. Dubbo 有哪些集群容错策略?默认是哪个?分别适用什么场景?
要点:六种,默认 Failover。Failover 失败换节点重试(读/幂等写);Failfast 一次失败即抛异常(非幂等写,如下单扣款);Failsafe 失败吞掉记日志(审计埋点等旁路);Failback 失败入内存队列后台重发(通知类,注意重启丢失);Forking 并行调多台一个成功即返回(实时性优先,资源换延迟);Broadcast 广播所有节点任一失败即失败(缓存刷新通知)。
2. Dubbo 有哪些负载均衡算法?各自原理与适用?
要点:默认 Random 加权随机,大样本趋近权重比;RoundRobin 平滑加权轮询,分配精确但对慢节点无感知;LeastActive 选在途活跃数最少的节点,慢节点自动少接活;ConsistentHash 一致性哈希 + 虚拟节点,相同参数落同一节点,用于粘滞与本地缓存;ShortestResponse(3.x)按滑动窗口平均耗时选最快节点。异构机器选 LeastActive/ShortestResponse,同质选 RoundRobin,参数粘滞选一致性哈希。
3. 一致性哈希在 Dubbo 里怎么实现?虚拟节点解决什么问题?
要点:把每个实例按权重映射到哈希环并生成虚拟节点(默认 160 个/实例),对调用参数(默认第一个参数,可配)哈希后顺时针找最近虚拟节点定位物理节点。虚拟节点解决物理节点少时分布不均的问题------放大采样点让流量均匀;同时保证增删节点只影响相邻区段,不全局洗牌。适用于同参数请求需要粘滞到同一节点的场景。
4. retries 配置的含义是什么?非幂等接口该怎么配?
要点:retries 是额外重试次数(总调用 = 1 + retries),默认 2;配合 Failover 时每次重试换节点;只对超时/网络类异常重试,业务异常不重试。非幂等接口(下单、扣款)必须设 retries=0 且 cluster=Failfast,否则超时重试会重复执行造成资损;同时注意重试风暴会在故障时放大下游压力。
5. Dubbo 超时是作用在哪一端?提供方超时会中断执行吗?
要点:超时是消费方的等待上限------到点消费方抛 TimeoutException 不再等,但提供方默认继续执行完(不会被打断)。所以超时只保护消费方线程,不自动释放提供方资源。配置优先级:方法级 > 接口级 > 全局,且消费方配置覆盖提供方;提供方配置作为"合理耗时建议"在消费方未配时兜底。
6. 什么是重试风暴?如何防护?
要点:提供方变慢 → 消费方超时 → Failover 重试 → 提供方请求量翻倍 → 更慢,形成雪崩放大回路。防护:写接口/非幂等调低重试次数;超时设置为显著大于 P99 但有限;提供方做并发控制与限流;更优的是用路由/故障摘除把流量从坏节点移开,而不是无差别重试;必要时用熔断(失败率达到阈值暂停调用)替代重试。
7. 灰度发布用 Dubbo 怎么实现?
要点:核心是标签路由。给新版本实例配置 tag=gray 标签,验证请求在上下文携带同标签,则只路由到灰度实例;未带标签的流量不会落到灰度实例,天然隔离。标签规则可由配置中心动态下发与回收。也可用条件路由做基于来源的粗粒度分流。灰度验证通过后摘标签全量,出问题秒级切回(改配置不改代码)。
8. mock = "fail:return null" 和 "force:return true" 的区别?
要点:fail: 是自动降级------先正常发起远程调用,只有失败(异常/超时)时才返回 Mock 结果,用于弱依赖兜底;force: 是强制降级------根本不发起远程调用直接返回,相当于人工熔断开关,用于故障期快速切断某依赖恢复主链路。两者都支持自定义 Mock 类(实现同接口)以返回复杂对象,Mock 在消费方本地执行不经过网络。
9. LeastActive 和 ShortestResponse 都能感知慢节点,有什么区别?
要点:LeastActive 用"在途请求数"作间接指标------处理快的节点手里积压少,活跃数小,适合并发视角;但它对"慢但当前并发低"的节点感知滞后。ShortestResponse 直接统计滑动窗口内平均响应耗时选最快节点(类似 P2C 思想),对慢节点识别更直接准确,是 3.x 新增。机器异构、性能不均时两者都优于静态权重。
10. 条件路由能做什么?应急摘除一台故障机器怎么操作?
要点:条件路由是 if-then 表达式,基于 URL 参数(host、端口、参数等)过滤候选集。用途:白名单测试机(生产流量 => 排除测试机)、来源定向路由、应急摘机。应急摘除故障机器:下发规则把该机器从候选中排除(如配置"=> host != 故障IP"或直接令目标集不含它),动态生效无需重启,比等待健康探测摘除更快;机器恢复后再移除规则。
