最近在比賽一個項目 , 是給Dubbo寫一個負載均衡接口 , 其實dubbo已經實現了下面四種, 因此他作的不是這個單面負載均衡, 須要作雙向負載均衡 , 負載均衡的權重取決於服務端,因此有些時候咱們不知道如何計算權重, 權重受到不少因素影響 ,因此就須要動態考慮了.java
Dubbo 提供了4種負載均衡實現,分別是基於權重隨機算法的 RandomLoadBalance、基於最少活躍調用數算法的 LeastActiveLoadBalance、基於 hash 一致性的 ConsistentHashLoadBalance,以及基於加權輪詢算法的 RoundRobinLoadBalance。node
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 ... }
LeastActiveLoadBalance 翻譯過來是最小活躍數負載均衡。活躍調用數越小,代表該服務提供者效率越高,單位時間內可處理更多的請求。此時應優先將請求分配給該服務提供者。在具體實現中,每一個服務提供者對應一個活躍數 active。初始狀況下,全部服務提供者活躍數均爲0。每收到一個請求,活躍數加1,完成請求後則將活躍數減1。在服務運行一段時間後,性能好的服務提供者處理請求的速度更快,所以活躍數降低的也越快,此時這樣的服務提供者可以優先獲取到新的服務請求、這就是最小活躍數負載均衡算法的基本思想。除了最小活躍數,LeastActiveLoadBalance 在實現上還引入了權重值。因此準確的來講,LeastActiveLoadBalance 是基於加權最小活躍數算法實現的。舉個例子說明一下,在一個服務提供者集羣中,有兩個性能優異的服務提供者。某一時刻它們的活躍數相同,此時 Dubbo 會根據它們的權重去分配請求,權重越大,獲取到新請求的機率就越大。若是兩個服務提供者權重相同,此時隨機選擇一個便可。apache
這個須要咱們去維護一個最小鏈接數的計算, 配合加權 ,當鏈接數相同的時候,選擇加權分最高的 ...數組
一致性 hash 算法由麻省理工學院的 Karger 及其合做者於1997年提出的,算法提出之初是用於大規模緩存系統的負載均衡。它的工做過程是這樣的,首先根據 ip 或者其餘的信息爲緩存節點生成一個 hash,並將這個 hash 投射到 [0, 232 - 1] 的圓環上。當有查詢或寫入請求時,則爲緩存項的 key 生成一個 hash 值。而後查找第一個大於或等於該 hash 值的緩存節點,併到這個節點中查詢或寫入緩存項。若是當前節點掛了,則在下一次查詢或寫入緩存時,爲緩存項查找另外一個大於其 hash 值的緩存節點便可。緩存
這個主要是靠hash算法 , 經過hash % 服務器數量 = 服務器索引服務器
Java中可使用這個 :
int identityHashCode = System.identityHashCode(invokers);
來獲取數據結構
加權輪詢負載均衡的實現 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提供的數據結構進行的這種取巧方式