Davids原理探究:Dubbo负载均衡实现原理

文章目录

    • Dubbo负载均衡实现原理
      • 负载均衡概述
      • 负载均衡特性
      • 负载均衡逻辑过程
      • 负载均衡总体结构
      • 负载均衡具体实现
        • RandomRobin 随机负载均衡(default)
        • RoundRobin 轮询负载均衡
        • LeastActive 最少活跃调用数负载均衡
        • 一致性Hash负载均衡

Dubbo负载均衡实现原理

Davids原理探究:Dubbo负载均衡实现原理_第1张图片

关注可以查看更多粉丝专享blog~

负载均衡概述

Dubbo在使用负载均衡的时候并没有直接使用LoadBalance,而是使用的抽象父类AbstractClusterInvoker中定义的Invoker select方法。因为抽象父类在LoadBalance的基础上有风装了一些新的特性。

负载均衡特性

  1. 粘滞连接。Dubbo中有一种特性叫粘滞连接。

粘滞连接用于有状态服务,尽可能让客户端总是向同一提供者发起调用,除非该提供者“挂了”,再连接另一台。
粘滞连接将自动开启延迟连接,以减少长连接数

  1. 可用检测。Dubbo调用的URL中,如果含有cluster.availablecheck=false,则不会检测远程服务是否可用,直接调用。如果不设置,则会默认开启检查,对所有的服务都做是否可用检查,如果不可用,则再次做负载均衡。
  2. 避免重复调用。对于已经调用过的远程服务,避免重复选择,每次都是用同一个节点。这种特性主要是为了避免并发场景下,某个节点瞬间接收大量请求。

负载均衡逻辑过程

  1. 检查URL中是否有配置粘滞连接,如果有则使用粘滞连接的Invoker。如果没有配置粘滞连接,或者重复调用检测不通过、可用检测不通过,则进入第2步。
  2. 通过ExtensionLoader获取负载均衡的具体实现,并通过负载均衡做节点的选择。对选择出来的节点做重复调用、可用性检测,通过则直接返回,否则进入第3步。
  3. 进行节点重新选择。如果需要做可用性检测会遍历Directory中得到的所有节点,过滤不可用的和已经调用过的节点,在剩余的节点中做负载均衡;如果不需要做可用性检测,那么也会遍历Directory中得到的所有节点,但只过滤已经调用过的,在剩余的节点中重新做负载均衡。这里存在一种情况,就是在过滤不可用或已经调用过的节点时,节点全部被过滤,没有剩余节点,则进入第4步。
  4. 遍历所有已经调用过的节点,选出所有可用的节点,再通过负载均衡选出一个节点并返回。如果还找不到可用节点,则返回null。

负载均衡总体结构

Dubbo内置了4种负载均衡,也可以自行扩展,因为LoadBalance接口上有@SPI注解。

@SPI(RandomLoadBalance.NAME)
public interface LoadBalance {

    @Adaptive("loadbalance")
    <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException;

}

从代码中可以看出默认的负载均衡实现就是RandomLoadBalance,即随机负载均衡。由于select方法上有@Activate(“loadbalance”)注解,因此我们可以在URL中可以通过loadalance=xxx来动态指定select时的负载均衡算法。

负载均衡算法名称 效果说明
Random LoadBalance 随机,按权重设置随机概率。在一个节点上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者的权重
RoundRobin LoadBalance 轮询,按公约后的权重设置比例。存在慢的提供者累积请求的问题。比如:某台机器很慢,但没“挂”,当请求到第二台时就卡在那里,久而久之,所有的请求都卡在那里
LeastActive LoadBalance 最少活跃调用数,如果活跃数量相同则随机调用,活跃数指调用前后的计数差(调用前计数器+1,调用后计数器-1,如果一个服务提供者计数差很大,则说明该服务处理的比较慢,则减少请求量,防止服务请求阻塞,以及服务端崩溃。)。使慢的提供者收到更少的请求,因为越慢的提供者的调用前后计数差会越大
ConsistentHash LoadBalance 一致性Hash,相同参数的请求总是发到同一提供者。当某台提供者“挂”掉的时候,原本发往该提供者的请求,基于虚拟节点,会平摊到其他提供者,不会引发剧烈变动。默认根据只对第一个参数hash,可以通过配置进行修改。默认使用160个虚拟节点,可以通过配置进行修改。

负载均衡具体实现

