Skip to content

负载均衡与容错机制

1. 这是什么

很多人刚学 Dubbo 时,会把“负载均衡”和“容错”混成一件事。

其实它们解决的是两个不同层面的问题:

  • 负载均衡:这次请求应该打到哪个 Provider
  • 容错机制:如果这次调用失败了,接下来怎么办

你可以先这样理解:

负载均衡决定“选谁来处理”,容错机制决定“处理失败后怎么办”。

比如现在有 3 台用户服务:

  • 10.0.0.11
  • 10.0.0.12
  • 10.0.0.13

Consumer 发起一次请求时,首先要回答:

  • 这次选哪一台?

这就是负载均衡。

如果刚好选中的那台超时了,还要继续回答:

  • 是直接报错?
  • 还是换一台重试?
  • 还是快速失败?
  • 还是返回默认值?

这就是容错机制。


2. 为什么重要

远程调用和本地方法调用最大的区别之一是:

  • 本地调用失败的变量少
  • 远程调用失败的变量很多

例如:

  • 某台机器负载飙高
  • 网络抖动
  • 服务实例重启
  • 单个节点 GC 卡顿
  • 某个机房延迟变大

如果没有合理的负载均衡和容错策略,就会出现:

  • 流量总压到少数节点
  • 故障节点持续被打爆
  • 一次超时引发级联重试
  • 写请求被重复执行
  • 整条链路被拖慢

所以这一章的本质,不只是讲几个策略名词,而是在回答:

分布式系统里,怎么把“偶发失败”控制在可接受范围内。


3. 先建立最核心的直觉

可以把 Dubbo 调用看成“打车”:

负载均衡像什么

像你在平台上打车,系统要从附近司机里挑一个:

  • 随机挑
  • 轮流分配
  • 优先给距离近的
  • 尽量让同一个乘客总是分到同一类司机

容错像什么

像你叫的车如果取消了,你要怎么处理:

  • 重新叫一辆
  • 立刻放弃
  • 换别的平台
  • 返回一个兜底方案

所以你会发现:

  • 负载均衡发生在“调用前”
  • 容错策略发生在“调用失败后”

这两个动作顺序不同,作用也不同。


4. 常见负载均衡策略

4.1 Random:随机

随机是最常见、也最容易理解的一种。

特点:

  • 从多个 Provider 中随机选一个
  • 实现简单
  • 在实例数量足够多、请求量足够大时,整体分布通常比较均匀

适合:

  • 大多数普通读请求
  • 节点性能比较接近的场景

通俗理解:

  • 每次从可用实例里随机抽一个

优点

  • 简单
  • 分布通常比较自然
  • 不容易因为顺序固定导致局部热点

缺点

  • 单次短时间窗口内可能不均匀
  • 不考虑实例当前真实负载

4.2 Round Robin:轮询

轮询的思路是:

  • 这次给 A
  • 下次给 B
  • 再下次给 C
  • 然后再回到 A

适合:

  • 节点性能相近
  • 请求耗时也比较接近

优点

  • 直观
  • 理论上分配比较平均

缺点

如果某一台机器虽然“轮到它了”,但它刚好很慢,轮询并不会自动避开它。

所以轮询的问题在于:

  • 它只关注“分配顺序”
  • 不关注“实例当前状态”

4.3 Least Active:最少活跃调用数

这个策略更关注当前实例是否忙。

它会优先选择:

  • 当前正在处理请求数更少的实例

通俗理解:

  • 谁现在手头活少,就先把新请求给谁

适合:

  • 各实例处理能力不同
  • 请求耗时差异较大

优点

  • 比纯随机、纯轮询更能反映当前负载情况

缺点

  • 实现和理解都稍复杂
  • 对统计数据准确性有依赖

4.4 Consistent Hash:一致性哈希

一致性哈希常见于“同一类请求尽量打到同一台机器”的场景。

例如:

  • 同一个用户 ID 的请求尽量落到同一实例
  • 同一个会话尽量命中同一机器上的缓存

通俗理解:

  • 不是随机选机器
  • 而是根据某个 key 算出“它更适合去哪台机器”

适合:

  • 有会话粘性需求
  • 希望同类请求稳定命中同一节点
  • 本地缓存命中率很重要

优点

  • 同一个 key 通常映射到固定实例
  • 有利于局部缓存命中

缺点

  • 节点变更时,映射关系会变化
  • 不适合所有业务都无脑使用

5. 负载均衡到底怎么选

没有“永远最好的”策略,只有“更适合当前业务”的策略。

可以先这么记:

  • 普通均匀流量:随机很常见
  • 追求平均分配:轮询可作为入门理解
  • 实例忙闲差异明显:最少活跃更合适
  • 希望同一类请求稳定落同一机器:一致性哈希更合适

不要把负载均衡理解成一道标准答案题,它更像权衡题。


6. 常见容错策略

容错机制解决的是:

  • 调用失败后怎么处理

这件事一定要结合业务类型来定。

6.1 Failfast:快速失败

含义是:

  • 调用失败后立刻报错
  • 不做额外重试

适合:

  • 非幂等写请求
  • 对重复执行敏感的操作

例如:

  • 扣库存
  • 下单
  • 支付确认

为什么适合写请求?

因为很多写操作不能随便重试,不然可能产生重复副作用。


6.2 Failover:失败转移

含义是:

  • 调用某个 Provider 失败后
  • 再换一个 Provider 继续重试

适合:

  • 幂等读请求
  • 查询类请求

例如:

  • 查用户资料
  • 查商品详情
  • 查配置

