主頁 > 後端開發 > Dubbo 負載均衡的實作

Dubbo 負載均衡的實作

2020-10-03 19:58:43 後端開發

前言

負載均衡是指在集群中,將多個資料請求分散在不同單元上進行執行,主要為了提高系統容錯能力和加強系統對資料的處理能力,

在 Dubbo 中,一次服務的呼叫就是對所有物體域 Invoker 的一次篩選過濾,最終選定具體呼叫的 Invoker,首先在 Directory 中獲取全部 Invoker 串列,通過路由篩選出符合規則的 Invoker,最后再經過負載均衡選出具體的 Invoker,所以 Dubbo 負載均衡機制是決定一次服務呼叫使用哪個提供者的服務,

整體結構

Dubbo 負載均衡的分析入口是 org.apache.dubbo.rpc.cluster.loadbalance.AbstractLoadBalance 抽象類,查看這個類繼承關系,

這個被 RandomLoadBalance、LeastActiveLoadBalance、RoundRobinLoadBalance 及 ConsistentHashLoadBalance 類繼承,這四個類是 Dubbo 中提供的四種負載均衡演算法的實作,

名稱 說明
RandomLoadBalance 隨機演算法,根據權重設定隨機的概率
LeastActiveLoadBalance 最少活躍數演算法,指請求數和完成數之差,使執行效率高的服務接收更多請求
RoundRobinLoadBalance 加權輪訓演算法,根據權重設定輪訓比例
ConsistentHashLoadBalance Hash 一致性演算法,相同請求引數分配到相同提供者

以上則是 Dubbo 提供的四種負載均衡演算法,

從上圖中,看到 AbstractLoadBalance 實作了 LoadBalance 介面,同時是一個 SPI 介面,指定默認實作為 RandomLoadBalance 隨機演算法機制,

抽象類 AbstractLoadBalance 中,實作了負載均衡通用的邏輯,同時給子類宣告了一個抽象方法供子類實作其負載均衡的邏輯,

public abstract class AbstractLoadBalance implements LoadBalance {
    /**
     *
     * @param 運行時間(毫秒)
     * @param 預熱時間(毫秒)
     * @param 要計算的 Invoker 權重值
     */
    static int calculateWarmupWeight(int uptime, int warmup, int weight) {
        // 計算預熱時期的權重
        int ww = (int) ((float) uptime / ((float) warmup / (float) weight));
        // 回傳的權重值區間在: 1 ~ weight
        return ww < 1 ? 1 : (ww > weight ? weight : ww);
    }

    @Override
    public <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) {
        // 校驗 invokers 是否為空
        if (CollectionUtils.isEmpty(invokers)) {
            return null;
        }
        // 當到達負載均衡流程時,invokers 中只有一個 Invoker 時,直接回傳該 Invoker
        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) {
        // 獲取當前Invoker配置的權重值
        int weight = invoker.getUrl().getMethodParameter(invocation.getMethodName(), WEIGHT_KEY, DEFAULT_WEIGHT);
        if (weight > 0) {
            // 服務啟動時間
            long timestamp = invoker.getUrl().getParameter(REMOTE_TIMESTAMP_KEY, 0L);
            if (timestamp > 0L) {
                // 服務已運行時長
                int uptime = (int) (System.currentTimeMillis() - timestamp);
                // 服務預熱時間,默認 DEFAULT_WARMUP = 10 * 60 * 1000 ,預熱十分鐘
                int warmup = invoker.getUrl().getParameter(WARMUP_KEY, DEFAULT_WARMUP);
                // 如果服務運行時長小于預熱時長,重新計算出預熱時期的權重
                if (uptime > 0 && uptime < warmup) {
                    weight = calculateWarmupWeight(uptime, warmup, weight);
                }
            }
        }
        // 保證最后回傳的權重值不小于0
        return weight >= 0 ? weight : 0;
    }

}

在 AbstractLoadBalance 中,getWeight 和 calculateWarmupWeight 方法是獲取和計算當前 Invoker 的權重值,

