負載均衡算法 - 基本實現

​ 最近在比賽一個項目 , 是給Dubbo寫一個負載均衡接口 , 其實dubbo已經實現了下面四種, 因此他作的不是這個單面負載均衡, 須要作雙向負載均衡 , 負載均衡的權重取決於服務端,因此有些時候咱們不知道如何計算權重, 權重受到不少因素影響 ,因此就須要動態考慮了.java

​ Dubbo 提供了4種負載均衡實現,分別是基於權重隨機算法的 RandomLoadBalance、基於最少活躍調用數算法的 LeastActiveLoadBalance、基於 hash 一致性的 ConsistentHashLoadBalance,以及基於加權輪詢算法的 RoundRobinLoadBalance。node

1. RandomLoadBalance

RandomLoadBalance 是加權隨機算法的具體實現,它的算法思想很簡單。假設咱們有一組服務器 servers = [A, B, C],他們對應的權重爲 weights = [5, 3, 2],權重總和爲10。如今把這些權重值平鋪在一維座標值上,[0, 5) 區間屬於服務器 A,[5, 8) 區間屬於服務器 B,[8, 10) 區間屬於服務器 C。接下來經過隨機數生成器生成一個範圍在 [0, 10) 之間的隨機數,而後計算這個隨機數會落到哪一個區間上。好比數字3會落到服務器 A 對應的區間上,此時返回服務器 A 便可。權重越大的機器,在座標軸上對應的區間範圍就越大,所以隨機數生成器生成的數字就會有更大的機率落到此區間內。只要隨機數生成器產生的隨機數分佈性很好,在通過屢次選擇後,每一個服務器被選中的次數比例接近其權重比例。好比,通過一萬次選擇後,服務器 A 被選中的次數大約爲5000次,服務器 B 被選中的次數約爲3000次,服務器 C 被選中的次數約爲2000次。算法

private <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {

    int len = invokers.size();
    int[] arr = new int[len];
    int count = 0;
    int totalWight = 0;
    for (Invoker<T> invoker : invokers) {
        int wight = invoker.getUrl().getParameter(org.apache.dubbo.common.Constants.WEIGHT_KEY, 1);
        arr[count++] = wight;
        totalWight += wight;
    }

    // 隨機偏移量
    int offset = ThreadLocalRandom.current().nextInt(totalWight);

    for (int i = 0; i < arr.length; i++) {

        // 好比 [2 , 3 , 5]  , offset=7 , 此時 offset-2=5 , offset-3=2 , offset-5=-3 ,此時當他爲負數時,說明就在這個區間內, 咱們是左閉右開[0,2) ,[2,5) ,[5,10)
        offset -= arr[i];

        // 此時已經小於0了, 說明在這個區間內
        if (offset < 0) {
            // 返回就好了
            return invokers.get(i);
        }
    }

    return ... 
}

2. LeastActiveLoadBalance

LeastActiveLoadBalance 翻譯過來是最小活躍數負載均衡。活躍調用數越小,代表該服務提供者效率越高,單位時間內可處理更多的請求。此時應優先將請求分配給該服務提供者。在具體實現中,每一個服務提供者對應一個活躍數 active。初始狀況下,全部服務提供者活躍數均爲0。每收到一個請求,活躍數加1,完成請求後則將活躍數減1。在服務運行一段時間後,性能好的服務提供者處理請求的速度更快,所以活躍數降低的也越快,此時這樣的服務提供者可以優先獲取到新的服務請求、這就是最小活躍數負載均衡算法的基本思想。除了最小活躍數,LeastActiveLoadBalance 在實現上還引入了權重值。因此準確的來講,LeastActiveLoadBalance 是基於加權最小活躍數算法實現的。舉個例子說明一下,在一個服務提供者集羣中,有兩個性能優異的服務提供者。某一時刻它們的活躍數相同,此時 Dubbo 會根據它們的權重去分配請求,權重越大,獲取到新請求的機率就越大。若是兩個服務提供者權重相同,此時隨機選擇一個便可。apache

​ 這個須要咱們去維護一個最小鏈接數的計算, 配合加權 ,當鏈接數相同的時候,選擇加權分最高的 ...數組

3. ConsistentHashLoadBalance

一致性 hash 算法由麻省理工學院的 Karger 及其合做者於1997年提出的,算法提出之初是用於大規模緩存系統的負載均衡。它的工做過程是這樣的,首先根據 ip 或者其餘的信息爲緩存節點生成一個 hash,並將這個 hash 投射到 [0, 232 - 1] 的圓環上。當有查詢或寫入請求時,則爲緩存項的 key 生成一個 hash 值。而後查找第一個大於或等於該 hash 值的緩存節點,併到這個節點中查詢或寫入緩存項。若是當前節點掛了,則在下一次查詢或寫入緩存時,爲緩存項查找另外一個大於其 hash 值的緩存節點便可。緩存

​ 這個主要是靠hash算法 , 經過hash % 服務器數量 = 服務器索引服務器