优点

  • 能提高读请求成功率

风险

  • 会放大调用次数
  • 在下游整体不稳定时,可能让雪崩更严重

所以要特别注意:

重试不是免费补救,它会扩大流量。


6.3 Failsafe:失败安全

含义通常是:

  • 调用失败后,不抛出强错误
  • 记录日志或忽略异常

适合:

  • 非核心辅助操作
  • 不影响主流程的统计、审计类动作

例如:

  • 异步埋点
  • 非关键日志上报

不适合:

  • 核心交易流程

6.4 Failback:失败自动恢复

含义是:

  • 失败后先记录下来
  • 后续再后台重试补偿

适合:

  • 允许最终一致
  • 不要求请求必须同步成功

例如:

  • 某些通知、消息补偿类场景

6.5 Forking / Broadcast(了解即可)

有些策略会:

  • 并行调用多个实例,谁先返回用谁
  • 或广播到所有实例

这些策略适用面更窄,通常只在特定场景下使用。

入门阶段先重点掌握:

  • 快速失败
  • 失败转移

就够用了。


7. 为什么写请求通常不适合盲目重试

这是实战里最重要的点之一。

假设你有一个创建订单接口:

java
createOrder(userId, productId)

如果第一次调用已经在服务端执行成功,但 Consumer 因为网络超时没收到结果,这时如果再重试一次,可能发生:

  • 第二次又创建出一个订单
  • 或第二次扣减了一次库存

于是就出现了典型问题:

  • 重复下单
  • 重复扣款
  • 重复发券

所以写请求一般要谨慎:

  • 能不重试就别乱重试
  • 真要重试,必须有幂等保障

这也是为什么:

读请求和写请求的容错策略,往往不能一刀切。


8. 动手验证:感受策略差异

如果你本地有 Dubbo 环境,可以做几个小实验。

8.1 准备两个 Provider

例如启动两个用户服务实例:

  • provider-A
  • provider-B

8.2 故意让一个实例变慢

在其中一个实例里加:

java
Thread.sleep(3000);

8.3 连续发起请求

观察:

  • 随机策略下,请求分布是否均匀
  • 轮询策略下,是否会稳定轮到慢实例
  • 最少活跃策略下,慢实例是否逐渐少接请求

8.4 再模拟失败

把其中一个实例停掉,观察:

  • 快速失败时,Consumer 如何报错
  • 失败转移时,是否会切到另一台实例继续调用

这类实验能让你真正理解:

  • 负载均衡在选谁
  • 容错策略在失败后怎么收场

9. 超时设置为什么和容错强相关

很多人只盯着“重试次数”,却忽略超时。

其实超时是容错里的关键参数。

比如:

  • 单次超时 5 秒
  • 重试 2 次

那一次最坏调用时间,可能就远超你想象。

这会导致:

  • 上游线程堆积
  • 接口整体变慢
  • 故障在链路里层层放大

所以容错不是只看“要不要重试”,还要同时看:

  • 超时多久
  • 重试几次
  • 总体 SLA 能否接受

10. 最容易踩的坑

10.1 所有接口统一重试

这是最危险的做法之一。

读写不分地统一重试,极容易把写接口搞出重复副作用。

10.2 看到失败就拼命重试

当下游已经不稳定时,盲目重试往往不是修复,而是放大故障。

10.3 只关注平均分配,不关注实例健康

负载均衡不是“分得平均”就够了,还要关注:

  • 某台实例是不是已经慢了
  • 某台实例是不是故障前兆

10.4 一致性哈希乱用

如果业务根本不需要“同类请求稳定命中同一实例”,那上来就用一致性哈希,只会增加理解和维护成本。

10.5 超时设置过大

超时过大会导致故障暴露太慢,线程和连接资源会被拖住。


11. 一套实用决策思路

以后你给某个 Dubbo 接口选策略时,可以先问自己 4 个问题:

  1. 这是读请求还是写请求?
  2. 它是不是幂等?
  3. 调用失败后,业务是更怕“失败”,还是更怕“重复执行”?
  4. 下游实例之间是否需要稳定路由到同一台?

通常可以得到一个比较务实的起点:

  • 查询类接口:可考虑失败转移 + 合理超时
  • 核心写接口:优先快速失败 + 业务幂等设计
  • 本地缓存敏感接口:可评估一致性哈希

12. 练习建议

练习 1:为读写接口分别选策略

任选两个接口:

  • 一个查询接口
  • 一个写入接口

分别设计:

  • 负载均衡方式
  • 容错策略
  • 超时设置

练习 2:模拟一次下游故障

步骤:

  1. 启动两个 Provider
  2. 停掉其中一个
  3. 观察不同容错策略的行为差异

练习 3:思考重试风险

找一个“创建/扣减/支付”类接口,分析:

  • 如果 Consumer 因超时重试,会造成什么副作用

13. 自测问题

  • 负载均衡和容错分别解决什么问题?
  • 为什么写请求通常不适合盲目重试?
  • 一致性哈希最适合哪类场景?
  • 超时设置为什么会直接影响容错效果?
  • 当下游已经抖动时,为什么重试可能会放大故障?

14. 这一章你至少要带走什么

如果你看完只记住 4 件事,就先记这 4 件:

  1. 负载均衡负责选实例,容错机制负责处理失败
  2. 读请求和写请求不能用同一套重试思路
  3. 重试会放大流量,必须谨慎设置
  4. 超时、负载策略、容错策略要一起看,不能孤立配置

把这几个点建立起来,后面你看 Dubbo 的集群容错配置时,就不会只停留在“背策略名”了。