dubbo负载均衡原理解析

AbstractLoadBalance中的有四个实现

RandomLoadBalance:随机
LeastActiveLoadBalance:最小活跃数
RoundRobinLoadBalance:加权轮询
ConsistentHashLoadBalance:一致性hash

底层父类,其中getWeight当启动时间小于预热的时间会调用calculateWarmupWeight方法会重新计算权重,防止由于刚启动是因为权重太大导致的高负荷运行,dubbo优化的一种方式

public abstract class AbstractLoadBalance implements LoadBalance {

    static int calculateWarmupWeight(int uptime, int warmup, int weight) {
        // 运行时间 / 预热时间  防止刚启动 负载就大量增加
        //换算成数据公式: weight(设置的权重) * uptime(运行时间)/warmup(预热时间)
        //小计:预热时间越长,重新计算的权重就越接近设置的权重
        int ww = (int) ((float) uptime / ((float) warmup / (float) weight));
        return ww < 1 ? 1 : (ww > weight ? weight : ww);
    }

    @Override
    public <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) {
        if (invokers == null || invokers.isEmpty())
            return null;
        if (invokers.size() == 1)
            return invokers.get(0);
        //调用具体的负载策略
        return doSelect(invokers, url, invocation);
    }

    protected abstract <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation);

    //获取权重
    protected int getWeight(Invoker<?> invoker, Invocation invocation) {
        //从url中获取权重
        int weight = invoker.getUrl().getMethodParameter(invocation.getMethodName(), Constants.WEIGHT_KEY, Constants.DEFAULT_WEIGHT);
        if (weight > 0) {
            //获取时间戳
            long timestamp = invoker.getUrl().getParameter(Constants.REMOTE_TIMESTAMP_KEY, 0L);
            if (timestamp > 0L) {
                //运行时间
                int uptime = (int) (System.currentTimeMillis() - timestamp);
                //预热时间
                int warmup = invoker.getUrl().getParameter(Constants.WARMUP_KEY, Constants.DEFAULT_WARMUP);
                //运行时间小于预热时间
                if (uptime > 0 && uptime < warmup) {
                    weight = calculateWarmupWeight(uptime, warmup, weight);
                }
            }
        }
        return weight;
    }

}

RandomLoadBalance随机负载

RandomLoadBalance随机负载,假设我们有三台机器权重分别是【5,3,2】,三台机器的区间[0-5),[5-8),[8-10)随意一个长度在0-10的数值,假设随机数是6,那么就会落在第二个区间,也就是第二台机器上

public class RandomLoadBalance extends AbstractLoadBalance {

    public static final String NAME = "random";

    private final Random random = new Random();

    @Override
    protected <T> Invoker<T> doSelect(List<Invoker<T>> 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?
        //随机负载  所有的权重相加 例如 【A.B,C】 三台机器 权重【5,3,2】 总权重是10 [0-5)服务A [5-8)服务B 。。。。随机数
        for (int i = 0; i < length; i++) {
            int weight = getWeight(invokers.get(i), invocation);
            totalWeight += weight; // Sum
            if (sameWeight && i > 0
                    && weight != getWeight(invokers.get(i - 1), invocation)) {
                sameWeight = false;
            }
        }
        if (totalWeight > 0 && !sameWeight) {
            // If (not every invoker has the same weight & at least one invoker's weight>0), select randomly based on totalWeight.
            int offset = random.nextInt(totalWeight);
            // Return a invoker based on the random value.
            for (int i = 0; i < length; i++) {
                //获取的随机数依次减去每台的权重,如果为负数就是该台机器的invoker
                //三台机器 权重【5,3,2】 总权重是10 [0-5)服务A [5-8)服务B
                //例如随机数是7 7-5 = 2 > 0, 2 - 3 = -1 < 0,应该选择第二个
                offset -= getWeight(invokers.get(i), invocation);
                if (offset < 0) {
                    return invokers.get(i);
                }
            }
        }
        // If all invokers have the same weight value or totalWeight=0, return evenly.
        return invokers.get(random.nextInt(length));
    }

}

LeastActiveLoadBalance

LeastActiveLoadBalance,最小活跃数负载均衡,活跃数也就是dubbo的连接数,每当收到一个请求活跃数+1,结束请求活跃数-1,假设如果多台机器的连接数是相同的,如果一台机器性能比较好,处理请求比较快那么活跃数减少的就快,活跃数就少。所以活跃数少的就会获取到的请求会变多,这样就可以合理的使用性能不同的机器了。dubbo在最小活跃数的基础上加上了权重的配置,当有活跃数相同的配置时候,通过权重来进行选择