getWeight 中獲取當前權重值,通過 URL 獲取當前 Invoker 設定的權重,如果當前服務提供者啟動時間小于預熱時間,則會重新計算權重值,對服務進行降權處理,保證服務能在啟動初期不分發設定比例的全部流量,健康運行下去,

calculateWarmupWeight 是重新計算權重值的方法,計算公式為:服務運行時長 / (預熱時長 / 設定的權重值),等價于(服務運行時長 / 預熱時長) * 設定的權重值,同時條件服務運行時長 < 預熱時長,由該公式可知,預熱時長和設定的權重值不變,服務運行時間越長,計算出的值越接近 weight,但不會等于 weight,
在回傳計算后的權重結果中,對小于1和大于設定的權重值進行了處理,當重新計算后的權重小于1時回傳1;處于1和設定的權重值之間時,直接回傳計算后的結果;當權重大于設定的權重值時(因為條件限制,不會出現該類情況),回傳設定的權重值,所以得出結論:重新計算后的權重值為 1 ~ 設定的權重值,運行時間越長,計算出的權重值越接近設定的權重值

配置方式

服務端

通過 XML 配置方式:

<!-- 服務級別配置 -->
<dubbo:service id="xXXXService" interface="top.ytao.service.XXXXService"  loadbalance="負載策略" />

<!-- 方法級別配置 -->
<dubbo:service id="xXXXService" interface="top.ytao.service.XXXXService" >
    <dubbo:method name="方法名" loadbalance="負載策略"/>
</dubbo:service>

通過 Properties 配置:

dubbo.service.loadbalance=負載策略

通過注解方式:

@Service(loadbalance = "負載策略")

客戶端

通過 XML 配置方式:

<!-- 服務級別配置 -->
<dubbo:reference id="xXXXService" interface="top.ytao.service.XXXXService" loadbalance="負載策略" />

<!-- 方法級別配置 -->
<dubbo:reference id="xXXXService" interface="top.ytao.service.XXXXService">
    <dubbo:method name="方法名" loadbalance="負載策略"/>
</dubbo:reference>

通過 Properties 配置:

dubbo.reference.loadbalance=負載策略

通過注解配置方式:

@Reference(loadbalance = "負載策略")

實作方式也可通過 Dubbo-Admin 管理后臺進行配置,如圖:

隨機演算法

加權隨機演算法負載均衡策略(RandomLoadBalance)是 dubbo 負載均衡的默認實作方式,根據權重分配各個 Invoker 隨機選中的比例,這里的意思是:將到達負載均衡流程的 Invoker 串列中的 權重進行求和,然后求出單個 Invoker 權重在總權重中的占比,亂數就在總權重值的范圍內生成,

如圖,假如當前有192.168.1.10192.168.1.11兩個負載均衡的服務,權重分別為 4、6 ,則它們的被選中的比例為 2/5、3/5,

當生成亂數為 6 時,就會選中192.168.1.11的服務,

dubbo 中 RandomLoadBalance 的 doSelect 實作代碼:

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) {
        // Invoker 數量
        int length = invokers.size();
        // 標識所有 Invoker 的權重是否都一樣
        boolean sameWeight = true;
        // 用一個陣列保存每個 Invoker 的權重
        int[] weights = new int[length];
        // 第一個 Invoker 的權重
        int firstWeight = getWeight(invokers.get(0), invocation);
        weights[0] = firstWeight;
        // 求和總權重
        int totalWeight = firstWeight;
        for (int i = 1; i < length; i++) {
            int weight = getWeight(invokers.get(i), invocation);
            // 保存每個 Invoker 的權重到陣列總
            weights[i] = weight;
            // 累加求和總權重
            totalWeight += weight;
            // 如果不是所有 Invoker 的權重都一樣,就給標記上 sameWeight = false
            if (sameWeight && weight != firstWeight) {
                sameWeight = false;
            }
        }
        // 計算亂數取到的 Invoker,條件是必須總權重大于0,并且每個 Invoker 的權重都不一樣
        if (totalWeight > 0 && !sameWeight) {
            // 基于 0~總數 范圍內生成亂數
            int offset = ThreadLocalRandom.current().nextInt(totalWeight);
            // 計算亂數對應的 Invoker
            for (int i = 0; i < length; i++) {
                offset -= weights[i];
                if (offset < 0) {
                    return invokers.get(i);
                }
            }
        }
        // 如果所有 Invoker 的權重都一樣則隨機從 Invoker 串列中回傳一個
        return invokers.get(ThreadLocalRandom.current().nextInt(length));
    }

}

