自適應負載均衡(Dubbo)

一、Dubbo框架模型

在這裏插入圖片描述

說明:

  • dubbo中有消費者Consumer,服務提供者Provider,註冊中心Registry,以及RPC調用和監控中心。
  • Consumer、Provider將URL信息以字符串的方式存儲在註冊中心,註冊中心採用發佈/訂閱模式。
  • Consumer、Provider與註冊中心採用單一長連接,使用心跳模式,Provider的變更(宕機,變更資源信息)會及時通過發佈/訂閱模式通知到訂閱者。
  • Consumer尋找服務的過程稱爲服務發現,Provider註冊URL到註冊中心的過程爲服務註冊,Consumer通過RPC調用服務的過程爲服務調用。

二、負載均衡定位

在這裏插入圖片描述

負載均衡功能:Consumer通過服務發現從註冊中心拉取服務列表(Dubbo消費端將服務封裝Invoker對象),Consumer在RPC調用服務之前,通過負載均衡從服務列表中選擇一臺服務器。

三、負載均衡接口

在這裏插入圖片描述
說明:dubbo提供負載均衡接口LoadBalance,開發者需要實現該接口,完成自己設計的select方法。
invokers:服務列表
invocation:Consumer調用服務器的傳輸的參數,方法名,封裝在Invocatin對象中

在集羣負載均衡時,Dubbo 提供了多種均衡策略,缺省爲 random 隨機調用。一般有選擇算法 隨機算法,哈希算法,加權算法,最小連接數算法。
各個算法流程圖:
在這裏插入圖片描述

  • 隨機算法:如果各臺服務器權重一樣,使用隨機算法選擇一臺服務器;否則,按照權重比率選擇服務器。適應於服務器參數一致場景。
  • 一致性哈希算法:相同參數的請求總是發到同一提供者,當某一臺提供者掛時,原本發往該提供者的請求,基於虛擬節點,平攤到其它提供者,不會引起劇烈變動。缺省用 160 份虛擬節點。適應於服務器參數一致場景。
  • 加權輪詢算法:如果各臺服務器權重一致,按照輪詢選擇服務器,否則,按照權重加權選擇。適應於服務器參數不一致場景。
  • 最小連接數算法:獲取各臺服務的活躍數(未處理的請求數),如果都不相等,選擇最小活躍數對應的服務器,否則,判斷各臺服務器的權重是否相同,如果不同,按照權重加權隨機選擇,否則,隨機選擇。適應於服務器參數一致場景。

四、自適應負載均衡能力要求

  • Gateway(Consumer) 端能夠自動根據服務處理能力變化動態最優化分配請求保證較低響應時間,較高吞吐量;
  • Provider 端能自動進行服務容量評估,當請求數量超過服務能力時,允許拒絕部分請求,以保證服務不過載;
  • 當請求速率高於所有的 Provider 服務能力之和時,允許 Gateway( Consumer ) 拒絕服務新到請求。
    在這裏插入圖片描述

五、自適應負載均衡設計目標

  • 成功請求數最高
  • TPS最大

六、自適應負載均衡與dubbo負載均衡區別

  1. dubbo隨機算法、加權輪詢算法、最小活躍數調用算法都使用到權重,但是這個權重是手動配置(寫死的,無法被改變的),作爲服務器的參數註冊到註冊中心,沒有真正意義上實現自適應。
  2. 自適應負載均衡算法使用到的算法也離不開隨機、加權、最小活躍數算法,但是權重的來源是隨服務器當前運行狀況動態平衡的,權重的計算來源可以是:服務器處理請求平均時長,任務隊列阻塞任務數,活躍數等。

七、設計自適應負載均衡算法 – 動態計算權重

本次比賽小組計算權重是根據服務器的處理請求平均時長確定

7.1、自適應負載均衡流程設計

在這裏插入圖片描述
說明:服務器參數表是自己維護的ConcurrentHashMap,key爲服務器IP,value爲服務器的參數對象。

7.2、計算權重流程

  1. 開始接受請求:服務器都還沒接收到請求時,按照輪詢算法打流量,目的是爲了獲取各臺服務器的參數,參數包括:平均處理請求時長,每臺服務器的活躍數。
  2. 計算新權重:每臺服務器都接收到請求後,根據返回參數計算每臺服務器的權重,權重的計算依據每臺服務器的平均處理請求時長。
    weigth = weight + (500 / 平均處理時長)即平均處理時長越短,新權重會越大。
  3. 按照新權重加權按照概率隨機打流量:
		//在總權重中隨機獲取一個偏移量
        int offsetWeight = ThreadLocalRandom.current().nextInt(totalWeight);   
		//遍歷服務器列表
        for (遍歷服務器列表) {
            int currentWeight = 獲取當前服務器權重;
            offsetWeight = offsetWeight - currentWeight;
            if (offsetWeight < 0) {
                選擇該服務器
            }
        }

7.3、如何獲取服務器參數

  1. 請求鏈路圖如下:
    在這裏插入圖片描述
