跳至正文
来两杯美式
返回

Dubbo 负载均衡:四种策略与源码分析

By 来两杯美式
发布于

上一篇 《Dubbo 注册中心与直连调试:只订阅、只注册、多注册中心》 讲了怎么连服务。这一篇讲流量分发:一个服务有多个 Provider 节点时,Consumer 把请求发给谁?——这就是 Dubbo 的负载均衡策略(LoadBalance)。

四种策略一览

Dubbo 内置四种负载均衡策略,接口 LoadBalanceselect 方法负责从 List<Invoker> 中选出一个:

策略类名原理特点
按权重随机RandomLoadBalance按权重随机概率默认策略,最常用
轮询RoundRobinLoadBalance按顺序轮流均摊,但有慢节点时会”打满”
最少活跃数LeastActiveLoadBalance活跃数最少的优先自动避让慢节点
一致性哈希ConsistentHashLoadBalance相同参数落同一节点无状态节点缓存友好
<!-- 消费方指定负载均衡策略 -->
<dubbo:reference id="userService" interface="com.example.UserService" loadbalance="leastactive"/>

<!-- 注解方式 -->
@Reference(loadbalance = "leastactive")
private UserService userService;

Random:按权重随机(默认)

假设有 4 个集群节点 A、B、C、D,对应的权重分别是 1、2、3、4,总权重为 10(1+2+3+4)。怎么做到按权重随机呢?

根据 10 随机出一个整数,然后依次和权重相减:

随机数为 2:
2 - A的权重(1) = 1
1 - B的权重(2) = -1  ← 变成负数,命中 B
随机数为 7:
7 - A(1) = 6
6 - B(2) = 4
4 - C(3) = 1
1 - D(4) = -3  ← 命中 D

权重越大,被随机命中的区间越大——这就是”按权重随机”的本质:把总权重当成一条线段,随机落点落在哪个节点的区间,就选谁。

源码分析

RandomLoadBalance 源码:按权重随机与均等随机

protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
    int length = invokers.size();       // 总个数
    int totalWeight = 0;                // 总权重
    boolean sameWeight = true;          // 权重是否都一样
    for (int i = 0; i < length; i++) {
        int weight = getWeight(invokers.get(i), invocation);
        totalWeight += weight;          // 累计总权重
        if (sameWeight && i > 0
                && weight != getWeight(invokers.get(i - 1), invocation)) {
            sameWeight = false;         // 计算所有权重是否一样
        }
    }
    if (totalWeight > 0 && !sameWeight) {
        // 如果权重不相同且权重大于 0,则按总权重数随机
        int offset = random.nextInt(totalWeight);
        // 并确定随机值落在哪个片段上
        for (int i = 0; i < length; i++) {
            offset -= getWeight(invokers.get(i), invocation);
            if (offset < 0) {
                return invokers.get(i);
            }
        }
    }
    // 如果权重相同或权重为 0,则均等随机
    return invokers.get(random.nextInt(length));
}

算法三步:

  1. 第一遍循环:累加总权重 totalWeight,同时判断所有节点权重是否相同。
  2. 权重不同random.nextInt(totalWeight) 生成 [0, totalWeight) 的随机数,依次减去各节点权重,第一个使 offset 变负的节点被选中
  3. 权重相同或为 0:退化为 random.nextInt(length) 的均等随机。

权重通过 getWeight() 获取,默认权重 100;可以在注册中心动态调整节点权重(如给新扩容的机器临时调低权重做灰度)。

RoundRobin:轮询

按顺序轮流调用每个 Provider,权重高的节点被轮到的次数多。

节点 A(权重1) B(权重2)
第 1 次调用 → A
第 2 次调用 → B
第 3 次调用 → B
第 4 次调用 → A
第 5 次调用 → B
第 6 次调用 → B

优点:请求均匀分布,实现简单。 缺点如果某节点处理慢,请求会排队堆积——因为轮询不管节点当前忙不忙,照样平均分配。这是它后来被很多场景弃用的原因。

LeastActive:最少活跃数

每个服务有一个活跃计数器。假如有 A、B 两个提供者,计数均为 0。当 A 提供者开始处理请求,该计数 +1(此时 A 还没处理完);处理完后计数 -1。而 B 请求接收后处理得很快,B 处理完时 A 还没处理完,所以此时 A、B 的计数为 1、0。

那么当有新的请求来的时候,就会选择 B 提供者(B 的活跃计数比 A 小)

节点     活跃数      状态
A        1          正在处理一个慢请求
B        0          空闲
→ 新请求选择 B(活跃数最小)

活跃数 = 正在处理中的请求数。活跃数越少说明节点越闲,新请求优先打给最闲的节点——天然避让慢节点

源码分析

LeastActiveLoadBalance 源码:最小活跃数 + 权重随机

protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
    int length = invokers.size();       // 总个数
    int leastActive = -1;               // 最小的活跃数
    int leastCount = 0;                 // 相同最小活跃数的个数
    int[] leastIndexes = new int[length]; // 相同最小活跃数的下标
    int totalWeight = 0;                // 总权重
    int firstWeight = 0;                // 第一个权重,用于计算是否相同
    boolean sameWeight = true;          // 是否所有权重相同

    for (int i = 0; i < length; i++) {
        Invoker<T> invoker = invokers.get(i);
        int active = RpcStatus.getStatus(invoker.getUrl(),
                invocation.getMethodName()).getActive();  // 活跃数
        int weight = invoker.getUrl().getMethodParameter(
                invocation.getMethodName(),
                Constants.WEIGHT_KEY, Constants.DEFAULT_WEIGHT); // 权重
        if (leastActive == -1 || active < leastActive) {
            // 发现更小的活跃数,重新开始
            leastActive = active;       // 记录最小活跃数
            leastCount = 1;             // 重新统计相同最小活跃数的个数
            leastIndexes[0] = i;        // 重新记录最小活跃数下标
            totalWeight = weight;       // 重新累计总权重
            firstWeight = weight;       // 记录第一个权重
            sameWeight = true;          // 还原权重相同标识
        } else if (active == leastActive) {
            // 累计相同最小的活跃数
            leastIndexes[leastCount++] = i;  // 累计相同最小活跃数下标
            totalWeight += weight;           // 累计总权重
            if (sameWeight && i > 0
                    && weight != firstWeight) {
                sameWeight = false;          // 判断所有权重是否一样
            }
        }
    }
    if (leastCount == 1) {
        // 如果只有一个最小则直接返回
        return invokers.get(leastIndexes[0]);
    }
    if (!sameWeight && totalWeight > 0) {
        // 如果权重不相同且权重大于 0,则按总权重数随机
        int offsetWeight = random.nextInt(totalWeight);
        for (int i = 0; i < leastCount; i++) {
            int leastIndex = leastIndexes[i];
            offsetWeight -= getWeight(invokers.get(leastIndex), invocation);
            if (offsetWeight <= 0)
                return invokers.get(leastIndex);
        }
    }
    // 如果权重相同或权重为 0,则均等随机
    return invokers.get(leastIndexes[random.nextInt(leastCount)]);
}

算法逻辑分五步:

  1. 遍历所有节点:通过 RpcStatus.getStatus(...).getActive() 拿到每个节点的活跃数,同时取权重。
  2. 找出最小活跃数active < leastActive 时更新最小值;active == leastActive 时把下标收进 leastIndexes 数组。
  3. 只有一个最小活跃节点:直接返回它。
  4. 多个最小活跃节点且权重不同:在最小活跃节点内部,按权重随机(复用 Random 的线段算法)。
  5. 权重相同或为 0:在最小活跃节点内部均等随机。

关键点:LeastActive 是两级选择——第一级找最闲的节点,第二级在同样闲的节点里按权重挑。笔记里的例子”A、B 权重 1、2,则 A 概率 1/3、B 概率 2/3”说的就是第二级。

ConsistentHash:一致性哈希

一致性哈希解决的是”相同参数的请求固定打到同一节点”——对无状态的节点缓存(如本地缓存)特别友好。

原理(摘自当年笔记):

假如有 N 个真实节点,把每个真实节点映射成 M 个虚拟节点,再把 M×N 个虚拟节点散列在圆环上。各真实节点对应的虚拟节点相互交错分布,这样某真实节点 down 后,则把其影响平均分担到其他所有节点上。

也就是 a、b、c、d 的虚拟节点 a0,a1,a2b0,b1,b2c0,c1,c2d0,d1,d2 散落在圆环上。假设 C 号节点 down,则 c0,c1,c2 的压力分别传给 d0,a1,b1,如下图:

一致性哈希环:C 节点宕机后其虚拟节点压力转移到相邻节点

一致性哈希的流程

1. 每个真实节点生成 M 个虚拟节点(如每个节点 160 个)
2. 所有虚拟节点按哈希值散列到圆环(0 ~ 2^32-1)上
3. 请求参数做同样的哈希,落到环上某点
4. 顺时针找到第一个虚拟节点,即目标节点
5. 节点 down 时,只有其环上"后继区间"的请求受影响
   (转移给下一个虚拟节点),其他节点不受影响

为什么需要虚拟节点

没有虚拟节点时,真实节点在环上分布不均匀,会导致数据倾斜(某些节点负载过高)。引入虚拟节点后:

参数维度

一致性哈希默认按第一个参数做哈希(hash.arguments 可配置),相同参数的请求会落到同一个节点:

<dubbo:reference interface="com.example.UserService"
                 loadbalance="consistenthash"/>

典型场景:getUserById(userId) 这类按 ID 查询的接口,同一 userId 的请求固定打到同一节点,可以利用该节点上的本地缓存,减少数据库压力。

四种策略选型建议

场景推荐策略理由
默认、通用场景Random权重随机,简单可靠,默认就是它
节点处理速度差异大LeastActive自动避让慢节点,防止请求堆积
有本地缓存、按 key 查询ConsistentHash相同 key 固定同节点,缓存命中率高
无状态、要求绝对均匀RoundRobin轮询均摊(注意慢节点问题)

小结

下一篇 《Dubbo 集群容错:架构、五种模式与服务暴露》 讲服务调用失败时怎么办。


分享这篇文章:
通过邮件分享这篇文章✓ 链接已复制
所属专题
Dubbo
第 3 / 5 篇
查看系列全部文章
  1. 01.Dubbo 入门:什么是 RPC 与服务治理?
  2. 02.Dubbo 注册中心与直连调试:只订阅、只注册、多注册中心
  3. 03.Dubbo 负载均衡:四种策略与源码分析
  4. 04.Dubbo 集群容错:架构、五种模式与服务暴露
  5. 05.Dubbo 服务拆分与架构设计:子系统划分的四大原则

上一篇
Dubbo 集群容错:架构、五种模式与服务暴露
下一篇
Dubbo 注册中心与直连调试:只订阅、只注册、多注册中心