​ Java中可使用這個 : int identityHashCode = System.identityHashCode(invokers);來獲取數據結構

4. RoundRobinLoadBalance

加權輪詢負載均衡的實現 RoundRobinLoadBalance , 我選擇的就是這種負載均衡

這裏從最簡單的輪詢開始講起,所謂輪詢是指將請求輪流分配給每臺服務器。舉個例子,咱們有三臺服務器 A、B、C。咱們將第一個請求分配給服務器 A,第二個請求分配給服務器 B,第三個請求分配給服務器 C,第四個請求再次分配給服務器 A。這個過程就叫作輪詢。輪詢是一種無狀態負載均衡算法,實現簡單,適用於每臺服務器性能相近的場景下。但現實狀況下,咱們並不能保證每臺服務器性能均相近。若是咱們將等量的請求分配給性能較差的服務器,這顯然是不合理的。dom

所以,這個時候咱們須要對輪詢過程進行加權,以調控每臺服務器的負載。通過加權後,每臺服務器可以獲得的請求數比例,接近或等於他們的權重比。好比服務器 A、B、C 權重比爲 5:2:1。那麼在8次請求中,服務器 A 將收到其中的5次請求,服務器 B 會收到其中的2次請求,服務器 C 則收到其中的1次請求。

下面有個表格 , 默認權重是 [5,1,1]

請求編號 currentWeight 數組 選擇結果 減去權重總和後的 currentWeight 數組
1 [5, 1, 1] A [-2, 1, 1]
2 [3, 2, 2] A [-4, 2, 2]
3 [1, 3, 3] B [1, -4, 3]
4 [6, -3, 4] A [-1, -3, 4]
5 [4, -2, 5] C [4, -2, -2]
6 [9, -1, -1] A [2, -1, -1]
7 [7, 0, 0] A [0, 0, 0]

此時 7 次中A節點被選中的次數是 5 , B 是1 ,C是1 ,因此符合咱們的需求

計算方法以下 , 首先有三個變量 , 記錄了 currentWeight , effectiveWeight , totalWeight

private class Node {
        private int currentWeight; // 當前權重
        private int effectiveWeight; // 有效權重,初始化的時候等於當前權重
        private int totalWeight; // 總權重
 }

咱們先看初始化 , 初始化時 ,計算權重

好比咱們知道權重了 A : 5 , B : 1 , C : 1

初始化節點

A : new Node(5, 5, 7)
B : new Node(1, 1, 7)
C : new Node(1, 1, 7)
    
此時currentWeight的和是 : 7

當第一次的時候 , A節點權重最大 ,此時 5-7=-2, A節點變成了 Node(-2, 5, 7)

A : new Node(-2, 5, 7)
B : new Node(1, 1, 7)
C : new Node(1, 1, 7)

此時currentWeight的和是 : 0

而後從新迴歸, 迴歸須要currentWeight= currentWeight+effectiveWeight

A : new Node(3, 5, 7)
B : new Node(2, 1, 7)
C : new Node(2, 1, 7)

此時currentWeight的和是 : 7 // 因此又回來了 .. ..

怎樣作呢 ?

此時咱們用一個 PriorityQueue<Pair<String, Node>> 維護全部節點的信息 , 同時使用 Pair<String, Node>維護單個節點

1. 初始化隊列
PriorityBlockingQueue<Pair<String, Node>> weightQueue = new PriorityBlockingQueue<>(3, (o1, o2) -> o2.getValue().getCurrentWeight() - o1.getValue().getCurrentWeight());


2. 遍歷放入權重
weightQueue.add(new Pair<>(key, new Node(currentWeight, effectiveWeight, totalWeight))); 

3. 當放入之後, 此時就能夠拿到一個當前權重最大的節點 , 若是不想使用優先隊列, 能夠本身實現一個大頂堆 ,很簡單的. 
    
// 獲取當前節點權重最大的 ,     
Pair<String, Node> hPair = weightQueue.take(); 
// 此時hPair節點的值應當是 5 - 7 = -2 
int afterCurrentWeight = value.getCurrentWeight() - value.getTotalWeight();
// 設置值
hNode.setCurrentWeight(afterCurrentWeight);

// 遍歷
for (Pair<String, Node> stringNodePair : weightQueue) {
    //獲取節點 
    Node node = stringNodePair.getValue();
    // 剩餘每一個節點值 + 有效值 
    int after = node.getCurrentWeight() + node.getEffectiveWeight();
    
    // 設置節點值 
    node.setCurrentWeight(after);
}
// 因爲咱們拿出來的節點尚未 從新計算, 還要計算
hNode.setCurrentWeight(value.getCurrentWeight() + value.getEffectiveWeight());
// 從新放入節點. ... ,從新排序
weightQueue.add(hPair);

其實本身維護的話能夠進行 heapfy的 , 咱們只能依賴JDK提供的數據結構進行的這種取巧方式

相關文章
相關標籤/搜索