第15章 Dubbo 负载均衡的设计与实现

image.png
  • LoadBalance:负载均衡 SPI 接口;
  • AbstractLoadBalance:负载均衡模板基类

提供了 “获取一个 Invoker(filtered) 的权重” 的方式:

  1. 获取当前Invoker设置的权重weight和预热时间warmup,并且计算启动至今时间uptime
  2. 如果uptime

eg. 假设设置的权重是 100, 预热时间 10min,

  • 第一分钟的时候:权重变为 (1/10)*100=10, 也就是承担 10/100 = 10% 的流量;
  • 第二分钟的时候:权重变为 (2/10)*100=20, 也就是承担 20/100 = 20% 的流量;
  • 第十分钟的时候:权重变为 (10/10)*100=100, 也就是承担 100/100 = 100% 的流量;
  • 超过十分钟之后(即 uptime>warmup,表示预热期过了,则直接返回 weight=100,不再计算)
  • RandomLoadBalance(默认):带权重的随机负载均衡器;
  1. 通过计算每一个 Invoker 的权重来计算总权重 totalWeight,并判断是否所有的 Invoker 都有相同的权重;
  2. 如果总权重=0或者所有的 Invoker 都有相同的权重,则直接随机获取;
  3. 如果总权重>0并且不是所有的 Invoker 都有相同的权重,则根据权重进行随机获取,算法如下:

假设有 4 个 Invoker,权重分别是1,2,3,4,则总权重是 1+2+3+4=10,说明每个 Invoker 被选中的概率为1/10,2/10,3/10,4/10。先随机生成一个 [0,10) 的值 index,比如 5,从前向后让 index 递减权重,直到差值<0,那么最后那个使差值<0的 Invoker 就是当前选择的 Invoker - RandomLoadBalance 采用该种算法

  1. 减第一个 Invoker,5-1=4>0,继续减
  2. 减第二个 Invoker,4-2=2>0,继续减
  3. 减第三个 Invoker,2-3<0,则获取第三个 Invoker
  • RoundRobinLoadBalance:带权重的轮询负载均衡器;
    平滑权重轮询算法:
    1. 每次做负载均衡时,遍历所有的服务端(Invoker)列表。对每个 Invoker,
      a) current = current + weight
      b) 计算总权重 totalWeight = totalWeight + weight
    1. 遍历完所有的 Invoker 后,current 最大的节点就是本次要选择的节点。最后,该节点的 current = current - totalWeight
  • LeastActiveLoadBalance:带权重的最小活跃数负载均衡器;
  • 需要与 ActiveLimitFilter 配合使用,后者用于记录当前客户端对当前 Invoker 的活跃数及其当前调用方法的活跃数。(注意:actives如果设置为0,则不会加载;设置为<0,只会记录活跃数,不会进行并发数限流;设置为>0,则会进行每个客户端的并发限制逻辑)
  • 总体步骤(都是针对当前客户端对指定 Invoker 的并发执行数)
  1. 初始化最小活跃数的 Invoker 列表:leastIndexs[]
    遍历所有的 Invoker,

1.1. 获取每一个 Invoker 的当前被调用方法的活跃数 active 及其权重;
1.2. 如果遍历到的 Invoker 是第一个遍历的 Invoker 或者有更小的活跃数的 Invoker,所有的计数清空,重新进行初始化;
1.3. 如果遍历到的 Invoker 的活跃数 active 与之前记录的 leastActive 相同,则将当前的 Invoker 记录到 leastIndexs[] 中
判断所有的 Invoker 是否都有相同的权重。

  1. 如果 leastIndexs[] 中只有一个值,则直接获取对应索引的 Invoker;否则按照 RandomLoadBalance 的逻辑进行选择:如果总权重=0或者所有的 Invoker 都有相同的权重,则直接随机获取;如果总权重>0并且不是所有的 Invoker 都有相同的权重,则根据权重进行随机获取。
  • ConsistentHashLoadBalance:一致性 Hash 负载均衡器(与权重无关):
  1. 组装 serviceKey.methodName => {group/}interface{:version}.methodName,获取或创建(第一次或者 invokers 发生了变化)该 serviceKey 对应的的 selector。
    为每一个Invoker创建160个虚拟节点,存储到 TreeMap 中:
  • key 的计算(需要将每个虚拟节点的 key 打散到整个环上:0 ~ 232)
  • value:当前遍历的 Invoker
  1. 根据请求参数值 invocation.getArguments() 使用 selector 获取 Invoker