/**
 * LeastActiveLoadBalance
 * 最小活跃数负载
 */
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) {
        //总共的invoker数目
        int length = invokers.size(); // Number of invokers
        //最小的活跃数
        int leastActive = -1; // The least active value of all invokers
        //相同 最小活跃数 统计
        int leastCount = 0; // The number of invokers having the same least active value (leastActive)
        //记录最小活跃数 invoker角标
        int[] leastIndexs = new int[length]; // The index of invokers having the same least active value (leastActive)
        //权重总和
        int totalWeight = 0; // The sum of with warmup weights
        //第一个权重
        int firstWeight = 0; // Initial value, used for comparision
        boolean sameWeight = true; // Every invoker has the same weight value?
        for (int i = 0; i < length; i++) {
            Invoker<T> invoker = invokers.get(i);
            //获取活跃数,ExecuteLimitFilter活跃数处理逻辑
            
            int active = RpcStatus.getStatus(invoker.getUrl(), invocation.getMethodName()).getActive(); // Active number
            //疑问一     获取权重,为什么不能直接获取url中的权重呢??
            //int weight = invoker.getUrl().getMethodParameter(invocation.getMethodName(), Constants.WEIGHT_KEY, Constants.DEFAULT_WEIGHT);        
            int afterWarmup = getWeight(invoker, invocation); // Weight
            if (leastActive == -1 || active < leastActive) { // Restart, when find a invoker having smaller least active value.
                leastActive = active; // Record the current least active value
                leastCount = 1; // Reset leastCount, count again based on current leastCount
                leastIndexs[0] = i; // Reset
                totalWeight = afterWarmup; // Reset
                firstWeight = afterWarmup; // Record the weight the first invoker
                sameWeight = true; // Reset, every invoker has the same weight value?
            } else if (active == leastActive) { // If current invoker's active value equals with leaseActive, then accumulating.
                leastIndexs[leastCount++] = i; // Record index number of this invoker
                totalWeight += afterWarmup; // Add this invoker's weight to totalWeight.
                // If every invoker has the same weight?
                if (sameWeight && i > 0
                        && afterWarmup != firstWeight) {
                    sameWeight = false;
                }
            }
        }
        // assert(leastCount > 0)
        if (leastCount == 1) {
            // If we got exactly one invoker having the least active value, return this invoker directly.
            return invokers.get(leastIndexs[0]);
        }
        if (!sameWeight && totalWeight > 0) {
            // If (not every invoker has the same weight & at least one invoker's weight>0), select randomly based on totalWeight.
            //疑问二    想想这里的+1有什么用处呢?????
            int offsetWeight = random.nextInt(totalWeight) + 1;
            // Return a invoker based on the random value.
            for (int i = 0; i < leastCount; i++) {
                int leastIndex = leastIndexs[i];
                offsetWeight -= getWeight(invokers.get(leastIndex), invocation);
                if (offsetWeight <= 0)
                    return invokers.get(leastIndex);
            }
        }
        // If all invokers have the same weight value or totalWeight=0, return evenly.
        return invokers.get(leastIndexs[random.nextInt(leastCount)]);
    }
}

1、获取invoker的活跃数

2、如果有多个的invoker拥有这个最小活跃数,我们需要将权重取出,权重相加,判断权重是否相同,如果不同,sameWeight置为false

3、如果只有一个最小活跃数则直接返回

4、如果权重不相同,获取invoker与RandomLoadBalance相同的逻辑

5、如果权重相同随机一个invoker

现在有两个问题,

疑问一:获取权重,为什么不能直接获取url中的权重呢??(int afterWarmup = getWeight(invoker, invocation);)这是经过预热处理过的权重,为什么不能直接获取配置的权重呢?

​​​​​​offsetWeight是所有的权重总和,getWeight是降权之后的权重,这个权重小于设置的权重,如果totalWeight是所有的设置权重的总和的话,红框中的if就永远不能成立,导致选不到invoker,详细可以看一下dubbo的issue:https://github.com/apache/dubbo/issues/904

疑问二:int offsetWeight = random.nextInt(totalWeight) + 1;这里问什么要+1呢?