以上就是加權隨機策略的實作,這里比較主要關注計算生成的亂數對應的 Invoker,通過遍歷權重陣列,生成的數累減當前權重值,當 offset 為 0 時,就表示 offset 對應當前的 Invoker 服務,

以生成的亂數為 6 為例,遍歷 Invokers 長度:

  1. 第一輪:offset = 6 - 4 = 2 不滿足 offset < 0,繼續遍歷,

  2. 第二輪:offset = 2 - 6 = -4 滿足 offset < 0,回傳當前索引對應的 Invoker,因為 offset 回傳負數,表示 offset 落在當前 Invoker 權重的區間里,

加權隨機策略并非一定按照比例被選到,理論上呼叫次數越多,分布的比例越接近權重所占的比例,

最少活躍數演算法

最小活躍數負載均衡策略(LeastActiveLoadBalance)是從最小活躍數的 Invoker 中進行選擇,什么是活躍數呢?活躍數是一個 Invoker 正在處理的請求的數量,當 Invoker 開始處理請求時,會將活躍數加 1,完成請求處理后,將相應 Invoker 的活躍數減 1,找出最小活躍數后,最后根據權重進行選擇最終的 Invoker,如果最后找出的最小活躍數相同,則隨機從中選中一個 Invoker,

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) {
        // Invoker 數量
        int length = invokers.size();
        // 所有 Invoker 中的最小活躍值都是 -1
        int leastActive = -1;
        // 最小活躍值 Invoker 的數量
        int leastCount = 0;
        // 最小活躍值 Invoker 在 Invokers 串列中對應的下標位置
        int[] leastIndexes = new int[length];
        // 保存每個 Invoker 的權重
        int[] weights = new int[length];
        // 總權重
        int totalWeight = 0;
        // 第一個最小活躍數的權重
        int firstWeight = 0;
        // 最小活躍數 Invoker 串列的權重是否一樣
        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;
            // 如果當前最小活躍數為-1(-1為最小值)或小于leastActive
            if (leastActive == -1 || active < leastActive) {
                // 重置最小活躍數
                leastActive = active;
                // 重置最小活躍數 Invoker 的數量
                leastCount = 1;
                // 保存當前 Invoker 在 Invokers 串列中的索引至leastIndexes陣列中
                leastIndexes[0] = i;
                // 重置最小活躍數 invoker 的總權重值
                totalWeight = afterWarmup;
                // 記錄當前 Invoker 權重為第一個最小活躍數 Invoker 的權重
                firstWeight = afterWarmup;
                // 因為當前 Invoker 重置為第一個最小活躍數 Invoker ,所以標識所有最小活躍數 Invoker 權重都一樣的值為 true
                sameWeight = true;
            // 如果當前最小活躍數和已宣告的最小活躍數相等 
            } else if (active == leastActive) {
                // 記錄當前 Invoker 的位置
                leastIndexes[leastCount++] = i;
                // 累加當前 Invoker 權重到總權重中
                totalWeight += afterWarmup;
                // 如果當前權重與firstWeight不相等,則將 sameWeight 改為 false
                if (sameWeight && i > 0
                        && afterWarmup != firstWeight) {
                    sameWeight = false;
                }
            }
        }
        // 如果最小活躍數 Invoker 只有一個,直接回傳該 Invoker
        if (leastCount == 1) {
            return invokers.get(leastIndexes[0]);
        }
        if (!sameWeight && totalWeight > 0) {
            // 根據權重隨機從最小活躍數 Invoker 串列中選擇一個 
            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);
                }
            }
        }
        // 如果所有 Invoker 的權重都一樣則隨機從 Invoker 串列中回傳一個
        return invokers.get(leastIndexes[ThreadLocalRandom.current().nextInt(leastCount)]);
    }
}