a) 获取argumentIndex[](默认只是用第一个参数值,可配置)中指定的参数值,连接起来作为key
b) 对该key进行md5,得到长度为16的字节数组 byte[] digest,对 digest[0~3] 进行 hash
c) 从 TreeMap 中获取第一个 >= 该hash 的 Entry,如果没有获取到,则直接获取 TreeMap 的第一个 Entry 元素
d) 返回该 Entry 的 value 值,即 Invoker

一、LoadBalance

@SPI(RandomLoadBalance.NAME) // 默认是 random=RandomLoadBalance
public interface LoadBalance {
    /** 从 List 中选择一个 Invoker */
    @Adaptive("loadbalance")
     Invoker select(List> invokers, URL url, Invocation invocation) throws RpcException;
}

二、AbstractLoadBalance

public abstract class AbstractLoadBalance implements LoadBalance {
    @Override
    public  Invoker select(List> invokers, URL url, Invocation invocation) {
        if (invokers == null || invokers.isEmpty()) {
            return null;
        }
        // 1. 如果只有一个 Invoker,直接返回
        if (invokers.size() == 1) {
            return invokers.get(0);
        }
        // 2. 调用子类进行选择
        return doSelect(invokers, url, invocation);
    }

    /**
     * 子类重写的方法:真正选择 Invoker(filtered) 的方法
     */
    protected abstract  Invoker doSelect(List> invokers, URL url, Invocation invocation);

    /**
     * 获取一个 Invoker(filtered) 的权重
     * 1、获取当前Invoker设置的权重weight和预热时间warmup,并且计算启动至今时间uptime
     * 2、如果uptime invoker, Invocation invocation) {
        // 1. 获取当前Invoker设置的权重:weight=100(该值配置在provider端)
        //     全局:
        //     单个服务:
        int weight = invoker.getUrl().getMethodParameter(invocation.getMethodName(), Constants.WEIGHT_KEY, Constants.DEFAULT_WEIGHT);
        if (weight > 0) {
            // 2. 获取启动的时间点,该值服务启动时会存储在注册的URL上(timestamp):dubbo://10.213.11.98:20880/com.alibaba.dubbo.demo.DemoService?...×tamp=1565775720703&warmup=10000&weight=10
            long timestamp = invoker.getUrl().getParameter(Constants.REMOTE_TIMESTAMP_KEY, 0L);
            if (timestamp > 0L) {
                // 3. 计算启动至今时间
                int uptime = (int) (System.currentTimeMillis() - timestamp);
                // 4. 获取当前Invoker设置的预热时间,默认 warmup=10*60*1000=10min
                int warmup = invoker.getUrl().getParameter(Constants.WARMUP_KEY, Constants.DEFAULT_WARMUP); // 
                // 5. 如果没有过完预热时间,则计算预热权重
                if (uptime > 0 && uptime < warmup) {
                    weight = calculateWarmupWeight(uptime, warmup, weight);
                }
            }
        }
        return weight;
    }

    /**
     * 计算预热权重
     * 预热公式:uptime/warmup*weight => 启动至今时间/设置的预热总时间*权重
     */
    private int calculateWarmupWeight(int uptime, int warmup, int weight) {
        /**
         * eg. 设置的权重是 100, 预热时间 10min,
         * 第一分钟的时候:权重变为 (1/10)*100=10, 也就是承担 10/100 = 10% 的流量;
         * 第二分钟的时候:权重变为 (2/10)*100=20, 也就是承担 20/100 = 20% 的流量;
         * 第十分钟的时候:权重变为 (10/10)*100=100, 也就是承担 100/100 = 100% 的流量;
         * 超过十分钟之后(即 uptime>warmup,表示预热期过了,则直接返回 weight=100,不再计算)
         */
        int ww = (int) ((float) uptime / ((float) warmup / (float) weight));
        return ww < 1 ? 1 : (ww > weight ? weight : ww);
    }
}