RandomRobin 随机负载均衡(default)
  1. 计算权重并判断每个Invoker的权重是否一样。遍历整个Invoker列表,求和总权重。在遍历过程中,会对比每个Invoker的权重,判断所有Invoker的权重是否相同。
  2. 如果权重相同,则说明每个Invoker的概率都一样,因此直接使用nextInt随机选一个Invoker返回即可。
  3. 如果权重不同,则首先得到偏移值,然后根据偏移值找到对应的Invoker,代码如下:
// 根据总权重计算一个随机的偏移量
int offset = ThreadLocalRandom.current().nextInt(totalWeight);
// 遍历所有Invoker,累减,得到被选中的Invoker
for (int i = 0; i < length; i++) {
	// 累减的目的是如果轮询到某一个服务的时候累减完小于0,说明随机值落在此区间,则返回该服务提供者的Invoker
    offset -= weights[i];
    if (offset < 0) {
        return invokers.get(i);
    }
}
RoundRobin 轮询负载均衡

普通轮询的好处是每个节点获得的请求会很均匀,如果某些节点能力明显较弱,则这个节点会堆积比较多的请求。因此普通轮询还不能满足需求,还需要能根据节点权重进行干预。权重轮询又分为普通权重轮询和平滑权重轮询。普通权重轮询会造成某个节点会突然被频繁选中,这样容易让一个节点流量暴增。Dubbo的轮询负载均衡采用的是平滑加权轮询算法,负载均衡时挑选最大权重返回,并将最大权重数值进行调整(最大权重新值 = 最大权重当前值 - 总权重),然后开启下一轮,开启下一轮时,所有权重新值 = 当前值 + 自身基础权重值,听上去有点懵,但是列个表就清楚了。

请求次数 被选中前Invoker的current值 被选中后Invoker的current值 被选中的节点
1 {1,6,9} {1,6,-7} C
2 {2,12,2} {2,-4,2} B
3 {3,2,11} {3,2,-5} C
4 {4,8,4} {4,-8,4} B
5 {5,-2,13} {5,-2,-3} C
6 {6,4,6} {-10,4,6} A
7 {-9,10,15} {-9,10,-1} C
8 {-8,16,8} {-8,0,8} B
9 {-7,6,17} {-7,6,1} C
10 {-6,12,10} {-6,-4,10} B
11 {-5,2,19} {-5,2,3} C
12 {-4,8,12} {-4,8,-4} C
13 {-3,14,5} {-3,-2,5} B
14 {-2,4,14} {-2,4,-2} C
15 {-1,10,7} {-1,-6,7} B
16 {0,0,16} {0,0,0} C

跟着表格从上到下捋一遍,自己算一下,思路就很清晰了。从这16次负载均衡来看,我们可以很清楚得知,A刚好被调用1次,B刚好被调用6次,C刚好被调用9次。符合权重轮询策略,因为他们的权重比例是1:6:9。此外,C并没有频繁地的一直调用,其中会穿插B和A的调用。

LeastActive 最少活跃调用数负载均衡

最少活跃调用数,如果活跃数量相同则随机调用,活跃数指调用前后的计数差(调用前计数器+1,调用后计数器-1,如果一个服务提供者计数差很大,则说明该服务处理的比较慢,则减少请求量,防止服务请求阻塞,以及服务端崩溃。)。使慢的提供者收到更少的请求,因为越慢的提供者的调用前后计数差会越大

  1. 遍历 invokers 列表,寻找活跃数最小的 Invoker
  2. 如果有多个 Invoker 具有相同的最小活跃数,此时记录下这些 Invoker 在 invokers 集合中的下标,并累加它们的权重,比较它们的权重值是否相等
  3. 如果只有一个 Invoker 具有最小的活跃数,此时直接返回该 Invoker 即可
  4. 如果有多个 Invoker 具有最小活跃数,且它们的权重不相等,此时处理方式和 RandomLoadBalance 一致
  5. 如果有多个 Invoker 具有最小活跃数,但它们的权重相等,此时随机返回一个即可
public class LeastActiveLoadBalance extends AbstractLoadBalance {

    public static final String NAME = "leastactive";

    private final Random random = new Random();

    @Override
    protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
        int length = invokers.size();
        // 最小的活跃数
        int leastActive = -1;
        // 具有相同“最小活跃数”的服务者提供者(以下用 Invoker 代称)数量
        int leastCount = 0; 
        // leastIndexs 用于记录具有相同“最小活跃数”的 Invoker 在 invokers 列表中的下标信息
        int[] leastIndexs = new int[length];
        int totalWeight = 0;
        // 第一个最小活跃数的 Invoker 权重值,用于与其他具有相同最小活跃数的 Invoker 的权重进行对比,
        // 以检测是否“所有具有相同最小活跃数的 Invoker 的权重”均相等
        int firstWeight = 0;
        boolean sameWeight = true;