這段代碼的整個邏輯就是,從 Invokers 串列中篩選出最小活躍數的 Invoker,然后類似加權隨機演算法策略方式選擇最終的 Invoker 服務,

輪詢演算法

加權輪詢負載均衡策略(RoundRobinLoadBalance)是基于權重來決定輪詢的比例,普通輪詢會將請求均勻的分布在每個節點,但不能很好調節不同性能服務器的請求處理,所以加權負載均衡來根據權重在輪詢機制中分配相對應的請求比例給每臺服務器,

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) {
        // key 為 介面名+方法名
        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();
        // 最大 current 的 Invoker
        Invoker<T> selectedInvoker = null;
        // 保存選中的 WeightedRoundRobin 物件
        WeightedRoundRobin selectedWRR = null;
        // 遍歷 Invokers 串列
        for (Invoker<T> invoker : invokers) {
            // 從快取中獲取 WeightedRoundRobin 物件
            String identifyString = invoker.getUrl().toIdentityString();
            WeightedRoundRobin weightedRoundRobin = map.get(identifyString);
            // 獲取當前 Invoker 物件
            int weight = getWeight(invoker, invocation);

            // 如果當前 Invoker 沒有對應的 WeightedRoundRobin 物件,則新增一個
            if (weightedRoundRobin == null) {
                weightedRoundRobin = new WeightedRoundRobin();
                weightedRoundRobin.setWeight(weight);
                map.putIfAbsent(identifyString, weightedRoundRobin);
            }
            // 如果當前 Invoker 權重不等于對應的 WeightedRoundRobin 物件中的權重,則重新設定當前權重到對應的 WeightedRoundRobin 物件中
            if (weight != weightedRoundRobin.getWeight()) {
                weightedRoundRobin.setWeight(weight);
            }
            // 累加權重到 current 中
            long cur = weightedRoundRobin.increaseCurrent();
            // 設定 weightedRoundRobin 物件最后更新時間
            weightedRoundRobin.setLastUpdate(now);
            // 最大 current 的 Invoker,并賦值給相應的變數
            if (cur > maxCurrent) {
                maxCurrent = cur;
                selectedInvoker = invoker;
                selectedWRR = weightedRoundRobin;
            }
            // 累加權重到總權重中
            totalWeight += weight;
        }
        // 如果 Invokers 串列中的數量不等于快取map中的數量
        if (!updateLock.get() && invokers.size() != map.size()) {
            if (updateLock.compareAndSet(false, true)) {
                try {
                    // 拷貝 map 到 newMap 中
                    ConcurrentMap<String, WeightedRoundRobin> newMap = new ConcurrentHashMap<String, WeightedRoundRobin>();
                    newMap.putAll(map);
                    // newMap 轉化為 Iterator
                    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();
                        }
                    }
                    // 將當前newMap服務快取中
                    methodWeightMap.put(key, newMap);
                } finally {
                    updateLock.set(false);
                }
            }
        }
        // 如果存在被選中的 Invoker
        if (selectedInvoker != null) {
            // 計算 current = current - totalWeight
            selectedWRR.sel(totalWeight);
            return selectedInvoker;
        }
        // 正常情況這里不會到達
        return invokers.get(0);
    }

}

上面選中 Invoker 邏輯為:每個 Invoker 都有一個 current 值,初始值為自身權重,在每個 Invoker 中current = current + weight,遍歷完 Invoker 后,current 最大的那個 Invoker 就是本次選中的 Invoker,選中 Invoker 后,將本次 current 值計算current = current - totalWeight

以上面192.168.1.10192.168.1.11兩個負載均衡的服務,權重分別為 4、6 ,基于選中前current = current + weight、選中后current = current - totalWeight計算公式得出如下

請求次數 選中前 current 選中后 current 被選中服務
1 [4, 6] [4, -4] 192.168.1.11
2 [8, 2] [-2, 2] 192.168.1.10
3 [2, 8] [2, -2] 192.168.1.11
4 [6, 4] [-4, 4] 192.168.1.10
5 [0, 10] [0, 0] 192.168.1.11