预热和权重的设置

  • 配置在 provider 端,注册在 provider 的 URL 上:dubbo://10.213.11.98:20880/com.alibaba.dubbo.demo.DemoService?...&timestamp=1565775720703&warmup=10000&weight=10
  • consumer 服务发现时,会拉取该 URL,获取其参数,其中 timestamp 会用来计算启动至今时间 uptime。
全局:
单个服务:

三、RandomLoadBalance

  • 随机,按权重设置随机概率。
  • 在一个截面上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重。
三种使用姿势






    

public class RandomLoadBalance extends AbstractLoadBalance {
    public static final String NAME = "random";
    private final Random random = new Random();

    @Override
    protected  Invoker doSelect(List> invokers, URL url, Invocation invocation) {
        int length = invokers.size(); // Number of invokers
        int totalWeight = 0; // The sum of weights
        boolean sameWeight = true; // Every invoker has the same weight?
        for (int i = 0; i < length; i++) {
            // 计算每一个Invoker的权重
            int weight = getWeight(invokers.get(i), invocation);
            // 计算总权重
            totalWeight += weight; // Sum
            // 计算所有Invoker的权重是否相同
            // 判断方法:每次遍历一个Invoker,都与其前一个Invoker的权重作比较,如果不相等,则设置sameWeight=false,一旦sameWeight=false后,后续的遍历就不必再进行判断了
            if (sameWeight && i > 0 && weight != getWeight(invokers.get(i - 1), invocation)) {
                sameWeight = false;
            }
        }

        // 如果总权重>0&&不是所有的Invoker都有相同的权重,则根据权重进行随机获取
        // eg. 4个Invoker,权重分别是1,2,3,4,则总权重是1+2+3+4=10,说明每个Invoker被选中的概率为1/10,2/10,3/10,4/10。此时有两种算法可以实现带概率的选择:
        // 1. 想象有这样的一个数组 [1,2,2,3,3,3,4,4,4,4], 先随机生成一个[0,10)的值,比如5,该值作为数组的index,此时获取到的是3,即使用第三个Invoker
        // 2. 先随机生成一个[0,10)的值,比如5,从前向后让索引递减权重,直到差值<0,那么最后那个使差值<0的Invoker就是当前选择的Invoker,5-1-2-3<0,那么最终获取的就是第三个Invoker(Dubbo 使用了该算法)
        if (totalWeight > 0 && !sameWeight) {
            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);
                }
            }
        }
        // 如果所有的Invokers都有相同的权重 or 总权重=0,则直接随机获取
        return invokers.get(random.nextInt(length));
    }
}
  • 总体步骤:
  1. 通过计算每一个 Invoker 的权重来计算总权重 totalWeight,并判断是否所有的 Invoker 都有相同的权重;
  2. 如果总权重=0或者所有的 Invoker 都有相同的权重,则直接随机获取;
  3. 如果总权重>0并且不是所有的 Invoker 都有相同的权重,则根据权重进行随机获取,算法如下:
  • 根据权重进行随机获取,有两种算法:假设有 4 个 Invoker,权重分别是1,2,3,4,则总权重是 1+2+3+4=10,说明每个 Invoker 被选中的概率为1/10,2/10,3/10,4/10。此时有两种算法可以实现带概率的选择:
  • 构造这样的一个数组 a = [1,2,2,3,3,3,4,4,4,4], 先随机生成一个 [0,10) 的值,比如 5,该值作为数组的 index,此时获取到的是 a[5] = 3,即使用第三个 Invoker;(该算法占用空间大)
  • 先随机生成一个 [0,10) 的值 index,比如 5,从前向后让 index 递减权重,直到差值<0,那么最后那个使差值<0的 Invoker 就是当前选择的 Invoker - RandomLoadBalance 采用该种算法
  1. 减第一个 Invoker,5-1=4>0,继续减
  2. 减第二个 Invoker,4-2=2>0,继续减
  3. 减第三个 Invoker,2-3<0,则获取第三个 Invoker
  • 判断所有 Invoker 的权重是否相同,有两种算法:
  • 每次遍历一个 Invoker,都与其前一个 Invoker 的权重作比较,如果不相等,则设置 sameWeight=false,一旦 sameWeight=false 后,后续的遍历就不必再进行判断了 - RandomLoadBalance 采用这种;
  • 将所有的 Invoker 与第一个 Invoker 的权重作比较,如果都相等,则sameWeight=true,否则 sameWeight=false - LeastActiveLoadBalance 采用这种。

