LeastActive 把新节点打满了?Dubbo active 计数的反直觉陷阱
场景:三台 Dubbo Provider 配置 LeastActive 负载均衡,灰度重启后新节点 CPU 冲到 90%、流量占 72%,其余两台仅 30% 路径:
AbstractClusterInvoker.invoke()→initLoadBalance()→LeastActiveLoadBalance.doSelect()→RpcStatus.getActive()
【遗迹】异常堆栈:LeastActive 不均衡,新节点被打满
上篇讲了 Dubbo 路由规则 force=true 能把 Provider 全过滤掉,这篇来看另一个 Dubbo 集群层的反直觉问题。
Dubbo(2.7.23)三台 Provider 配置了 loadbalance=leastactive——这本意是"谁空闲发给谁"。但灰度重启后监控面板显示:新节点 CPU 飙升到 90%,流量占比 72%;剩下两台 CPU 仅 30%,加起来才 28% 的流量。

翻到 LeastActiveLoadBalance 的源码才发现——RpcStatus.getActive() 返回的是"正在处理的请求数",不是"处理能力"。新节点刚启动时活跃数为 0,LeastActive 看它最"空闲",持续把请求塞给它——直到它被打满。
前置条件:LeastActive 的 active 计数依赖 ActiveLimitFilter
这个反直觉陷阱的前提是 Consumer 端已配置 actives 参数(开启了 ActiveLimitFilter),否则 RpcStatus.getActive() 永远返回 0,LeastActive 退化成加权随机。如果你的 Consumer xml 里有类似配置:
<dubbo:reference id="orderService" interface="..."
actives="10" loadbalance="leastactive"/>
或者注解:
@DubboReference(actives = 10, loadbalance = "leastactive")
那你就处于这个陷阱的触发条件内。
【发掘】源码追踪:LeastActive 的"活性"测量什么
调用入口:AbstractClusterInvoker 的负载均衡选择
追根溯源:Consumer 发起 RPC 调用时,经过 AbstractClusterInvoker.invoke() 进入集群层的负载均衡选择(AbstractClusterInvoker.java#L253-L266):
public Result invoke(final Invocation invocation) throws RpcException {
checkWhetherDestroyed();
List<Invoker<T>> invokers = list(invocation);
LoadBalance loadbalance = initLoadBalance(invokers, invocation);
return doInvoke(invocation, invokers, loadbalance);
}
initLoadBalance() 根据 Consumer URL 的 loadbalance 参数实例化对应的策略实现。如果配了 leastactive,就走 LeastActiveLoadBalance.doSelect()(LeastActiveLoadBalance.java#L40-L114):

核心逻辑三行:
- L63:
RpcStatus.getStatus(invoker.getUrl(), methodName).getActive()——获取每个 Provider 当前活跃请求数 - L69-L75: 找到活跃数最低的 Provider——每次找到更低值就重置选中的节点集合
- L95-L97: 如果只有一个 Provider 活跃数最低,直接返回,忽略权重
第三行是关键。当新节点启动后,它的 active=0,而稳定运行的旧节点可能 active=2 或 3。新节点成为唯一最"空闲"的 Provider——leastCount=1,直接返回,warmup 权重根本用不上。
active 计数器的来源:RpcStatus + ActiveLimitFilter
那么 getActive() 的值到底从哪来?RpcStatus 是一个全局计数器(RpcStatus.java#L94-L156):
public static boolean beginCount(URL url, String methodName, int max) {
RpcStatus methodStatus = getStatus(url, methodName);
for (int i; ; ) {
i = methodStatus.active.get();
if (i == Integer.MAX_VALUE || i + 1 > max) return false;
if (methodStatus.active.compareAndSet(i, i + 1)) break;
}
return true;
}
private static void endCount(RpcStatus status, long elapsed, boolean succeeded) {
status.active.decrementAndGet();
}
beginCount CAS 递增 active,endCount 递减。谁调用的?Consumer 端的 ActiveLimitFilter(ActiveLimitFilter.java#L50-L90):
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
int max = invoker.getUrl().getMethodParameter(methodName, ACTIVES_KEY, 0);
if (!RpcStatus.beginCount(url, methodName, max)) { ... }
return invoker.invoke(invocation);
}
public void onResponse(Result appResponse, ...) {
RpcStatus.endCount(url, methodName, getElapsed(invocation), true);
}
整个链条:请求进来 → ActiveLimitFilter.beginCount → active++ → 向下执行 → 响应回来 → ActiveLimitFilter.endCount → active--。LeastActiveLoadBalance 在每次选择时读到的 active,就是"当前这个 Provider 上有几个请求尚未返回"。
正反馈:为什么 LeastActive 会把冷启动节点打满
正反馈链条就此形成:
① 新节点启动,active=0
② LeastActive 选中它(唯一最低 active),发送请求
③ 请求执行期间 active=1,执行完毕 active 归零
④ 归零一瞬间,LeastActive 又选中它
⑤ 新节点冷缓存 → 单请求处理比旧节点慢 3 倍 →
但 active 在请求间隙始终为 0 →
框架以为它最空闲 → 持续发送请求
⑥ 旧节点永远 active=1 或 2 → 永远不会成为唯一最低 →
稳定流量被新节点抢走

【路径】🔍 IDE 到达路径:三步验证 LeastActive 行为
第一步:确认 active 计数是否生效
Ctrl+Shift+N → 输入 ActiveLimitFilter → 回车
→ Ctrl+F → 输入 beginCount → 回车
→ 看第 55 行的 RpcStatus.beginCount(url, methodName, max)
文件: dubbo-rpc/dubbo-rpc-api/src/main/java/
org/apache/dubbo/rpc/filter/ActiveLimitFilter.java:47
在 beginCount 处设断点。如果请求进来没有触发这个断点 →
Consumer 未配置 actives,LeastActive 退化为随机。
第二步:观察 LeastActive 的选择过程
Ctrl+Shift+N → 输入 LeastActiveLoadBalance → 回车
→ Ctrl+F → 输入 doSelect → 回车
→ 看第 63 行:RpcStatus.getStatus(...).getActive()
文件: dubbo-cluster/src/main/java/org/apache/dubbo/rpc/
cluster/loadbalance/LeastActiveLoadBalance.java:63
设断点在第 63 行和第 95 行。
观察每次选择时各 Provider 的 active 值和 leastCount。
如果 leastCount=1 → 权重无效,直接返回。
第三步:对比 ShortestResponse 的估算方式
Ctrl+Shift+N → 输入 ShortestResponseLoadBalance → 回车
→ Ctrl+F → 输入 estimateResponse → 回车
→ 看第 63 行:succeededAverageElapsed * (active + 1)
文件: dubbo-cluster/src/main/java/org/apache/dubbo/rpc/
cluster/loadbalance/ShortestResponseLoadBalance.java:63
对比 LeastActive 和 ShortestResponse 的"负载评判标准"差异。

【解读】为什么 active 不等同于"负载"
LeastActive:单一维度测量
LeastActiveLoadBalance 的评判标准只有一个维度:RpcStatus.getActive()。它是 Consumer 端记录的、到某个 Provider 的并发请求数。
这个数字只回答了"当前有多少请求在路上",不回答: - 这些请求处理了多久? - Provider 的 CPU 是否吃满? - Provider 是否有缓存命中?
这就是单一维度的局限性。你以为是测"哪台机器最空闲",实际测的是"哪台机器上的请求最晚返回"。
ShortestResponse:双维度估算
Dubbo 2.7.x 内置了 ShortestResponseLoadBalance(ShortestResponseLoadBalance.java#L40-L99):

核心公式:estimateResponse = succeededAverageElapsed × (active + 1)
两个维度相乘:
- succeededAverageElapsed:历史平均成功响应时间——反映"这个 Provider 处理够不够快"
- active + 1:当前并发数 +1——反映"还有多少个请求在排队"
如果一个 Provider 冷缓存导致响应慢(succeededAverageElapsed 高),即使 active 低,相乘后的 estimateResponse 也不会成为最低——不会被打满。
设计对比
LeastActive:
getActive() ← 只问"几个在路上"
= 单一维度,不感知处理能力
ShortestResponse:
succeededAverageElapsed × (active+1) ← 问"还要等多久"
= 双维度,估算综合负载
负载均衡不是"谁空闲给谁",而是"谁能最快处理完给谁"。
LeastActive 理解成前者,ShortestResponse 实现了后者。

不这么写的后果
回到 LeastActiveLoadBalance.doSelect() L95-L97:
if (leastCount == 1) {
return invokers.get(leastIndexes[0]);
}
这段代码没有考虑权重。即使新节点处于 warmup 期(getWeight 返回 10 而非 100),只要它的 active 是唯一最低值,直接返回,warmup 权重形同虚设。
对比来看,如果有多个节点同权(active 相同),走的是 L99-L113 的加权随机——那里会用到 getWeight 的 warmup 权重。但冷启动场景恰恰是新节点的 active 远低于其他节点,走的是唯一最低分支,warmup 机制被跳过了。
选型决策
| 策略 | 判断依据 | 冷启动行为 | 异构集群 | 默认开启 |
|---|---|---|---|---|
| Random | 权重概率 | 均匀,由 weight 控制 | 需手动配 weight | ✅ |
| RoundRobin | 权重轮询 | 自动慢启动 | 权重累积偏差 | ✅ |
| LeastActive | active 并发数 | 新节点被打满 | 活性反转 | ✅ |
| ShortestResponse | 响应时间×并发 | 自动抑制慢节点 | 自适应 | ❌ 需指定 |
| ConsistentHash | 参数哈希 | 缓存倾斜 | 节点变更重哈希 | ❌ 需指定 |
LeastActive 是 Dubbo 2.7.x 的默认负载均衡策略之一(与 Random 并列常用),但"默认"不意味着"通用"。选 LeastActive 的前提是:各 Provider 的处理能力均衡,且请求耗时不悬殊。一旦出现冷缓存、异构硬件、"长请求 + 短请求"混合场景,active 单一维度就会失效。
【收获】排查锚点
下次看到"一台 Provider 负载异常高 + 配置了 LeastActive",按以下顺序排查:
两步验证法
Step 1: Consumer 端是否配置了 actives?
搜索项目中的 @DubboReference(actives=...) 或 xml
的 <dubbo:reference actives="...">。
如果没配 actives -> ActiveLimitFilter 未启用,
LeastActive = 加权随机(所有 active 为 0)。
那不关负载均衡的事——查其他原因。
Step 2: 确认 LeastActive 的选择倾斜
在 LeastActiveLoadBalance.doSelect() 第 63 行设断点,
观察三台 Provider 的 active 值。
如果新节点 active 始终最低且 leastCount=1 ->
走第 95 行直接返回,权重被跳过。
修复方案
// 方案 A:换 ShortestResponse
@DubboReference(actives = 10, loadbalance = "shortestresponse")
// 方案 B:保留 LeastActive + 手动配 weight
// 新节点 weight 降低,旧节点 weight 提高
// 但注意 warmup 阶段的 weight 在 leastCount=1 时无效!
// 所以方案 B 不彻底
// 方案 C:异构集群直接用 Random + 显式 weight
@DubboReference(loadbalance = "random")
// Provider 端配置 weight:
// <dubbo:provider weight="50"/> // 弱小机器
// <dubbo:provider weight="200"/> // 强大机器
排查锚点速查
| 异常现象 | 入口类 | 关键方法 | 排查优先级 |
|---|---|---|---|
| 某台 Provider 流量异常偏高 | LeastActiveLoadBalance |
doSelect() L63 getActive |
1️⃣ 先确认 active 值差异 |
| active 值为 0 且所有节点同值 | ActiveLimitFilter |
invoke() L55 beginCount |
2️⃣ 检查 actives 是否配置 |
| warmup 不生效 | LeastActiveLoadBalance |
doSelect() L95-L97 |
3️⃣ leastCount=1 跳过权重 |
| 自动规避慢节点 | ShortestResponseLoadBalance |
doSelect() L63 estimateResponse |
4️⃣ 换策略验证 |
自检清单
□ 你的 Consumer 端配置了 actives 吗?
—— 没有则 LeastActive=Random,排查方向错了
□ 各 Provider 的处理能力/硬件规格一致吗?
—— 不一致且开 LeastActive → 快的节点被打满
□ 服务有"长请求+短请求"混合吗?
—— 有混合 → LeastActive 失效风险高
□ 你知道 LeastActive 和 ShortestResponse 的区别吗?
—— active 单维度 vs 响应时间×并发双维度
□ warmup 保护被跳过了吗?
—— leastCount=1 时完全不看权重
"LeastActive 的 active 计数器不是负载指示器——它只告诉你此时此刻有几个请求在路上,不告诉你每个请求要跑多久。一个计数器不够的,加一个平均响应时间。"

下篇我们聊 Dubbo 与高版本 Spring 集成时的兼容性问题。
📺 公众号「Ai拆代码的曹操」 🌟 知识星球「Ai拆代码的曹操」