        // 遍历 invokers 列表
        for (int i = 0; i < length; i++) {
            Invoker<T> invoker = invokers.get(i);
            // 获取 Invoker 对应的活跃数
            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) {
            	// 使用当前活跃数 active 更新最小活跃数 leastActive
                leastActive = active;
                // 更新 leastCount 为 1
                leastCount = 1;
                // 记录当前下标值到 leastIndexs 中
                leastIndexs[0] = i;
                totalWeight = weight;
                firstWeight = weight;
                sameWeight = true;

            // 当前 Invoker 的活跃数 active 与最小活跃数 leastActive 相同 
            } else if (active == leastActive) {
            	// 在 leastIndexs 中记录下当前 Invoker 在 invokers 集合中的下标
                leastIndexs[leastCount++] = i;
                // 累加权重
                totalWeight += weight;
                // 检测当前 Invoker 的权重与 firstWeight 是否相等,
                // 不相等则将 sameWeight 置为 false
                if (sameWeight && i > 0
                    && weight != firstWeight) {
                    sameWeight = false;
                }
            }
        }
        
        // 当只有一个 Invoker 具有最小活跃数,此时直接返回该 Invoker 即可
        if (leastCount == 1) {
            return invokers.get(leastIndexs[0]);
        }

        // 有多个 Invoker 具有相同的最小活跃数,但它们之间的权重不同
        if (!sameWeight && totalWeight > 0) {
        	// 随机生成一个 [0, totalWeight) 之间的数字
            int offsetWeight = random.nextInt(totalWeight);
            // 循环让随机数减去具有最小活跃数的 Invoker 的权重值,
            // 当 offset 小于等于0时,返回相应的 Invoker
            for (int i = 0; i < leastCount; i++) {
                int leastIndex = leastIndexs[i];
                // 获取权重值,并让随机数减去权重值
                offsetWeight -= getWeight(invokers.get(leastIndex), invocation);
                if (offsetWeight <= 0)
                    return invokers.get(leastIndex);
            }
        }
        // 如果权重相同或权重为0时,随机返回一个 Invoker
        return invokers.get(leastIndexs[random.nextInt(leastCount)]);
    }
}
一致性Hash负载均衡

Dubbo一致性Hash负载均衡采用的是一致性Hash算法

doSelect 方法主要做了一些前置工作,比如检测 invokers 列表是不是变动过,以及创建 ConsistentHashSelector。这些工作做完后,接下来开始调用 ConsistentHashSelector 的 select 方法执行负载均衡逻辑。

// ConsistentHashLoadBalance#doSelect
public class ConsistentHashLoadBalance extends AbstractLoadBalance {

    private final ConcurrentMap<String, ConsistentHashSelector<?>> selectors = 
        new ConcurrentHashMap<String, ConsistentHashSelector<?>>();

    @Override
    protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
        String methodName = RpcUtils.getMethodName(invocation);
        String key = invokers.get(0).getUrl().getServiceKey() + "." + methodName;

        // 获取 invokers 原始的 hashcode
        int identityHashCode = System.identityHashCode(invokers);
        ConsistentHashSelector<T> selector = (ConsistentHashSelector<T>) selectors.get(key);
        // 如果 invokers 是一个新的 List 对象,意味着服务提供者数量发生了变化,可能新增也可能减少了。
        // 此时 selector.identityHashCode != identityHashCode 条件成立
        if (selector == null || selector.identityHashCode != identityHashCode) {
            // 创建新的 ConsistentHashSelector
            selectors.put(key, new ConsistentHashSelector<T>(invokers, methodName, identityHashCode));
            selector = (ConsistentHashSelector<T>) selectors.get(key);
        }

        // 调用 ConsistentHashSelector 的 select 方法选择 Invoker
        return selector.select(invocation);
    }
}

在分析 select 方法之前,先看一下一致性 hash 选择器 ConsistentHashSelector 的初始化过程

private static final class ConsistentHashSelector<T> {

    // 使用 TreeMap 存储 Invoker 虚拟节点
    private final TreeMap<Long, Invoker<T>> virtualInvokers;