四、RoundRobinLoadBalance

  • 轮询,按公约后的权重设置轮询比率。
  • 存在慢的提供者累积请求的问题,比如:第二台机器很慢,但没挂,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上。
public class RoundRobinLoadBalance extends AbstractLoadBalance {
    public static final String NAME = "roundrobin";
    private static int RECYCLE_PERIOD = 60000;
    
    protected static class WeightedRoundRobin {
        private int weight; // Invoker的权重
        private AtomicLong current = new AtomicLong(0); // 同时被多个线程选中的权重之后,假设同时被4个线程选中,weight=100,那么current=400
        private long lastUpdate; // 用于缓存超时的判断
        ...
        // 增加一个权重值
        public long increaseCurrent() {
            return current.addAndGet(weight);
        }
        public void sel(int total) {
            current.addAndGet(-1 * total);
        }
        ...
    }

    /**
     * 外层 key: serviceKey.methodName
     * 内层 key: url = {protocol://username:password@ip:port/path?xx=yy&uu=ii}
     */
    private ConcurrentMap> methodWeightMap = new ConcurrentHashMap>();
    private AtomicBoolean updateLock = new AtomicBoolean();
    ...
    
    @Override
    protected  Invoker doSelect(List> invokers, URL url, Invocation invocation) {
        // 创建缓存 Map> methodWeightMap
        String key = invokers.get(0).getUrl().getServiceKey() + "." + invocation.getMethodName();
        ConcurrentMap map = methodWeightMap.get(key);
        if (map == null) {
            methodWeightMap.putIfAbsent(key, new ConcurrentHashMap());
            map = methodWeightMap.get(key);
        }
        int totalWeight = 0;
        long maxCurrent = Long.MIN_VALUE;
        long now = System.currentTimeMillis();
        Invoker selectedInvoker = null;
        WeightedRoundRobin selectedWRR = null;
        for (Invoker invoker : invokers) {
            // url = {protocol://username:password@ip:port/path?xx=yy&uu=ii}
            String identifyString = invoker.getUrl().toIdentityString();
            WeightedRoundRobin weightedRoundRobin = map.get(identifyString);
            // 获取当前 Invoker 的权重
            int weight = getWeight(invoker, invocation);
            if (weight < 0) {
                weight = 0;
            }
            // 创建 WeightedRoundRobin
            if (weightedRoundRobin == null) {
                weightedRoundRobin = new WeightedRoundRobin();
                weightedRoundRobin.setWeight(weight);
                map.putIfAbsent(identifyString, weightedRoundRobin);
                weightedRoundRobin = map.get(identifyString);
            }
            // 权重发生了变化,eg. 在预热期间,预热权重随时间发生变化
            if (weight != weightedRoundRobin.getWeight()) {
                //weight changed
                weightedRoundRobin.setWeight(weight);
            }
            // 1. 将weight加到current上:current=current+weight
            long cur = weightedRoundRobin.increaseCurrent();
            // 设置最后更新的时间
            weightedRoundRobin.setLastUpdate(now);
            // 2. 最后选出current最大的Invoker作为最终要调用的Invoker
            if (cur > maxCurrent) {
                maxCurrent = cur;
                selectedInvoker = invoker;
                selectedWRR = weightedRoundRobin;
            }
            // 计算总权重
            totalWeight += weight;
        }
        // 加锁做缓存清除操作:
        // invokers.size() != map.size() 说明 invokers 发生了变化(新增或下线)
        // 新增:下次循环会增加到map
        // 下线:只能靠如下的缓存清除策略从map中进行删除
        if (!updateLock.get() && invokers.size() != map.size()) {
            if (updateLock.compareAndSet(false, true)) {
                try {
                    // CopyOnWrite
                    // copy -> modify -> update reference
                    ConcurrentMap newMap = new ConcurrentHashMap();
                    newMap.putAll(map);
                    Iterator> it = newMap.entrySet().iterator();
                    while (it.hasNext()) {
                        Entry item = it.next();
                        // 如果该缓存已经有60s没有使用了,则清除
                        if (now - item.getValue().getLastUpdate() > RECYCLE_PERIOD) {
                            it.remove();
                        }
                    }
                    methodWeightMap.put(key, newMap);
                } finally {
                    updateLock.set(false);
                }
            }
        }
        // 返回current最大的Invoker作为最终要调用的Invoker
        if (selectedInvoker != null) {
            // 3. 当前的Invoker的current减去总权重:current=current-totalWeight
            selectedWRR.sel(totalWeight);
            return selectedInvoker;
        }
    }
}