如果不+1,假设活跃数相同的invoker的权重是5,3,1,这个offsetWeight随机数是[0-9),最大值是8,8-5-3=0,跳出循环,权重为1的invoker永远不会执行

RoundRobinLoadBalance

RoundRobinLoadBalance,加权轮询,假设有三台机器A、B、C,轮询的含义就是第一次请求是A,那么第二次请求就是B,第三次请求就是C,加权轮询的含义就是,假设A、B、C的权重是5,3,1,假设有9次请求,那么A要获取到5次请求,B要获取到3次请求,C要获取到1次请求,一起see see代码

public class RoundRobinLoadBalance extends AbstractLoadBalance {
    public static final String NAME = "roundrobin";
    
    private static int RECYCLE_PERIOD = 60000;
    
    protected static class WeightedRoundRobin {
        private int weight;
        private AtomicLong current = new AtomicLong(0);
        private long lastUpdate;
        public int getWeight() {
            return weight;
        }
        public void setWeight(int weight) {
            this.weight = weight;
            current.set(0);
        }
        public long increaseCurrent() {
            return current.addAndGet(weight);
        }
        public void sel(int total) {
            current.addAndGet(-1 * total);
        }
        public long getLastUpdate() {
            return lastUpdate;
        }
        public void setLastUpdate(long lastUpdate) {
            this.lastUpdate = lastUpdate;
        }
    }

    private ConcurrentMap<String, ConcurrentMap<String, WeightedRoundRobin>> methodWeightMap = new ConcurrentHashMap<String, ConcurrentMap<String, WeightedRoundRobin>>();
    private AtomicBoolean updateLock = new AtomicBoolean();
    
    /**
     * get invoker addr list cached for specified invocation
     * <p>
     * <b>for unit test only</b>
     * 
     * @param invokers
     * @param invocation
     * @return
     */
    protected <T> Collection<String> getInvokerAddrList(List<Invoker<T>> invokers, Invocation invocation) {
        String key = invokers.get(0).getUrl().getServiceKey() + "." + invocation.getMethodName();
        Map<String, WeightedRoundRobin> map = methodWeightMap.get(key);
        if (map != null) {
            return map.keySet();
        }
        return null;
    }
    
    @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最小值
        long maxCurrent = Long.MIN_VALUE;
        long now = System.currentTimeMillis();
        Invoker<T> selectedInvoker = null;
        WeightedRoundRobin selectedWRR = null;
        for (Invoker<T> invoker : invokers) {
            String identifyString = invoker.getUrl().toIdentityString();
            WeightedRoundRobin weightedRoundRobin = map.get(identifyString);
            int weight = getWeight(invoker, invocation);
            if (weight < 0) {
                weight = 0;
            }
            if (weightedRoundRobin == null) {
                weightedRoundRobin = new WeightedRoundRobin();
                weightedRoundRobin.setWeight(weight);
                map.putIfAbsent(identifyString, weightedRoundRobin);
                weightedRoundRobin = map.get(identifyString);
            }
            //当前权重不等于缓存中的权重,将weight设置成当前权重
            if (weight != weightedRoundRobin.getWeight()) {
                //weight changed
                weightedRoundRobin.setWeight(weight);
            }
            //甚至当前权重 5 3 1 可以权重的分配逻辑处理
            long cur = weightedRoundRobin.increaseCurrent();
            //设置最新时间
            weightedRoundRobin.setLastUpdate(now);
            //当前权重与最大值比较
            if (cur > maxCurrent) {
                maxCurrent = cur;
                selectedInvoker = invoker;
                selectedWRR = weightedRoundRobin;
            }
            //计算总权重
            totalWeight += weight;
        }
        //移除1分钟没动过的
        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();
                        if (now - item.getValue().getLastUpdate() > RECYCLE_PERIOD) {
                            it.remove();
                        }
                    }
                    methodWeightMap.put(key, newMap);
                } finally {
                    updateLock.set(false);
                }
            }
        }
        if (selectedInvoker != null) {
            selectedWRR.sel(totalWeight);
            return selectedInvoker;
        }
        // should not happen here
        return invokers.get(0);
    }

}

1、获取缓存中的WeightedRoundRobin,不存在重新创建

2、获取权重,如果权重小于0重新置为0

3、比较关键的一步就是long cur = weightedRoundRobin.increaseCurrent();获取当前缓存中的current值,所有的权重求和

