乐于分享
好东西不私藏

从源码到原理:Dubbo Adaptive 负载均衡,为何是云原生首选?

从源码到原理:Dubbo Adaptive 负载均衡,为何是云原生首选?

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;}

关键依赖:

  1. getWeight:继承自 AbstractLoadBalance,计算节点权重(考虑预热、配置等);

  2. getTimeout:获取节点的方法级超时时间(优先级:方法配置 > 全局配置 > 默认值);

  3. 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)。

核心设计总结

模块
核心逻辑
节点选择
P2C 算法(随机选 2 个节点)+ 负载比较,兼顾性能与均衡性
负载计算
结合权重、超时时间、自适应指标(AdaptiveMetrics),多维评估节点负载
性能优化
服务标识缓存、ThreadLocalRandom 随机数,避免重复计算和多线程竞争
兼容性
复用 Dubbo 抽象负载均衡类、通用工具类(RpcUtils),遵循 Dubbo 配置规则

该实现的核心优势是轻量高效(仅比较两个随机节点),同时通过 AdaptiveMetrics 接入动态负载指标,适配服务节点的实时状态变化。