    private final int replicaNumber;

    private final int identityHashCode;

    private final int[] argumentIndex;

    ConsistentHashSelector(List<Invoker<T>> invokers, String methodName, int identityHashCode) {
        this.virtualInvokers = new TreeMap<Long, Invoker<T>>();
        this.identityHashCode = identityHashCode;
        URL url = invokers.get(0).getUrl();
        // 获取虚拟节点数,默认为160
        this.replicaNumber = url.getMethodParameter(methodName, "hash.nodes", 160);
        // 获取参与 hash 计算的参数下标值,默认对第一个参数进行 hash 运算
        String[] index = Constants.COMMA_SPLIT_PATTERN.split(url.getMethodParameter(methodName, "hash.arguments", "0"));
        argumentIndex = new int[index.length];
        for (int i = 0; i < index.length; i++) {
            argumentIndex[i] = Integer.parseInt(index[i]);
        }
        for (Invoker<T> invoker : invokers) {
            String address = invoker.getUrl().getAddress();
            for (int i = 0; i < replicaNumber / 4; i++) {
                // 对 address + i 进行 md5 运算,得到一个长度为16的字节数组
                byte[] digest = md5(address + i);
                // 对 digest 部分字节进行4次 hash 运算,得到四个不同的 long 型正整数
                for (int h = 0; h < 4; h++) {
                    // h = 0 时,取 digest 中下标为 0 ~ 3 的4个字节进行位运算
                    // h = 1 时,取 digest 中下标为 4 ~ 7 的4个字节进行位运算
                    // h = 2, h = 3 时过程同上
                    long m = hash(digest, h);
                    // 将 hash 到 invoker 的映射关系存储到 virtualInvokers 中,
                    // virtualInvokers 需要提供高效的查询操作,因此选用 TreeMap 作为存储结构
                    virtualInvokers.put(m, invoker);
                }
            }
        }
    }
}

ConsistentHashSelector 的构造方法执行了一系列的初始化逻辑,比如从配置中获取虚拟节点数以及参与 hash 计算的参数下标,默认情况下只使用第一个参数进行 hash。需要特别说明的是,ConsistentHashLoadBalance 的负载均衡逻辑只受参数值影响,具有相同参数值的请求将会被分配给同一个服务提供者。ConsistentHashLoadBalance 不关心权重,因此使用时需要注意一下。

在获取虚拟节点数和参数下标配置后,接下来要做的事情是计算虚拟节点 hash 值,并将虚拟节点存储到 TreeMap 中。到此,ConsistentHashSelector 初始化工作就完成了。最后处理select逻辑。

select逻辑如下,选择的过程相对比较简单了。首先是对参数进行 md5 以及 hash 运算,得到一个 hash 值。然后再拿这个值到 TreeMap 中查找目标 Invoker 即可。

public Invoker<T> select(Invocation invocation) {
    // 将参数转为 key
    String key = toKey(invocation.getArguments());
    // 对参数 key 进行 md5 运算
    byte[] digest = md5(key);
    // 取 digest 数组的前四个字节进行 hash 运算,再将 hash 值传给 selectForKey 方法,
    // 寻找合适的 Invoker
    return selectForKey(hash(digest, 0));
}

private Invoker<T> selectForKey(long hash) {
    // 到 TreeMap 中查找第一个节点值大于或等于当前 hash 的 Invoker
    Map.Entry<Long, Invoker<T>> entry = virtualInvokers.tailMap(hash, true).firstEntry();
    // 如果 hash 大于 Invoker 在圆环上最大的位置,此时 entry = null,
    // 需要将 TreeMap 的头节点赋值给 entry
    if (entry == null) {
        entry = virtualInvokers.firstEntry();
    }

    // 返回 Invoker
    return entry.getValue();
}

相关文章:
Davids原理探究:Dubbo源码编译(2.7.8)
Davids原理探究:Dubbo SPI和Java SPI实现原理
Davids原理探究:Dubbo注册中心(ZooKeeper、Redis)实现原理
Davids原理探究:Dubbo配置解析原理
Davids原理探究:Dubbo服务暴露原理
Davids原理探究:Dubbo服务消费原理
Davids原理探究:Dubbo优雅停机原理解析
Davids原理探究:Dubbo调用流程图
Davids原理探究:Dubbo路由实现原理
Davids原理探究:Dubbo负载均衡实现原理
Davids原理探究:Dubbo过滤器原理

你可能感兴趣的:(Dubbo,微服务,Java)