平滑权重轮询算法:

    1. 每次做负载均衡时,遍历所有的服务端(Invoker)列表。对每个 Invoker,
      a) current = current + weight
      b) 计算总权重 totalWeight = totalWeight + weight
    1. 遍历完所有的 Invoker 后,current 最大的节点就是本次要选择的节点。最后,该节点的 current = current - totalWeight

举例说明:
eg. 假设有3个Invoker:A,B,C, 权重是1,2,3,调用如下,我们发现最后调用次数 A:B:C=1:2:3 与权重相符。而且A,B,C的调用也是穿插的(平滑权重轮询的好处,而普通权重轮询是会出现 [C,C,C,B,B,A] 这样短时间内不断调用同一个节点的问题-会导致该节点压力骤增)


image.png

五、LeastActiveLoadBalance

  • 最少活跃调用数,相同活跃数的随机,活跃数指调用前后计数差(通过 ActiveLimitFilter 计算每个接口方法的活跃数)
  • 使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大
三种使用姿势







    







    

注意

LeastActiveLoadBalance 需要与 ActiveLimitFilter 配合使用,后者用于记录当前客户端对当前 Invoker 的活跃数及其当前调用方法的活跃数。(注意:actives如果设置为0,则不会加载;设置为<0,只会记录活跃数,不会进行并发数限流;设置为>0,则会进行每个客户端的并发限制逻辑)

/**
 * LeastActiveLoadBalance
 *
 * 需要与ActiveLimitFilter配合使用(ActiveLimitFilter用于记录当前的Invoker的当前方法的活跃数active)
 * @see com.alibaba.dubbo.rpc.filter.ActiveLimitFilter
 */
public class LeastActiveLoadBalance extends AbstractLoadBalance {
    public static final String NAME = "leastactive";
    private final Random random = new Random();

    @Override
    protected  Invoker doSelect(List> invokers, URL url, Invocation invocation) {
        int length = invokers.size(); // Number of invokers
        int leastActive = -1; // 所有Invoker的最小活跃数
        int leastCount = 0; // 有最小活跃数leastActive的Invoker个数(leastCount=2,表示有两个Invoker其leastActive相同)
        int[] leastIndexs = new int[length]; // 存储leastActive的Invoker在List> invokers列表中的索引值
        int totalWeight = 0; // 总权重
        int firstWeight = 0; // 第一个被遍历的Invoker的权重,用于比较来计算是否所有的Invoker都有相同的权重
        boolean sameWeight = true; // 是否所有的Invoker都有相同的权重
        /**
         * 1、初始化最小活跃数的Invoker列表:leastIndexs[]
         *   遍历所有的Invoker,
         *   a) 获取每一个方法的活跃数active及其权重;
         *   b) 如果遍历到的Invoker是第一个遍历的Invoker或者有更小的活跃数的Invoker,所有的计数清空,重新进行初始化;
         *   c) 如果遍历到的Invoker的活跃数active与之前记录的leastActive相同,则将当前的Invoker记录到 leastIndexs[] 中
         *   判断所有的Invoker是否都有相同的权重。
         * 2、如果leastIndexs[]中只有一个值,则直接获取对应索引的Invoker;否则按照 RandomLoadBalance 的逻辑进行选择
         */
        for (int i = 0; i < length; i++) {
            Invoker invoker = invokers.get(i);
            // 获取当前Invoker当前方法的活跃数,该活跃数由 ActiveLimitFilter 进行记录
            int active = RpcStatus.getStatus(invoker.getUrl(), invocation.getMethodName()).getActive(); // Active number
            int afterWarmup = getWeight(invoker, invocation); // Weight
            if (leastActive == -1 || active < leastActive) { // 如果遍历的是第一个Invoker或者有更小的活跃数,所有的计数清空,重新进行初始化
                leastActive = active; // 记录最小活跃数
                leastCount = 1; // Reset leastCount, count again based on current leastCount
                leastIndexs[0] = i; // Reset
                totalWeight = afterWarmup; // Reset
                firstWeight = afterWarmup; // 记录第一个被遍历的Invoker的权重
                sameWeight = true; // Reset, every invoker has the same weight value?
            } else if (active == leastActive) { // 如果遍历到的Invoker的活跃数active与之前记录的leastActive相同
                leastIndexs[leastCount++] = i; // 则将当前的Invoker记录到 leastIndexs[] 中
                totalWeight += afterWarmup; // Add this invoker's weight to totalWeight.
                // 判断所有的Invoker是否都有相同的权重?
                if (sameWeight && i > 0 && afterWarmup != firstWeight) {
                    sameWeight = false;
                }
            }
        }
        if (leastCount == 1) {
            return invokers.get(leastIndexs[0]);
        }
        // 后续的逻辑与 RandomLoadBalance 相同
        if (!sameWeight && totalWeight > 0) {
            int offsetWeight = random.nextInt(totalWeight) + 1;
            for (int i = 0; i < leastCount; i++) {
                int leastIndex = leastIndexs[i];
                offsetWeight -= getWeight(invokers.get(leastIndex), invocation);
                if (offsetWeight <= 0) {
                    return invokers.get(leastIndex);
                }
            }
        }
        return invokers.get(leastIndexs[random.nextInt(leastCount)]);
    }
}
  • 总体步骤(都是针对当前客户端对指定 Invoker 的并发执行数)
  1. 初始化最小活跃数的 Invoker 列表:leastIndexs[]

