
Dubbo 的负载均衡是一个 SPI 扩展点 org.apache.dubbo.rpc.cluster.LoadBalance,默认实现是 random。官方注册了六种策略:
名称 | 实现类 | 一句话定位 |
|---|---|---|
random | RandomLoadBalance | 无状态加权随机,默认兜底 |
roundrobin | RoundRobinLoadBalance | 平滑加权轮询 |
leastactive | LeastActiveLoadBalance | 选活跃请求数最少的节点 |
shortestresponse | ShortestResponseLoadBalance | 估算响应最短,滑动窗口 |
consistenthash | ConsistentHashLoadBalance | 一致性哈希,保 key 亲和 |
adaptive | AdaptiveLoadBalance | P2C + provider 指标 + EWMA |
读完这篇你能搞清三件事:
1. 一次 RPC 调用里,负载均衡到底在哪个环节被触发,前后发生了什么。
2. 六种策略各自的真实行为、长处和代价——结合代码和历史 commit 看取舍,不做文档式罗列。
3. 为什么 Dubbo 没有独立的 ewma 策略,以及为什么给 consistenthash 加「过载转移」的尝试被回退了。
这篇基于 Dubbo 源码与 git 历史的分析记录,行文偏工程笔记,结论给得比较克制——选型没有银弹,历史教训比「哪个最好」更有参考价值。
负载均衡并不是一个独立的步骤,它嵌在集群调用器(Cluster Invoker)的选路逻辑里。消费者侧正常调用路径如下:

第 ④ 步 select(...) 内部还有一层决策,放大看:

调用链上有两个常被忽略的点。
一是候选集是路由之后的列表:Directory + Router 会先按规则和可用性筛一轮,LoadBalance 只在剩下的 provider 里挑。所以「为什么某个节点一直没流量」这类问题,有时根因在 Router 而不在 LoadBalance。
二是集群容错策略调用 select 的方式不一样:
• FailfastClusterInvoker:选一次,调一次,失败即返回。
• FailoverClusterInvoker:每次重试都重新选,并把已经调用过的 provider 列表传进去,尽量避开同一节点。
• ForkingClusterInvoker:一次选多个 Invoker 并行调用,谁先成功用谁。
先用一张表给整体适配性,下面再逐个展开。
策略 | 适合 | 不适合 |
|---|---|---|
random | 默认兜底、provider 容量接近、低复杂度系统 | 延迟或负载差异显著 |
roundrobin | provider 稳定、请求成本接近、要平滑分布 | 慢节点照样被均匀分到流量 |
leastactive | 请求成本波动大、想避开当前繁忙节点 | 活跃数不等于真实响应成本 |
shortestresponse | 对响应时间敏感、延迟有可见波动 | 冷启动和无样本时信号弱 |
consistenthash | 缓存亲和、租户/会话/key 固定路由 | 热点 key 和动态负载均衡 |
adaptive | 有指标反馈回路、想动态优化负载 | 指标反馈链路不可靠时退化 |
random:无状态的兜底RandomLoadBalance 是无状态的。没有显式权重、没有 warmup 时间戳时,它在候选列表里均匀随机;有权重或 warmup 时,按累积权重做加权随机,候选较多时用二分查找。
长处
• 协调成本极低。
• 不维护 per-method / per-invoker 状态。
• provider 列表频繁变动时依然稳。
代价
• 短时间窗口内分布可能不均。
• 对延迟、活跃请求数、失败、负载都没有反应。
一句话:它的强项就是「什么都不依赖」,所以适合做默认值,也适合在拿不准时兜底。
roundrobin:平滑加权轮询RoundRobinLoadBalance 用的是平滑加权轮询(smooth weighted round robin)。它为每个 service/method 维护一张 WeightedRoundRobin 表,按 invoker 标识做 key。每次选择:把当前权重加到该 invoker 的当前值上,挑当前值最大的那个,再从它身上减去总权重。
长处
• 加权分布稳定。
• 短时间窗口比 random 更平滑。
代价
• 热路径上要维护可变状态。
• provider 下线时要做缓存清理。
• 同样不感知延迟和活跃数。
注意:状态在热路径上共享,并发问题历史上出过。commit 317e62f73f(Fix concurrency problems in RpcStatus and RoundRobinLoadBalance (#5881))就专门修过这里的并发 bug。任何想新加状态型策略的人,这都是前车之鉴。
leastactive:选活跃数最少的LeastActiveLoadBalance 扫一遍所有候选 Invoker,读 RpcStatus.active(对 invoker URL + method 维度),挑活跃数最低的那些;如果权重不同,再在并列者里做加权随机。
长处
• 能对在途请求压力做反应。
• 直接复用现有 RpcStatus 计数器,实现简单。
代价
• 活跃数不等于服务成本:一个慢请求占的活跃数和一个快请求一样,但实际负载差很多。
• 忽略成功延迟、失败和超时率。
• 要扫全部候选。
shortestresponse:滑动窗口估响应ShortestResponseLoadBalance 用一个滑动窗口的成功耗时来估算响应成本,对每个 invoker/method 状态算:
estimated response = 当前窗口内成功调用的平均耗时 × (active + 1)
选估算值最小的,并列时加权随机。
长处
• 比 leastactive 更贴近真实延迟。
• 用滑动窗口而不是全量历史,避免老数据拖累。
代价
• 估算只基于成功调用,失败和超时压力没被直接体现。
• 冷启动或无样本节点估算值可能是 0。
• 要维护 per-status 的窗口偏移,并周期性重置。
它的历史能反映 Dubbo 的取舍演变:commit 6d2ba7ec7b(add new loadbalance strategy (#6064))最初用全量成功平均耗时 × 活跃数;后来 commit ffb520908a(optmization about shortestResponseLoadBalance (#8441))改成滑动窗口 + 周期重置。项目显然认为全量平均太陈旧,不适合做负载均衡决策。
consistenthash:保 key 亲和ConsistentHashLoadBalance 在方法级别构建带虚拟节点的选择器,对配置的入参做哈希,在环上找到第一个匹配的虚拟节点返回对应 Invoker。
长处
• 保 key 亲和。
• 适合缓存本地性、会话式路由。
代价
• 故意不感知负载——这是设计上的取舍,不是缺陷。
• 热 key 会把单个 provider 打爆。
• 选择器的缓存和重建对并发、对路由后 invoker 列表的 identity 很敏感。
这里有一条非常重要的历史教训,单独放到第四节讲。
adaptive:P2C + 多信号融合AdaptiveLoadBalance 用的是 power-of-two choices(P2C):随机抽两个 Invoker,挑负载得分低的那个。负载得分来自 AdaptiveMetrics,而且它不只是选,还会往 invocation 的 attribute 和 attachment 里写信息,让 AdaptiveLoadBalanceFilter 在调用结束后采集响应时间和 provider 侧指标。
这意味着 adaptive 的正确性依赖的不止是 LoadBalance 这个类本身:
• 选择阶段:记录选中的 provider、开始时间、loadbalance=adaptive 标记,以及请求 provider 指标(mem、load)的 attachment。
• 调用结束后:AdaptiveLoadBalanceFilter 检查标记,成功就读响应 attachment、加上消费者侧响应时间、更新 AdaptiveMetrics;失败就给选中 provider 记一次错误请求。
如果消费者侧 filter 没生效、响应没带回 provider 指标、或者指标更新链路延迟/丢失,策略还在跑,但丢了部分信号。这时选路会退化成「P2C + 不完整/默认指标 + 权重兜底」,对某些部署环境而言,未必比 random、leastactive、shortestresponse 这些简单策略更可靠。
长处
• 比「全表扫描」类策略更现代。
• 同时用消费者侧耗时和 provider 上报指标。
• 把权重和超时也纳入负载比较。
代价
• 不是单类策略,正确性依赖 filter + 指标反馈回路。
• provider 指标是 best-effort、异步的。
• 动件多,边界 case 多。
commit d17160591c(Adaptive loadbalance (#10745))一次性加上了 AdaptiveLoadBalance、AdaptiveLoadBalanceFilter 和测试;后来 commit 33f688e404(Update AdaptiveLoadBalance.java (#12636))又修了 P2C 两两比较里的超时查找 bug。这条历史的提示是:自适应策略的契约面更大——invoker identity、超时查找、指标 key、listener 行为,每一样都得对。
ewma 策略Dubbo 确实用了 EWMA 风格的延迟平滑,但它是嵌在 adaptive 里的一个信号,没有暴露成独立的 ewma 实现。
AdaptiveMetrics 里有一个 beta 和一个 ewma 字段,更新 provider 指标时套的是经典 EWMA 公式:
Vt = beta × Vt-1 + (1 - beta) × lastLatency
但最终的自适应负载得分远不止延迟一项,它把 provider CPU/load、EWMA 延迟、在途请求数、消费者侧成功率、配置权重都揉了进去。简化形式:
load =
providerCPULoad
× (sqrt(ewma) + 1)
× (inflight + 1)
/ (successRatio × weight + 1)
所以 Dubbo 当前的形态更接近:
adaptive = P2C + provider load + EWMA 延迟 + inflight + 成功率 + 权重
而不是:
ewma = 仅延迟 EWMA
为什么不把「仅延迟 EWMA」单独做成主选自适应策略?几个实际原因:
1. 延迟 EWMA 单独用会漏信号:高在途数、高 provider CPU/负载、近期错误、恢复期低样本置信,这些它都看不到。
2. EWMA 需要调用结束反馈,没法纯在 LoadBalance#doSelect(...) 里实现,必须有类似 AdaptiveLoadBalanceFilter 的反馈回路。
3. shortestresponse 已经覆盖了部分响应时间敏感场景,用的是滑动窗口估算。
4. 再加一个独立的 EWMA 策略,等于又引入一个带缓存生命周期、并发、冷启动、method key 隔离问题的热路径实现。
结论:Dubbo 把 EWMA 当作更宽自适应模型里的一个信号,而不是把它当成完整的负载均衡策略。这点和很多人的预期不一样。
consistenthash 的两次教训:别动语义契约consistenthash 的历史有两条值得记住的教训。
commit fee0dbab0c(Improve consistent hashing load balancing with a new algorithm (#8948))试过给一致性哈希加过载感知兜底:在环上选中节点后,如果请求计数超过某个均值阈值,就跳到下一个节点。
后来 commit 89b74fe7f1(Revert #8948 (#10233))把这套行为回退掉了。
为什么?一致性哈希的价值就是 key 亲和。在同一个策略里加负载再分配,等于改了它的语义契约——用户选 consistenthash 是为了「同一个 key 永远落到同一个节点」,一旦这个保证被打破,缓存命中、会话亲和全乱。
教训很直接:如果想要「带过载感知的一致性哈希」,应该做一个新的显式策略(比如叫 boundedconsistenthash),而不是改 consistenthash 本身。
consistenthash 在历史上反复修,集中在两条线:
• 正确性:commit caed64b2a6(Fix #13750, ConsistentHashLoadBalance not base on arguments (#13753))修正了选择逻辑——它应该对入参做哈希,而不是对方法名。
• 热路径成本:commit c8a8946f89(Fix the issue of high CPU load caused by continuously creating new ConsistentHashSelector instances under high concurrency (#15497))把选择器创建改成用 ConcurrentMap 的原子 compute;commit 8f2b4decdc(End the 'read/write' combination operation early to improve performance (#15666))又加了 read-first 快路径,让常见情况不必进入 compute(...)。
这几条 commit 合在一起说明一件事:负载均衡代码在非常热的路径上。多余的内存分配、map 写入、选择器重建,在高并发下都会被放大成可见的 CPU 开销。任何新策略都得把这条当作 API 契约的一部分来对待。
下面这些不是定论,是反复出现的模式给出的克制提示:
1. 新策略不应悄悄改老策略的语义,consistenthash 尤其如此。
2. 热路径成本应被视为 API 契约的一部分。全表扫描、compute(...)、内存分配、加锁,都需要理由。
3. 纯消费者侧策略好部署,但只看得到本地观察;要感知 provider,就得有类似 adaptive 的反馈回路。
4. 状态要小心地按 key 维护和清理。provider 上下线、router 输出变化、method 级隔离,历史上都引发过修复。
5. 延迟感知要避免全量平均。shortestresponse 的演变指向窗口或 EWMA 式衰减。
文中涉及的部分 Dubbo 概念和术语,简要说明如下。
Dubbo 的扩展机制。通过 META-INF/dubbo/internal/ 下的配置文件声明接口与实现的映射,运行时按名称动态加载。LoadBalance 就是一个 SPI 扩展点,默认实现是 random。
Dubbo 对远程服务提供者的抽象封装。每个 provider 在消费者侧会被包装成一个 Invoker 对象,负载均衡就是在多个 Invoker 里选一个来发起调用。
服务目录,维护某个服务的所有可用 Invoker 列表。它会和注册中心同步,感知 provider 上下线。负载均衡拿到的候选集,就是 Directory 输出的列表。
路由规则,在 Directory 输出的候选集上再做一次过滤。比如按区域、按标签、按条件筛掉不符合规则的 provider。路由发生在负载均衡之前。
集群层的调用器,封装了容错策略(Failover、Failfast、Forking 等)。它负责把 Invocation 分发给具体的 provider,负载均衡是它内部选路的一环。
会话粘性机制。如果启用了 sticky,同一个消费者对同一个服务的多次调用,会尽量固定到同一个 provider 上,用于有状态服务的场景。
当前在途请求数。RpcStatus 会记录每个 provider + method 维度的活跃调用计数,leastactive 策略用它来判断节点繁忙程度。
新上线 provider 的权重渐进机制。刚启动的节点不会立刻承担全量流量,而是逐步增加权重,避免冷启动时被瞬间打满。
provider 的流量分配权重。权重高的节点会收到更多请求。可以在 URL 配置,也可以通过预热动态调整。
随机抽两个候选,比较后选更优的那个。adaptive 策略用的就是这个模式,避免了全表扫描,同时仍有一定随机性。
指数加权移动平均,一种平滑历史数据的算法。公式:Vt = β × Vt-1 + (1 - β) × 新值。adaptive 策略用它来平滑延迟估计。
兜底选路机制。当首选的 provider 不可用(比如在调用过程中被剔除),会重新从剩余候选里再选一个。
Dubbo 的负载均衡表面看是「六选一」的配置项,背后其实是一组不同取舍的工程决策:
• 想要无脑兜底,用 random。
• 想要平滑分布,用 roundrobin,但要接受它有状态、要清理缓存。
• 想要避开繁忙节点,leastactive 简单,shortestresponse 更贴延迟。
• 想要key 亲和,consistenthash,但别指望它感知负载——也别试图让它感知。
• 想要动态自适应,adaptive 最全,但要保证 filter 和指标反馈链路是通的,否则会退化成不如简单策略的 P2C。
历史里最有价值的部分并非「哪个策略最好」,而是两条反复被验证的约束:别动已有策略的语义契约,热路径成本就是 API 契约。这两条比任何选型建议都更值得带走。
本文基于 Dubbo 源码与 git 历史整理,commit 编号均可在官方仓库对应 PR 中查到。策略选型无银弹,生产环境建议按真实流量特征做对比验证。