Dubbo LeastActive 负载均衡新节点被打满?源码解析 active 计数器陷阱

📅 2026/8/12 18:43:53
Dubbo LeastActive 负载均衡新节点被打满?源码解析 active 计数器陷阱
场景三台 Dubbo Provider 配置 LeastActive 负载均衡灰度重启后新节点 CPU 冲到 90%、流量占 72%其余两台仅 30%路径AbstractClusterInvoker.invoke()→initLoadBalance()→LeastActiveLoadBalance.doSelect()→RpcStatus.getActive()【遗迹】异常堆栈LeastActive 不均衡新节点被打满上篇讲了 Dubbo 路由规则 forcetrue 能把 Provider 全过滤掉这篇来看另一个 Dubbo 集群层的反直觉问题。Dubbo2.7.23三台 Provider 配置了loadbalanceleastactive——这本意是谁空闲发给谁。但灰度重启后监控面板显示新节点 CPU 飙升到 90%流量占比 72%剩下两台 CPU 仅 30%加起来才 28% 的流量。翻到LeastActiveLoadBalance的源码才发现——RpcStatus.getActive()返回的是正在处理的请求数不是处理能力。新节点刚启动时活跃数为 0LeastActive 看它最空闲持续把请求塞给它——直到它被打满。前置条件LeastActive 的 active 计数依赖 ActiveLimitFilter这个反直觉陷阱的前提是 Consumer 端已配置actives参数开启了ActiveLimitFilter否则RpcStatus.getActive()永远返回 0LeastActive 退化成加权随机。如果你的 Consumer xml 里有类似配置dubbo:referenceidorderServiceinterface...actives10loadbalanceleastactive/或者注解DubboReference(actives10,loadbalanceleastactive)那你就处于这个陷阱的触发条件内。【发掘】源码追踪LeastActive 的活性测量什么调用入口AbstractClusterInvoker 的负载均衡选择追根溯源Consumer 发起 RPC 调用时经过AbstractClusterInvoker.invoke()进入集群层的负载均衡选择AbstractClusterInvoker.java#L253-L266publicResultinvoke(finalInvocationinvocation)throwsRpcException{checkWhetherDestroyed();ListInvokerTinvokerslist(invocation);LoadBalanceloadbalanceinitLoadBalance(invokers,invocation);returndoInvoke(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 活跃数最低直接返回忽略权重第三行是关键。当新节点启动后它的active0而稳定运行的旧节点可能active2或3。新节点成为唯一最空闲的 Provider——leastCount1直接返回warmup 权重根本用不上。active 计数器的来源RpcStatus ActiveLimitFilter那么getActive()的值到底从哪来RpcStatus是一个全局计数器RpcStatus.java#L94-L156publicstaticbooleanbeginCount(URLurl,StringmethodName,intmax){RpcStatusmethodStatusgetStatus(url,methodName);for(inti;;){imethodStatus.active.get();if(iInteger.MAX_VALUE||i1max)returnfalse;if(methodStatus.active.compareAndSet(i,i1))break;}returntrue;}privatestaticvoidendCount(RpcStatusstatus,longelapsed,booleansucceeded){status.active.decrementAndGet();}beginCountCAS 递增 activeendCount递减。谁调用的Consumer 端的ActiveLimitFilterActiveLimitFilter.java#L50-L90publicResultinvoke(Invoker?invoker,Invocationinvocation)throwsRpcException{intmaxinvoker.getUrl().getMethodParameter(methodName,ACTIVES_KEY,0);if(!RpcStatus.beginCount(url,methodName,max)){...}returninvoker.invoke(invocation);}publicvoidonResponse(ResultappResponse,...){RpcStatus.endCount(url,methodName,getElapsed(invocation),true);}整个链条请求进来 → ActiveLimitFilter.beginCount → active → 向下执行 → 响应回来 → ActiveLimitFilter.endCount → active–。LeastActiveLoadBalance在每次选择时读到的 active就是当前这个 Provider 上有几个请求尚未返回。正反馈为什么 LeastActive 会把冷启动节点打满正反馈链条就此形成① 新节点启动active0② LeastActive 选中它唯一最低 active发送请求 ③ 请求执行期间active1执行完毕 active 归零 ④ 归零一瞬间LeastActive 又选中它 ⑤ 新节点冷缓存 → 单请求处理比旧节点慢3倍 → 但 active 在请求间隙始终为0→ 框架以为它最空闲 → 持续发送请求 ⑥ 旧节点永远active1或2→ 永远不会成为唯一最低 → 稳定流量被新节点抢走【路径】 IDE 到达路径三步验证 LeastActive 行为第一步确认 active 计数是否生效CtrlShiftN → 输入 ActiveLimitFilter → 回车 → CtrlF → 输入 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 未配置 activesLeastActive 退化为随机。第二步观察 LeastActive 的选择过程CtrlShiftN → 输入 LeastActiveLoadBalance → 回车 → CtrlF → 输入 doSelect → 回车 → 看第63行RpcStatus.getStatus(...).getActive()文件: dubbo-cluster/src/main/java/org/apache/dubbo/rpc/ cluster/loadbalance/LeastActiveLoadBalance.java:63 设断点在第63行和第95行。 观察每次选择时各 Provider 的 active 值和 leastCount。 如果leastCount1→ 权重无效直接返回。第三步对比 ShortestResponse 的估算方式CtrlShiftN → 输入 ShortestResponseLoadBalance → 回车 → CtrlF → 输入 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 内置了ShortestResponseLoadBalanceShortestResponseLoadBalance.java#L40-L99核心公式estimateResponse succeededAverageElapsed × (active 1)两个维度相乘succeededAverageElapsed历史平均成功响应时间——反映这个 Provider 处理够不够快active 1当前并发数 1——反映还有多少个请求在排队如果一个 Provider 冷缓存导致响应慢succeededAverageElapsed高即使active低相乘后的estimateResponse也不会成为最低——不会被打满。设计对比LeastActive: getActive()← 只问几个在路上单一维度不感知处理能力 ShortestResponse: succeededAverageElapsed ×(active1)← 问还要等多久双维度估算综合负载 负载均衡不是谁空闲给谁而是谁能最快处理完给谁。 LeastActive 理解成前者ShortestResponse 实现了后者。不这么写的后果回到LeastActiveLoadBalance.doSelect()L95-L97if(leastCount1){returninvokers.get(leastIndexes[0]);}这段代码没有考虑权重。即使新节点处于 warmup 期getWeight返回 10 而非 100只要它的 active 是唯一最低值直接返回warmup 权重形同虚设。对比来看如果有多个节点同权active 相同走的是 L99-L113 的加权随机——那里会用到getWeight的 warmup 权重。但冷启动场景恰恰是新节点的 active 远低于其他节点走的是唯一最低分支warmup 机制被跳过了。选型决策策略判断依据冷启动行为异构集群默认开启Random权重概率均匀由 weight 控制需手动配 weight✅RoundRobin权重轮询自动慢启动权重累积偏差✅LeastActiveactive 并发数新节点被打满活性反转✅ShortestResponse响应时间×并发自动抑制慢节点自适应❌ 需指定ConsistentHash参数哈希缓存倾斜节点变更重哈希❌ 需指定LeastActive 是 Dubbo 2.7.x 的默认负载均衡策略之一与 Random 并列常用但默认不意味着通用。选 LeastActive 的前提是各 Provider 的处理能力均衡且请求耗时不悬殊。一旦出现冷缓存、异构硬件、长请求 短请求混合场景active 单一维度就会失效。【收获】排查锚点下次看到一台 Provider 负载异常高 配置了 LeastActive按以下顺序排查两步验证法Step1: Consumer 端是否配置了 actives 搜索项目中的 DubboReference(actives...)或 xml 的dubbo:referenceactives...。 如果没配 actives -ActiveLimitFilter 未启用 LeastActive加权随机所有 active 为0。 那不关负载均衡的事——查其他原因。 Step2: 确认 LeastActive 的选择倾斜 在 LeastActiveLoadBalance.doSelect()第63行设断点 观察三台 Provider 的 active 值。 如果新节点 active 始终最低且leastCount1-走第95行直接返回权重被跳过。修复方案// 方案 A换 ShortestResponseDubboReference(actives10,loadbalanceshortestresponse)// 方案 B保留 LeastActive 手动配 weight// 新节点 weight 降低旧节点 weight 提高// 但注意 warmup 阶段的 weight 在 leastCount1 时无效// 所以方案 B 不彻底// 方案 C异构集群直接用 Random 显式 weightDubboReference(loadbalancerandom)// Provider 端配置 weight// dubbo:provider weight50/ // 弱小机器// dubbo:provider weight200/ // 强大机器排查锚点速查异常现象入口类关键方法排查优先级某台 Provider 流量异常偏高LeastActiveLoadBalancedoSelect()L63 getActive1️⃣ 先确认 active 值差异active 值为 0 且所有节点同值ActiveLimitFilterinvoke()L55 beginCount2️⃣ 检查 actives 是否配置warmup 不生效LeastActiveLoadBalancedoSelect()L95-L973️⃣ leastCount1 跳过权重自动规避慢节点ShortestResponseLoadBalancedoSelect()L63 estimateResponse4️⃣ 换策略验证自检清单□ 你的 Consumer 端配置了 actives 吗 —— 没有则LeastActiveRandom排查方向错了 □ 各 Provider 的处理能力/硬件规格一致吗 —— 不一致且开 LeastActive → 快的节点被打满 □ 服务有长请求短请求混合吗 —— 有混合 → LeastActive 失效风险高 □ 你知道 LeastActive 和 ShortestResponse 的区别吗 —— active 单维度 vs 响应时间×并发双维度 □ warmup 保护被跳过了吗 ——leastCount1时完全不看权重“LeastActive 的 active 计数器不是负载指示器——它只告诉你此时此刻有几个请求在路上不告诉你每个请求要跑多久。一个计数器不够的加一个平均响应时间。”下篇我们聊 Dubbo 与高版本 Spring 集成时的兼容性问题。