遍历所有的 Invoker,
1.1. 获取每一个 Invoker 的当前被调用方法的活跃数 active 及其权重;
1.2. 如果遍历到的 Invoker 是第一个遍历的 Invoker 或者有更小的活跃数的 Invoker,所有的计数清空,重新进行初始化;
1.3. 如果遍历到的 Invoker 的活跃数 active 与之前记录的 leastActive 相同,则将当前的 Invoker 记录到 leastIndexs[] 中
判断所有的 Invoker 是否都有相同的权重。

  1. 如果 leastIndexs[] 中只有一个值,则直接获取对应索引的 Invoker;否则按照 RandomLoadBalance 的逻辑进行选择:如果总权重=0或者所有的 Invoker 都有相同的权重,则直接随机获取;如果总权重>0并且不是所有的 Invoker 都有相同的权重,则根据权重进行随机获取。

最后看下 ActiveLimitFilter 的相关逻辑:(关于 ActiveLimitFilter 后续进行分析)

/**
 * 1. 仅用于 consumer 端
 * 2. 需要配置 actives 参数
 *    <0,只记录活跃数(并发度)
 *    >0, 记录活跃数(并发度)+ 限流(限制每个客户端的并发执行数)
 */
@Activate(group = Constants.CONSUMER, value = Constants.ACTIVES_KEY)
public class ActiveLimitFilter implements Filter {
    @Override
    public Result invoke(Invoker invoker, Invocation invocation) throws RpcException {
        URL url = invoker.getUrl();
        String methodName = invocation.getMethodName();
        // 获取最大并发数 actives=10
        int max = invoker.getUrl().getMethodParameter(methodName, Constants.ACTIVES_KEY, 0);
        // 获取当前调用方法的RpcStatus
        RpcStatus count = RpcStatus.getStatus(invoker.getUrl(), invocation.getMethodName());
        // 只有当配置的 actives>0,才会做并发度限流,否则只是简单的计数
        if (max > 0) {
            // 限流操作
            ...
        }
        long begin = System.currentTimeMillis();
        // 当前活跃数 + 1
        RpcStatus.beginCount(url, methodName);
        try {
            // 真正调用
            Result result = invoker.invoke(invocation);
            // 正常结束:当前活跃数-1
            RpcStatus.endCount(url, methodName, System.currentTimeMillis() - begin, true);
            return result;
        } catch (RuntimeException t) {
            // 发生异常:当前活跃数-1,抛出异常
            RpcStatus.endCount(url, methodName, System.currentTimeMillis() - begin, false);
            throw t;
        }
    }
}

