AdaptiveLoadBalance 是 Dubbo 中基于自适应负载指标的负载均衡实现,核心采用 P2C (Power of Two Choices) 算法选择低负载服务节点,以下拆解关键模块的具体实现:
一、核心结构与初始化
1. 类定义与核心属性
public class AdaptiveLoadBalance extends AbstractLoadBalance {public static final String NAME = "adaptive";// 默认负载指标维度(内存、负载)private String attachmentKey = "mem,load";// 自适应指标采集器,用于获取服务节点负载private final AdaptiveMetrics adaptiveMetrics;// 构造函数:从应用模型中获取AdaptiveMetrics实例public AdaptiveLoadBalance(ApplicationModel scopeModel) {adaptiveMetrics = scopeModel.getBeanFactory().getBean(AdaptiveMetrics.class);}}
继承 AbstractLoadBalance:复用 Dubbo 负载均衡抽象类的基础能力(如权重计算);
AdaptiveMetrics:核心依赖,用于采集 / 计算服务节点的负载值;
attachmentKey:默认标记负载维度(内存、CPU 负载)。
二、核心选择逻辑(doSelect)
doSelect 是负载均衡的入口方法,负责最终选择服务节点,流程如下:
@Overrideprotected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {// 1. 核心:通过P2C算法选择低负载节点Invoker<T> invoker = selectByP2C(invokers, invocation);// 2. 附加信息设置:标记负载均衡类型、请求开始时间等invocation.setAttachment(Constants.ADAPTIVE_LOADBALANCE_ATTACHMENT_KEY, attachmentKey);long startTime = System.currentTimeMillis();invocation.getAttributes().put(Constants.ADAPTIVE_LOADBALANCE_START_TIME, startTime);invocation.getAttributes().put(LOADBALANCE_KEY, LoadbalanceRules.ADAPTIVE);// 3. 指标埋点:记录消费端请求数、选择节点耗时adaptiveMetrics.addConsumerReq(getServiceKey(invoker, invocation));adaptiveMetrics.setPickTime(getServiceKey(invoker, invocation), startTime);return invoker;}
关键动作:
调用 selectByP2C 执行核心节点选择;
向 Invocation 中注入负载均衡标记、请求开始时间等元数据;
通过 AdaptiveMetrics 记录请求数、选择耗时等监控指标。
三、P2C 算法实现(selectByP2C)
P2C 核心逻辑:随机选两个节点,比较负载后选更低的那个,避免全量节点遍历,兼顾性能与均衡性:
private <T> Invoker<T> selectByP2C(List<Invoker<T>> invokers, Invocation invocation) {int length = invokers.size();// 1. 只有1个节点:直接返回if (length == 1) {return invokers.get(0);}// 2. 只有2个节点:直接比较两者负载if (length == 2) {return chooseLowLoadInvoker(invokers.get(0), invokers.get(1), invocation);}// 3. 多节点:随机选两个不重复的节点int pos1 = ThreadLocalRandom.current().nextInt(length);int pos2 = ThreadLocalRandom.current().nextInt(length - 1);if (pos2 >= pos1) {pos2 = pos2 + 1; // 避免pos2与pos1重复}// 4. 比较两个随机节点的负载,选更低的return chooseLowLoadInvoker(invokers.get(pos1), invokers.get(pos2), invocation);}
核心细节:
随机选点:通过 ThreadLocalRandom 生成随机索引,避免多线程竞争;
去重逻辑:pos2 >= pos1 时 pos2+1,确保两个随机索引不重复;
最终决策:委托 chooseLowLoadInvoker 比较两个节点的负载。
四、低负载节点选择(chooseLowLoadInvoker)
核心逻辑:计算两个节点的综合负载值,选择负载更低的节点,负载计算结合权重、超时时间、自适应指标:
private <T> Invoker<T> chooseLowLoadInvoker(Invoker<T> invoker1, Invoker<T> invoker2, Invocation invocation) {// 1. 获取两个节点的权重(继承自AbstractLoadBalance)int weight1 = getWeight(invoker1, invocation);int weight2 = getWeight(invoker2, invocation);// 2. 获取两个节点的超时时间(基于URL/方法配置)int timeout1 = getTimeout(invoker1, invocation);int timeout2 = getTimeout(invoker2, invocation);// 3. 计算综合负载值(通过AdaptiveMetrics),转换为long避免浮点数精度问题long load1 = Double.doubleToLongBits(adaptiveMetrics.getLoad(getServiceKey(invoker1, invocation), weight1, timeout1));long load2 = Double.doubleToLongBits(adaptiveMetrics.getLoad(getServiceKey(invoker2, invocation), weight2, timeout2));// 4. 负载相等时:按权重随机,权重和为0则纯随机if (load1 == load2) {int totalWeight = weight1 + weight2;if (totalWeight > 0) {int offset = ThreadLocalRandom.current().nextInt(totalWeight);return offset < weight1 ? invoker1 : invoker2;}return ThreadLocalRandom.current().nextInt(2) == 0 ? invoker1 : invoker2;}// 5. 负载不等:选负载更低的节点return load1 > load2 ? invoker2 : invoker1;}
关键依赖:
getWeight:继承自 AbstractLoadBalance,计算节点权重(考虑预热、配置等);
getTimeout:获取节点的方法级超时时间(优先级:方法配置 > 全局配置 > 默认值);
adaptiveMetrics.getLoad:核心负载计算逻辑(外部实现),输入「服务标识、权重、超时时间」,返回综合负载值。
五、服务标识生成(getServiceKey/buildServiceKey)
为每个节点生成唯一标识,用于 AdaptiveMetrics 关联负载数据:
private String getServiceKey(Invoker<?> invoker, Invocation invocation) {// 缓存复用:避免重复生成String key = (String) invocation.getAttributes().get(invoker);if (StringUtils.isNotEmpty(key)) {return key;}// 构建新标识:地址 + 协议服务键key = buildServiceKey(invoker, invocation);invocation.getAttributes().put(invoker, key);return key;}private String buildServiceKey(Invoker<?> invoker, Invocation invocation) {URL url = invoker.getUrl();StringBuilder sb = new StringBuilder(128);sb.append(url.getAddress()).append(":").append(invocation.getProtocolServiceKey());return sb.toString();}
设计目的:
缓存复用:将生成的标识存入 Invocation 属性,避免重复计算;
唯一性:地址(IP:Port) + 协议服务键 确保不同节点 / 服务的标识不重复。
六、超时时间获取(getTimeout)
精准获取方法级超时时间,影响负载计算:
private int getTimeout(Invoker<?> invoker, Invocation invocation) {URL url = invoker.getUrl();String methodName = RpcUtils.getMethodName(invocation);return (int)RpcUtils.getTimeout(url, methodName, RpcContext.getClientAttachment(), invocation, DEFAULT_TIMEOUT);}
优先级逻辑(Dubbo 通用规则):
方法级配置(method.xxx.timeout);
客户端附件(RpcContext);
全局配置(timeout);
默认值(DEFAULT_TIMEOUT,通常为 1000ms)。
核心设计总结
该实现的核心优势是轻量高效(仅比较两个随机节点),同时通过 AdaptiveMetrics 接入动态负载指标,适配服务节点的实时状态变化。
夜雨聆风