dubbo源码:dubbo中的负载均衡


###1. 概要

dubbo中支持的负载均衡有以下4种,

  • **RandomLoadBalance:**随机负载均衡,用户可设置接口的"weight"属性配置来调整权重加大随机概率,默认值
  • RoundRobinLoadBalance:轮询调度负载均衡,该策略于2018-10-26日被优化,优化后的策略先以权重最高的provider为调度对象,然后通过不断的减轻现有调用的当前权重来达到了轮询的目的
  • **LeastActiveLoadBalance:**最少活跃调用数负载均衡,相同活跃数根据所有Provider生成随机权重来选择Invoker
  • **ConsistentHashLoadBalance:**一致性hash负载均衡

常用的配置方式如下:

<dubbo:reference id="demoService" interface="com.alibaba.dubbo.demo.DemoService" loadbalance="roundrobin" weight="90"/>

<dubbo:service interface="com.alibaba.dubbo.demo.DemoService" ref="demoService" loadbalance="roundrobin"/>

2.介绍

2.1 RandomLoadBalance
public class RandomLoadBalance extends AbstractLoadBalance {

    public static final String NAME = "random";

    @Override
    protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
        int length = invokers.size(); // Invoker个数
        boolean sameWeight = true; // 每个Invoker是否具有同样的权重
        int[] weights = new int[length]; // 每个Invoker的权重放在weights数组中
      
        int firstWeight = getWeight(invokers.get(0), invocation); // 第一个Invoker的权重
        weights[0] = firstWeight;
        int totalWeight = firstWeight; // totalWeight表示所有Invoker的权重总和,初始化为第一个Invoker的权重
        for (int i = 1; i < length; i++) {
            int weight = getWeight(invokers.get(i), invocation);
            weights[i] = weight; // 第i个Invoker的权重
            totalWeight += weight; // 累加
            if (sameWeight && weight != firstWeight) {
                sameWeight = false;
            }
        }
      
        // 若所有的 Invoker 有不相同的权重值 && totalWeight>0,则基于totalWeight随机选择Invoker
        if (totalWeight > 0 && !sameWeight) {
            int offset = ThreadLocalRandom.current().nextInt(totalWeight);
            for (int i = 0; i < length; i++) {
                offset -= weights[i];
                if (offset < 0) {
                    return invokers.get(i);
                }
            }
        }
        // 如果所有的Invoker都具有相同的权重值 or totalWeight=0, 随机返回一个.
        return invokers.get(ThreadLocalRandom.current().nextInt(length));
    }
}

由上知,若Invoker中有不同的权重,则进行以下流程,否则随机选择一个Invoker

**Step 1:**从[0,totalWeight ]之间生成一个伪随机数 offset

**Step 2:**遍历所有的invoker,若其权重减去 offset 小于0,则返回

注:权重默认值100

2.2 RoundRobinLoadBalance
public class RoundRobinLoadBalance extends AbstractLoadBalance {
  
  //.......
  
  protected static class WeightedRoundRobin {
    private int weight;
    private AtomicLong current = new AtomicLong(0);
    private long lastUpdate;
    //......
  }
  
  //......
  
  @Override
  protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
    
    String key = invokers.get(0).getUrl().getServiceKey() + "." + invocation.getMethodName();
    
    ConcurrentMap<String, WeightedRoundRobin> map = methodWeightMap.get(key);
    if (map == null) {
      methodWeightMap.putIfAbsent(key, new ConcurrentHashMap<String, WeightedRoundRobin>());
      map = methodWeightMap.get(key);
    }
    
    
    int totalWeight = 0;
    long maxCurrent = Long.MIN_VALUE;
    long now = System.currentTimeMillis();
    Invoker<T> selectedInvoker = null;
    WeightedRoundRobin selectedWRR = null;
    
    // 找出权重最大的 Invoker,每个 Invoker 维持一个 WeightedRoundRobin ,每一次调用该方法就会把当前的Invoker持有的 WeightedRoundRobin 中的weight加倍
    for (Invoker<T> invoker : invokers) {
      String identifyString = invoker.getUrl().toIdentityString();
      WeightedRoundRobin weightedRoundRobin = map.get(identifyString);
      int weight = getWeight(invoker, invocation);
      
      if (weightedRoundRobin == null) {
        weightedRoundRobin = new WeightedRoundRobin();
        weightedRoundRobin.setWeight(weight);
        map.putIfAbsent(identifyString, weightedRoundRobin);
      }
      
      if (weight != weightedRoundRobin.getWeight()) {
        //weight changed
        weightedRoundRobin.setWeight(weight);
      }
      
      long cur = weightedRoundRobin.increaseCurrent();
      weightedRoundRobin.setLastUpdate(now);
      if (cur > maxCurrent) {
        maxCurrent = cur;
        selectedInvoker = invoker;
        selectedWRR = weightedRoundRobin;
      }
     
      totalWeight += weight; // 累加
    }
    
    // 更新 methodWeightMap 过程:创建新map,遍历 newMap 若当前的 Invoker 中的更新时间距离现在已经超过一次循环周期(默认60000毫秒,即60秒)就从newMap中移除,从而达到更新 methodWeightMap 的目的(对 <identifyString, WeightedRoundRobin> 进行检查,过滤掉长时间未被更新的节点。该节点可能挂了,invokers 中不包含该节点,所以该节点的 lastUpdate 长时间无法被更新。若未更新时长超过阈值后,就会被移除掉)
    if (!updateLock.get() && invokers.size() != map.size()) {
      if (updateLock.compareAndSet(false, true)) {
        try {
          // copy -> modify -> update reference
          ConcurrentMap<String, WeightedRoundRobin> newMap = new ConcurrentHashMap<String, WeightedRoundRobin>();
          newMap.putAll(map);
          Iterator<Entry<String, WeightedRoundRobin>> it = newMap.entrySet().iterator();
          while (it.hasNext()) {
            Entry<String, WeightedRoundRobin> item = it.next();
            // 若当前的 Invoker 中的更新时间距离现在已经超过一次循环周期,移出,则上面for循环进行筛选时就会重新对当前的 Invoker 生成 WeightedRoundRobin
            if (now - item.getValue().getLastUpdate() > RECYCLE_PERIOD) {
              it.remove();
            }
          }
          methodWeightMap.put(key, newMap);
        } finally {
          updateLock.set(false);
        }
      }
    }
    