六、ConsistentHashLoadBalance

关于一致性 hash 的介绍及其优点,见 https://www.cnblogs.com/java-zhao/p/5158034.html

  • 一致性 Hash,相同参数的请求总是发到同一提供者。
  • 当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。(https://www.cnblogs.com/java-zhao/p/5158034.html)
  • 缺省只对第一个参数值 hash,如果要修改,请配置 (表示对前两个参数值进行 hash)
  • 缺省用 160 份虚拟节点,如果要修改,请配置
/**
 * ConsistentHashLoadBalance
 * 1. 组装 serviceKey.methodName => {group/}interface{:version}.methodName,获取或创建(第一次或者invokers发生了变化)该 serviceKey 对应的的 selector
 *   为每一个Invoker创建160个虚拟节点,存储到 TreeMap 中
 *   key的计算(需要将key打散): 对于每一个Invoker,
 *   a) 40次外层循环:md5(ip:port+i)(i=[0~39]),此时生成一个长度为16的字节数组 byte[] digest
 *   b) 4层内层循环:分别对 digest 按照每四个字节(h=0=>digest[0~3],h=1=>digest[4~7],...)进行hash(公式:hash(md5(ip:port+i),h)(h=[0~3])),计算出4个不同的Long,该值作为TreeMap的key
 *   value:当前遍历的 Invoker
 *
 * 2. 根据请求参数值 invocation.getArguments() 使用 selector 获取 Invoker
 *   a) 获取argumentIndex[](默认只是用第一个参数值,可配置)中指定的参数值,连接起来作为key
 *   b) 对该key进行md5,得到长度为16的字节数组 byte[] digest,对digest[0~3]进行hash
 *   c) 从 TreeMap 中获取第一个 >= 该hash 的Entry,如果没有获取到,则直接获取 TreeMap 的第一个 Entry 元素
 *   d) 返回该 Entry 的 value 值,即 Invoker
 */
public class ConsistentHashLoadBalance extends AbstractLoadBalance {
    /**
     * key: serviceKey.methodName => {group/}interface{:version}.methodName
     */
    private final ConcurrentMap> selectors = new ConcurrentHashMap>();

    @SuppressWarnings("unchecked")
    @Override
    protected  Invoker doSelect(List> invokers, URL url, Invocation invocation) {
        // 1. 获取方法名
        String methodName = RpcUtils.getMethodName(invocation);
        // 2. 组装key:serviceKey.methodName => {group/}interface{:version}.methodName
        String key = invokers.get(0).getUrl().getServiceKey() + "." + methodName;
        // 3. 计算 invokers 的 hash 值
        int identityHashCode = System.identityHashCode(invokers);
        // 4. 根据 key 获取相应的 selector 实例
        ConsistentHashSelector selector = (ConsistentHashSelector) selectors.get(key);
        // 5. 如果是第一次创建selector || invokers已经发生了变化(宕机或者添加了新的机器 - 此时identityHashCode发生了变化),
        //    则新建selector,并存储到缓存中
        if (selector == null || selector.identityHashCode != identityHashCode) {
            selectors.put(key, new ConsistentHashSelector(invokers, methodName, identityHashCode));
            selector = (ConsistentHashSelector) selectors.get(key);
        }
        // 6. 根据请求参数invocation使用selector获取Invoker(一致性hash:相同的请求参数值会请求到同一台机器Invoker上去)
        return selector.select(invocation);
    }

    /**
     * 该 selector 是针对 key:serviceKey.methodName => {group/}interface{:version}.methodName 的存储器
     */
    private static final class ConsistentHashSelector {
        /**
         * 虚拟节点
         */
        private final TreeMap> virtualInvokers;
        /**
         * 虚拟节点数,默认是 160
         * 
         *     
         * 
         */
        private final int replicaNumber;
        /**
         * 默认只对第一个参数进行hash,配置 hash.arguments=0,1,表示对前两个参数进行hash
         * 
         *     
         * 
         */
        private final int[] argumentIndex;
        /**
         * 如上key的全部Invoker列表的hash值
         * 如果发生了变化,说明新添加了如上key的机器Invoker或者有如上key的机器Invoker下线,此时需要重建selector
         */
        private final int identityHashCode;

        ConsistentHashSelector(List> invokers, String methodName, int identityHashCode) {
            this.virtualInvokers = new TreeMap>();
            this.identityHashCode = identityHashCode;
            URL url = invokers.get(0).getUrl();
            // 默认的虚拟节点分片数为 160
            this.replicaNumber = url.getMethodParameter(methodName, "hash.nodes", 160);
            // 默认只对第一个参数进行 hash,配置 hash.arguments=0,1
            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 invoker : invokers) {
                // 1. 获取真实节点: ip:port
                String address = invoker.getUrl().getAddress();
                // 2. 生成虚拟节点
                for (int i = 0; i < replicaNumber / 4; i++) {
                    // 2.1. 对ip:port+递增数字做md5 -> 4个虚拟节点的总标识digest(16字节长度)
                    byte[] digest = md5(address + i);
                    for (int h = 0; h < 4; h++) {
                        // 2.2. 再对digest进行hash(每四位)得到最终的每个虚拟节点的标识m,m作为TreeMap的key,Invoker作为value,存储起来
                        long m = hash(digest, h);
                        virtualInvokers.put(m, invoker);
                    }
                }
            }
        }

        public Invoker select(Invocation invocation) {
            // 1. 获取argumentIndex中指定的参数值,连接起来作为key
            String key = toKey(invocation.getArguments());
            // 2. 对参数值连接key做md5
            byte[] digest = md5(key);
            // 3. 对digest[0~3]进行hash,然后进行选择
            return selectForKey(hash(digest, 0));
        }
        
        // 获取 argumentIndex[](默认只是用第一个参数值,可配置)中指定的参数值,连接起来作为 key
        private String toKey(Object[] args) {
            StringBuilder buf = new StringBuilder();
            for (int i : argumentIndex) {
                if (i >= 0 && i < args.length) {
                    buf.append(args[i]);
                }
            }
            return buf.toString();
        }

        // TreeMap 是有序的树形结构。
        // 1. 首先获取至少大于或者等于当前key的Entry - 即完成顺时针查找的目的
        // 2. 如果没有找到,则直接获取第一个Entry
        private Invoker selectForKey(long hash) {
            Map.Entry> entry = virtualInvokers.tailMap(hash, true).firstEntry();
            if (entry == null) {
                entry = virtualInvokers.firstEntry();
            }
            return entry.getValue();
        }

        // number=0,对 digest[0~3] 进行 hash
        // number=1,对 digest[4~7] 进行 hash
        // number=2,对 digest[8~11] 进行 hash
        // number=3,对 digest[12~15] 进行 hash
        private long hash(byte[] digest, int number) {
            ...
        }
        
        //  返回 16 个字节长度的字节数组
        private byte[] md5(String value) {
            ...
        }
    }
}
  • 总体步骤
  1. 组装 serviceKey.methodName => {group/}interface{:version}.methodName,获取或创建(第一次或者 invokers 发生了变化)该 serviceKey 对应的的 selector。
    为每一个Invoker创建160个虚拟节点,存储到 TreeMap 中:
  • key 的计算(需要将 key 打散): 对于每一个 Invoker,
  • a) 40次外层循环:md5(ip:port+i)(i=[0~39]),此时生成一个长度为16的字节数组 byte[] diges
  • b) 4次内层循环:分别对 digest 按照每四个字节(h=0=>digest[0 ~ 3],h=1=>digest[4 ~ 7],...)进行hash(公式:hash(md5(ip:port+i),h)(h=[0~3])),计算出4个不同的Long,该值作为TreeMap的key
  • value:当前遍历的 Invoker
  1. 根据请求参数值 invocation.getArguments() 使用 selector 获取 Invoker

a) 获取argumentIndex[](默认只是用第一个参数值,可配置)中指定的参数值,连接起来作为key
b) 对该key进行md5,得到长度为16的字节数组 byte[] digest,对 digest[0~3] 进行 hash
c) 从 TreeMap 中获取第一个 >= 该hash 的 Entry,如果没有获取到,则直接获取 TreeMap 的第一个 Entry 元素
d) 返回该 Entry 的 value 值,即 Invoker

你可能感兴趣的:(第15章 Dubbo 负载均衡的设计与实现)