- CallbackService:Consumer和Provider兩端請求回調接口
- Filter:Consumer和Provider兩端請求過濾器,dubbo開放該接口供開發者實現。
  1. 參數獲取
  • 服務器的活躍數:由於請求和返回都會調用Consumer端的filter接口,所以可以在Filter接口中設置自增字段,當請求某臺服務器時,該服務器對應的自增字段加1,返回結果時,自增字段值減1。自增字段可以藉助Dubbo內部維護的RpcStatus類實現。
  • 服務器的總請求數:當請求調用Consumer端的filter接口時,請求數累加,總請求數保存在服務器參數表中(自己維護的ConcurrentHashMap,key爲服務器IP,value爲服務器的參數對象)。
  • 服務器處理請求的平均時長:服務器處理一次請求時長計算方式有兩種,一種是服務器真正處理請求的時長+網絡延時,即當請求調用Consumer端的filter接口時,獲取開始時間,處理結束後回調filter接口時獲取結束時間,開始時間與結束時間的差值即一次處理請求的時長(時長包括網絡延時),每次請求的時差累加,將總時長保存在該服務器的參數表中;另外一種是隻在Provide端處理的時長,不包括網絡延時,即當請求到達Provider端Filter接口時,獲取當前時間,服務處理結束時,獲取結束時間,兩者的時差作爲一次處理請求的時長,每次請求的時差累加,通過回調CallbackService接口將參數以字符串的形式返回Consumer端,Consumer端CallbakService接收到返回字符串處理後將總時長保存在該服務器的參數表中。
  1. 權重計算
  • 從服務器列表選擇服務器的時候,遍歷服務器列表,從服務器參數表中獲取服務器的總時長,總的請求數,服務器的處理請求平均時長 = 總請求數 / 總時長。
  • 新權重 = 原始權重 + 500 / 平均時長。即平均處理時長越短,服務器性能越好,權重越大。

八、dubbo負載均衡源碼分析

8.1、隨機算法

流程圖:
在這裏插入圖片描述
源碼分析:

/**
 * random load balance.
 */
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) {
        // Number of invokers 服務器數量
        int length = invokers.size();
        // Every invoker has the same weight?  默認開始每臺服務器的權重一樣
        boolean sameWeight = true;
        // the weight of every invokers    每臺服務器的權重
        int[] weights = new int[length];
        // the first invoker's weight      第一臺服務器的權重,作爲比較值
        int firstWeight = getWeight(invokers.get(0), invocation);
        weights[0] = firstWeight;
        // The sum of weights  總權重值
        int totalWeight = firstWeight;
        //獲取各臺服務器的權重值,總權重值,各臺服務器權重值是否相同標誌
        for (int i = 1; i < length; i++) {
            int weight = getWeight(invokers.get(i), invocation);
            // save for later use
            weights[i] = weight;
            // Sum
            totalWeight += weight;
            if (sameWeight && weight != firstWeight) {
                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.
            //按照權重分配各臺服務器獲取到的概率,通過偏移量表現概率問題,比如三臺服務器,權重【2,5,1】,總權重是8,在總權重中隨機取到
            //2,5,1的概率分別是2/8,5/8,1/8。
            int offset = ThreadLocalRandom.current().nextInt(totalWeight);
            // Return a invoker based on the random value.
            for (int i = 0; i < length; i++) {
                offset -= weights[i];
                if (offset < 0) {
                    return invokers.get(i);
                }
            }
        }
        // If all invokers have the same weight value or totalWeight=0, return evenly.
        return invokers.get(ThreadLocalRandom.current().nextInt(length));
    }

}

8.2、哈希算法

一致性哈希算法:相同參數的請求總是發到同一提供者,當某一臺提供者掛時,原本發往該提供者的請求,基於虛擬節點,平攤到其它提供者,不會引起劇烈變動。缺省用 160 份虛擬節點。

流程圖:
在這裏插入圖片描述
源碼分析:

/**
 * ConsistentHashLoadBalance
 */
public class ConsistentHashLoadBalance extends AbstractLoadBalance {
    public static final String NAME = "consistenthash";

    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.ceilingEntry(hash);
            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 = value.getBytes(StandardCharsets.UTF_8);
            md5.update(bytes);
            return md5.digest();
        }
    }
}

8.3、加權輪訓算法

流程圖:
在這裏插入圖片描述
源碼分析:

/**
 * 循環負載平衡
 */
public class RoundRobinLoadBalance extends AbstractLoadBalance {
    public static final String NAME = "roundrobin";
    
    private static final 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 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 (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;
        }
        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);
    }

}

8.4、最小活躍數算法

當有請求打向某臺服務器時,該服務活躍數加1,當請求被處理結束,活躍數減1,活躍數量可以通過dubbo維護RpcStatus類存儲。

流程圖:
在這裏插入圖片描述
源碼分析

public class LeastActiveLoadBalance extends AbstractLoadBalance {

    public static final String NAME = "leastactive";

    private final Random random = new Random();

    protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
        int length = invokers.size(); // 總個數
        int leastActive = -1; // 最小的活躍數
        int leastCount = 0; // 相同最小活躍數的個數
        int[] leastIndexs = 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; // 重新統計相同最小活躍數的個數
                leastIndexs[0] = i; // 重新記錄最小活躍數下標
                totalWeight = weight; // 重新累計總權重
                firstWeight = weight; // 記錄第一個權重
                sameWeight = true; // 還原權重相同標識
            } else if (active == leastActive) { // 累計相同最小的活躍數
                leastIndexs[leastCount ++] = i; // 累計相同最小活躍數下標
                totalWeight += weight; // 累計總權重
                // 判斷所有權重是否一樣
                if (sameWeight && i > 0 
                        && weight != firstWeight) {
                    sameWeight = false;
                }
            }
        }
        // assert(leastCount > 0)
        if (leastCount == 1) {
            // 如果只有一個最小則直接返回
            return invokers.get(leastIndexs[0]);
        }
        if (! sameWeight && totalWeight > 0) {
            // 如果權重不相同且權重大於0則按總權重數隨機
            int offsetWeight = random.nextInt(totalWeight);
            // 並確定隨機值落在哪個片斷上
            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則均等隨機
        return invokers.get(leastIndexs[random.nextInt(leastCount)]);
    }
}

九、自適應負載均衡源碼地址

github地址:https://github.com/RuiDer/LoadBalance

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章