    if (selectedInvoker != null) {
      // 选中Invoker后会把当前Invoker生成的WeightedRoundRobin当前权重减去总权重
      selectedWRR.sel(totalWeight);
      return selectedInvoker;
    }
    
    // should not happen here
    return invokers.get(0);
  }
  
  //......
}

分析一:很多文章上说轮询负载均衡有个缺点:

轮询负载均衡算法也有不足的地方,存在慢的 Provider 累积请求的问题,比如:第二台机器很慢,但没挂,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上,导致整个系统变慢。

以上说法是对于旧版本的轮询负载均衡有道理的,上面版本的已经解决了这个问题,该版本的轮询负载均衡已经脱离了调用次数的限制,通过不断的减轻现有调用的当前权重来达到了轮询的目的。举例如下:

当前机器有3个provider,P1、P2、P3,权重分别是10,100,20

第一次请求

  • P1 WeightedRoundRobin [weight=10, current=10]
  • P2 WeightedRoundRobin [weight=100, current=100]
  • P3 WeightedRoundRobin [weight=20, current=20]

选中P2,totalWeight=130,执行完 P2变成WeightedRoundRobin [weight=100, current=-30]

第二次请求

  • P1 WeightedRoundRobin [weight=10, current=20]
  • P2 WeightedRoundRobin [weight=100, current=70]
  • P3 WeightedRoundRobin [weight=20, current=40]

选中P2,totalWeight=130,执行完 P2变成WeightedRoundRobin [weight=100, current=-60]

第三次请求

  • P1 WeightedRoundRobin [weight=10, current=30]
  • P2 WeightedRoundRobin [weight=100, current=40]
  • P3 WeightedRoundRobin [weight=20, current=60]

选中P3,totalWeight=130,执行完 P3变成WeightedRoundRobin [weight=20, current=-70]

由上可见,虽然P2权重比较大,但经过每一次调用后,P2的当前权重逐渐变小,这样权重较小的provide有了被调用的机会

分析二:在更新 methodWeightMap 过程中,什么时候会出现源码中的 invokers.size() != map.size()

该节点可能挂了,invokers 中不包含该节点,所以该节点的 lastUpdate 长时间无法被更新。若未更新时长超过阈值后,就会被移除掉

2.3 LeastActiveLoadBalance
public class LeastActiveLoadBalance extends AbstractLoadBalance {
  
  public static final String NAME = "leastactive";
  
  @Override
  protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
    int length = invokers.size();  // provider个数
    int leastActive = -1; // 所有invoker的最小活跃值
    int leastCount = 0; //最小活跃数的 Invoker 的个数
    int[] leastIndexes = new int[length]; //最小活跃数的 Invoker 的索引
    
    int[] weights = new int[length]; // 每个invoker的权重
    int totalWeight = 0;
    int firstWeight = 0;
    boolean sameWeight = true; // 每个Invoker是否具有同样的权重
    
    
    for (int i = 0; i < length; i++) {
      Invoker<T> invoker = invokers.get(i);
      int active = RpcStatus.getStatus(invoker.getUrl(), invocation.getMethodName()).getActive();
      
      int afterWarmup = getWeight(invoker, invocation);
      weights[i] = afterWarmup;
      
      if (leastActive == -1 || active < leastActive) {
        leastActive = active;
        leastCount = 1;
        leastIndexes[0] = i;
        
        totalWeight = afterWarmup;
        firstWeight = afterWarmup;
        sameWeight = true;
        
      } else if (active == leastActive) {
        leastIndexes[leastCount++] = i;
        totalWeight += afterWarmup;
        if (sameWeight && i > 0 && afterWarmup != firstWeight) {
          sameWeight = false;
        }
      }
    } // for
    
    // 如果具有最小活跃数的 Invoker 只有一个,直接返回该 Invoker
    if (leastCount == 1) {
      return invokers.get(leastIndexes[0]);  
    }
    
    // 如果最小活跃数的 Invoker 有多个,且权重不相等同时总权重大于0,这时随机生成一个权重,范围在 (0,totalWeight) 间内。最后根据随机生成的权重,来选择 Invoker。
    if (!sameWeight && totalWeight > 0) {
      int offsetWeight = ThreadLocalRandom.current().nextInt(totalWeight);
      for (int i = 0; i < leastCount; i++) {
        int leastIndex = leastIndexes[i];
        offsetWeight -= weights[leastIndex];
        if (offsetWeight < 0) {
          return invokers.get(leastIndex);
        }
      }
    }
    
    return invokers.get(leastIndexes[ThreadLocalRandom.current().nextInt(leastCount)]);
  }
  
}

源码比较清晰,不再解释。

2.4 ConsistentHashLoadBalance
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);
  }
  
  
  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);
          }
        }
      }
    }
    
    
    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();
    }
  }
  
}
2.5 参考

dubbo官方文档写得比较详细,且更新比较快,可以参考:http://dubbo.apache.org/zh-cn/docs/source_code_guide/loadbalance.html>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

bboyzqh

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值