4、校验invoker是否有过掉线或者删除的操作,删除无效invoker

5、选出current最大的那个invoker,将这个invoker的current减去权重总和

举例说明:A、B、C三者的权重分别是5 3 1,下面每一轮结束,进行下一轮的操作的时候,cur的值为上一轮的current + weight,代码是这一行:

long cur = weightedRoundRobin.increaseCurrent();
public long increaseCurrent() {
    return current.addAndGet(weight);
}

第一轮:cur【5,3,1】选择服务器A,经过第五步current变为【-4,3,1】

第二轮 cur【1,6,2】选择服务器B,经过第五步current变为【1,-3,1】

第三轮 cur【6,0,3】选择服务器A,经过第五步current变为【-3,0,3】

第四轮 cur【2,3,4】选择服务器C,经过第五步current变为【2,3 ,-5】

第五轮 cur【7,6,-4】选择服务器A,经过第五步current变为【-2,6,-4】

第六轮 cur【3,9,-3】选择服务器B,经过第五步current变为【 3,0,-3】

第七轮 cur【8,3,-2】选择服务器A,经过第五步current变为【-1,3,-2】

第八轮 cur【4,6,-1】选择服务器B,经过第五步current变为【4,-3,-1】

第九轮 cur【9,0,0】选择服务器A,经过第五步current变为【0,0,0】

以上是2.6.5修改之后的版本,感兴趣可以看看以前的版本实现的问题,https://github.com/apache/dubbo/issues/2578

ConsistentHashLoadBalance

一致性 hash 算法:感兴趣的可以看看一致性hash算法,这个就不详细描述了https://www.cnblogs.com/williamjie/p/9477852.html,dubbo

public class ConsistentHashLoadBalance extends AbstractLoadBalance {

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

    @SuppressWarnings("unchecked")
    @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;
        int identityHashCode = System.identityHashCode(invokers);
        ConsistentHashSelector<T> selector = (ConsistentHashSelector<T>) selectors.get(key);
        if (selector == null || selector.identityHashCode != identityHashCode) {
            selectors.put(key, new ConsistentHashSelector<T>(invokers, methodName, identityHashCode));
            selector = (ConsistentHashSelector<T>) selectors.get(key);
        }
        return selector.select(invocation);
    }

    private static final class ConsistentHashSelector<T> {

        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();
            this.replicaNumber = url.getMethodParameter(methodName, "hash.nodes", 160);
            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++) {
                    byte[] digest = md5(address + i);
                    for (int h = 0; h < 4; h++) {
                        long m = hash(digest, h);
                        virtualInvokers.put(m, invoker);
                    }
                }
            }
        }

        public Invoker<T> select(Invocation invocation) {
            String key = toKey(invocation.getArguments());
            byte[] digest = md5(key);
            return selectForKey(hash(digest, 0));
        }

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

        private Invoker<T> selectForKey(long hash) {
            Map.Entry<Long, Invoker<T>> entry = virtualInvokers.tailMap(hash, true).firstEntry();
            if (entry == null) {
                entry = virtualInvokers.firstEntry();
            }
            return entry.getValue();
        }

        private long hash(byte[] digest, int number) {
            return (((long) (digest[3 + number * 4] & 0xFF) << 24)
                    | ((long) (digest[2 + number * 4] & 0xFF) << 16)
                    | ((long) (digest[1 + number * 4] & 0xFF) << 8)
                    | (digest[number * 4] & 0xFF))
                    & 0xFFFFFFFFL;
        }

        private byte[] md5(String value) {
            MessageDigest md5;
            try {
                md5 = MessageDigest.getInstance("MD5");
            } catch (NoSuchAlgorithmException e) {
                throw new IllegalStateException(e.getMessage(), e);
            }
            md5.reset();
            byte[] bytes;
            try {
                bytes = value.getBytes("UTF-8");
            } catch (UnsupportedEncodingException e) {
                throw new IllegalStateException(e.getMessage(), e);
            }
            md5.update(bytes);
            return md5.digest();
        }

    }

}

首先是对参数进行 md5 以及 hash 运算,得到一个 hash 值。然后再拿这个值到 TreeMap 中查找目标 Invoker 即可。

欢迎关注作者公众号交流以及投稿

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

技术王老五

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

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

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

打赏作者

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

抵扣说明:

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

余额充值