一致性 Hash 演算法

一致性 Hash 負載均衡策略(ConsistentHashLoadBalance)是讓引數相同的請求分配到同一機器上,把每個服務節點分布在一個環上,請求也分布在環形中,以請求在環上的位置,順時針尋找換上第一個服務節點,如圖所示:

同時,為避免請求散列不均勻,dubbo 中會將每個 Invoker 再虛擬多個節點出來,使得請求呼叫更加均勻,

一致性 Hash 修改配置如下:

<!-- dubbo 默認只對第一個引數進行 hash 標識,指定hash引數 -->
<dubbo:parameter key="hash.arguments" value="https://www.cnblogs.com/ytao-blog/p/1" />

<!-- 虛擬節點數量 -->
<dubbo:parameter key="hash.nodes" value="https://www.cnblogs.com/ytao-blog/p/200" />

一致性 Hash 實作如下:

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

    /**
     * Hash nodes name
     */
    public static final String HASH_NODES = "hash.nodes";

    /**
     * Hash arguments name
     */
    public static final String HASH_ARGUMENTS = "hash.arguments";

    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);
        // key = 介面名+方法名
        String key = invokers.get(0).getUrl().getServiceKey() + "." + methodName;
        // invokers 的 hashcode
        int identityHashCode = System.identityHashCode(invokers);
        // 查看快取中是否存在對應 key 的資料,或 Invokers 串列是否有過變動,如果沒有,則新添加到快取中,并且回傳負載均衡得出的 Invoker
        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);
    }

    // ConsistentHashSelector class ...
}

doSelect 中主要實作快取檢查和 Invokers 變動檢查,一致性 hash 負載均衡的實作在這個內部類 ConsistentHashSelector 中實作,

private static final class ConsistentHashSelector<T> {

    // 存盤虛擬節點
    private final TreeMap<Long, Invoker<T>> virtualInvokers;
    
    // 節點數
    private final int replicaNumber;
    
    // invoker 串列的 hashcode,用來判斷 Invoker 串列是否變化
    private final int identityHashCode;
    
    // 請求中用來作Hash映射的引數的索引
    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 = 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) {
            // 獲取 Invoker 中的地址,包括埠號
            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);
                }
            }
        }
    }
    
    // 找出 Invoker
    public Invoker<T> select(Invocation invocation) {
        // 將引數轉為字串
        String key = toKey(invocation.getArguments());
        // 字串引數轉換為 md5
        byte[] digest = md5(key);
        // 根據 md5 找出 Invoker
        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();
    }
    
    // 利用 md5 匹配到對應的 Invoker
    private Invoker<T> selectForKey(long hash) {
        // 找到第一個大于當前 hash 的 Invoker
        Map.Entry<Long, Invoker<T>> entry = virtualInvokers.ceilingEntry(hash);
        if (entry == null) {
            entry = virtualInvokers.firstEntry();
        }
        return entry.getValue();
    }
    
    // hash 運算
    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;
    }
    
    // md5 運算
    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();
    }

}

一致 hash 實作程序就是先創建好虛擬節點,虛擬節點保存在 TreeMap 中,TreeMap 的 key 為配置的引數先進行 md5 運算,然后將 md5 值進行 hash 運算,TreeMap 的 value 為被選中的 Invoker,

最后請求時,計算引數的 hash 值,去從 TreeMap 中獲取 Invoker,

總結

Dubbo 負載均衡的實作,技巧上還是比較優雅,可以多多學習其編碼思維,在研究其代碼時,需要仔細研究其實作原理,否則比較難懂其思想,

推薦閱讀

《Dubbo 路由機制的實作》

《Dubbo 擴展點加載機制:從 Java SPI 到 Dubbo SPI》

《Dubbo之服務消費原理》

《Dubbo之服務暴露》

《你必須會的 JDK 動態代理和 CGLIB 動態代理》

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/152371.html

標籤:Java

上一篇:《程式員如何優雅地掙零花錢》電子書免費開源!!!

下一篇:Listener

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more