Dubbo——負載均衡的實現

負載均衡的實現

在整個集羣容錯流程中,首先經過Directory獲取所有Invoker列表,然後經過Router根據路由規則過濾Invoker,最後倖存下來的Invoker還需要經過負載均衡這一關,選出最終要調用的Invoker。

包裝後的負載均衡

所有的容錯策略中的負載均衡都使用了抽象父類AbstractClusterInvoker中定義的Invoker <T> select方法,而並不是直接使用LoadBalance方法。因爲抽象父類在LoadBalance的基礎上有封裝了一些新的特性:

  1. 粘滯連接:Dubbo中有一種特性叫粘滯連接,以下內容摘自官方文檔:
粘滯連接用於有狀態服務,儘可能讓客戶端總是向同一提供者發起調用,除非該提供者"掛了",再連接另一臺
粘滯連接將自動開啓延遲攔截,以減少長連接數。
<dubbo:protocol name="dubbo" stickytrue" />>
  1. 可用檢測:Dubbo調用的URL中,如果含有cluster.availablecheck=false,則不會檢測遠程服務是否可用,直接調用。如果不設置,則默認會開啓檢查,對所有的服務都做是否可用的檢查,如果不可用,則再次做負載均衡。
  2. 避免重複調用:對於已經調用過的遠程服務,避免重複選擇,每次都使用同意而節點。這種特性主要是爲了避免併發場景下,某個節點瞬間被大量請求,整個邏輯過程大致可以分爲4步:
    1. 檢查URL中是否有配置粘滯連接,如果有則使用粘滯連接的Invoker。如果沒有配置粘滯連接,或者重複調用檢測不通過、可用檢測不通過,則進入第2步。
    2. 通過ExtensionLoader獲取負載均衡的具體實現,並通過負載均衡做節點的選擇。對選擇出來的節點做重複調用、可用性檢測,通過則直接返回,否則進入第3步。
    3. 進行節點的重新選擇。如果需要做可用性檢測,則會遍歷Directory中得到的所有節點,過濾不可用和已經調用過的節點,在剩餘節點中重新做負載均衡;如果不需要做可用性檢測,那麼也會遍歷Directory中得到的所有節點,但只過濾已經調用過的,在剩餘的節點中重新做負載均衡。這裏存在一種情況,就是在過濾不可用或已經調用過的節點時,節點全部被過濾,沒有剩下任何節點,此時進入第4步。
    4. 遍歷所有已經調用過的節點,,選出所有可用的節點,再通過負載均衡選出一個結點並返回。如果還找不到可調用的節點,則返回null。

從上述邏輯中,我們可以得知,框架會優先處理粘滯連接。否則會根據可用性檢測或重複調用檢測過濾一些節點,並在剩餘的節點中做負載均衡。如果可用性檢測或重複調用檢測把節點都過濾了,則兜底的策略是:在已經調用過的節點中通過負載均衡選擇出一個可用的節點

負載均衡的總體結構

Dubbo內置了4中負載均衡算法,與用戶也可以自行擴展,因爲LoadBalance接口上有@SPI註解

在這裏插入圖片描述

從代碼中可以知道默認的負載均衡就是RandomloadBalance,即隨機負載均衡。由於select方法上有Adaptive("loadbalance")註解,因此在URL中可以通過loadbalance=xxx來懂愛指定select使得負載均衡算法。

負載均衡算法名稱 效果說明
Random LodBalance 隨機,按權重設置隨機概率。在一個節點上碰撞的概率高,但調用量越大分佈越均勻,而且按概率使用權重後也比較均勻,有利於動態調整提供者的權重
RoundRobin LoadBalance 輪詢,按公約後的權重設置輪詢比例。存在慢的提供者累積請求的問題,比如:第二臺機器很慢,但沒有"掛",當請求調用到第二臺就卡在案例,久而久之,所有請求都卡在第二臺上
LeastActive LoadBalance 最少活躍調用數,如果活躍數相同則隨機調用,活躍數指調用前後計數差使慢的提供者收到更少請求,因爲越慢的提供者的調用前後計數差會越大
ConsistentHash LoadBalance 一致性Hash,相同參數的請求總是發送同一提供者。當某一臺提供者"掛"時,原本發往該提供者的請求,基於虛擬節點,會平攤到其他提供者,不會引起劇烈變動。默認只對第一個參數"Hash",如果要修改,則配置
<dubbo:parameter key="hash.arguments" value="0, 1" />
默認使用160份虛擬節點,如果要修改,則配置<dubo:parameter key="hash.nodes" value="320"/>

四種負載均衡算法都繼承自同一個抽象類,使用的也是模板模式,抽象父類中已經把通用的邏輯完成,留了一個抽象的doSelect方法給子類實現。

在這裏插入圖片描述

抽象父類AbstractLoadBalance有兩個權重相關的方法:calculateWarmupWeight和getWeight。getWeight方法就是獲取當前Invoker的權重,calculteWarmupWeight是計算具體權重。getWeight方法中會調用calculateWarmupWeight:

在這裏插入圖片描述

calculateWarmupWeight的計算邏輯比較簡單,由於框架考慮了服務剛啓動的時候需要有一個預熱的過程,如果一啓動就給予100%的流量,則可能會讓服務器崩潰,因此實現了calculateWarmupWeight方法用於計算預熱時候的權重,計算邏輯:(啓動至今時間/給予的預熱總時間 * 權重)

例如:假設我們設置A服務的權重是5,讓它預熱10分鐘,則第一分鐘的時候,它的權重變爲(1/10) * 5 = 0..5, 0.5 / 5 = 0.1, 也就是隻承擔10%的流量;10分鐘後,權重就變爲(10 / 10) * 5= 5,也就是權重變爲設置的100%,承擔了所有流量

抽象父類的select方法是進行具體負載均衡邏輯的地方,這裏只是鎖了一些判斷並調用需要子類實現的doSelect方法。

Random負載均衡

Randon負載均衡是按照權重設置隨機概率做負載均衡的。這種負載均衡算法並不能精確地平均請求,但是隨着請求數量的增加,最終結果是大致平均的。它的負載計算步驟如下:

  1. 計算總權重並判斷每個 Invoker的權重是否一樣。遍歷整個 Invoker列表,求和總權
    重。在遍歷過程中,會對比每個 Invoker的權重,判斷所有 Invoker的權重是否相同。
  2. 如果權重相同,則說明每個 Invoker的概率都一樣,因此直接用 nextInt隨機選一個
    返回即可
  3. 如果權重不同,則首先得到偏移值,然後根據偏移值找到對應的 Invoker

在這裏插入圖片描述

示例:

假設有4個Invoker,它們權重分別是1234,則總權重是1+2+3+4=10。
說明每個Invoker分別有1/102/103/104/10的概率被選中。
然後nextInt(10)會返回0~10之間的一個整數,假設爲5.
如果進行類減,則減到3會小於0,此時會落入3的區間,即選擇3號Invoker:

  1   2     3      4
|__|____|______|________|5

RoundRobin負載均衡

權重輪詢負載均衡會根據設置的權重來判斷輪詢的比例。普通輪詢負載均衡的好處是每個節點獲得的請求會很均勻,如果某些節點的負載能力明顯較弱,則這個節點會堆積比較多的請求。

因此普通的輪詢還不能滿足需求,還需要能根據節點權重進行干預。權重輪詢又分爲普通權重輪詢和平滑權重輪詢。普通權重輪詢會造成某個節點會突然被頻繁選中,這樣很容易突然讓一個節點流量暴增。Nginx 中有一種叫 平滑輪詢的算法( smooth weighted rund-robion balancing),這種算法在輪詢時會穿插選擇其他節點,讓整個服務器選擇的過程比較均勻,不會“逮住”個節點一直調用。Dubbo框架中最新的RoundRobin代碼已經改爲平滑權重輪詢算法。

先來看一下 Dubbo中RoundRobin負載均衡的工作步驟,如下:

  1. 初始化權重緩存Map。以每個Invoker的URL爲key,對象WeightedRoundRobin爲vale生成一 個CocurentMap, 並把這個Map保存到全局的methodWeightMap 中: concurrentap<String, ConcurrentMap<string, WeightedRoundRobin>> methodweightMap。methodWeighMap的key是每個接口+方法名。這步只會生成這個緩存Map,但裏面是空的,第2步纔會生成每個Invoker對應的鍵值。

WeightedRoundRobin封裝了每個Invoker 的權重,對象中保存了三個屬性,如代碼所示:

private int weght;//Invoker設定的權重
//考慮到併發場景下某個Invoker會被同時選中,表示該節點被所有線程鑽中的權重總和
//例如:某節點權重是100,被4個線層同時選中,則變爲400
private AtomicLon current = new AtomicLong(0);

//最後一次更新的時間,用於後續緩存超時的判斷
private long lastUpdate;
  1. 遍歷所有lnoker首先,在遍歷的過程中把每個Invoker 的數據填充到第1步生成的權重緩存Map中。其次,獲取每個Invoker的預熱權重,新版的框架RoundRobin也支持預熱,通過和Random負載均衡中相同的方式獲得預熱階段的權重。如果預熱權重和Invoker 設置的權重不相等,則說明還在預熱階段,此時會以預熱權重爲準。然後,進行平滑輪詢。 每個Invoker會把權重加到自己的 current屬性上,並更新當前Invoker的lastUpdate。同時累加每個Invoke的權重到totalWeight。最終,遍歷完後,選出所有Invoker中current最大的作爲最終要調用的節點。

  2. 清除已經沒有使用的緩存節點。由於所有的Invoker 的權重都會被封裝成weightedRoundRobin對象,因此如果可調用的Invoker列表數量和緩存weightedRoundRobin對象的Map大小不相等,則說明緩存Map中有無用數據(有些Invoker已經不在了,但Map中還有緩存)。

    爲什麼不相等就說明有老數據呢?如果Invoker列表比緩存Map大,則說明有沒被緩存的Invoker,此時緩存Map會新增數據。因此緩存Map永遠大於等於Invoker列表。
    清除老舊數據時,各線程會先用CAS搶佔鎖(搶到鎖的線程才做清除操作,搶不到的線程就直接跳過,保證只有一個線程在 做清除操作),然後複製原有的 Map到一個新的Map中,根據lastUpdate清除新Map中的過期數據(默認60秒算過期),最後把Map從舊的Map引用修改到新的Map上面。這是一種CopyOnWrite的修改方式。

  3. 返回Invoker。 注意,返回之前會把當前Invoker的current減去總權重。這是平滑權重輪詢中重要的一步。

算法邏輯:

  1. 每次請求做負載均衡時,會遍歷所有可調用的節點(Invoker列表)。對於每個Invoker,讓它的current = current + weight。 屬性含義見weightedRoundRobin 對象。同時累加每個Invoker的weight到totalWeight,即totalWeight = totalweight + weight

  2. 遍歷完所有Invoker後,current值最大的節點就是本次要選擇的節點。最後,把該節點的current值減去totalWeight,即current = current - totalweight

假設有3個Invoker: A、B、C,它們的權重分別爲1、6、9,初始crretrt都是0,則平滑權重輪詢過程如表所示:

請求次數 被選中前Invoker的current值 被選中後Invoker的current值 被選中的節點
1 {1,6,9} {1,6,-7} C
2 {2,12,2} {2,-4,2} B
3 {3,2,11} {3,2,-5} C
4 {4,8,4} {4,-8,4} B
5 {5,-2,13} {5,-2,-3} C
6 {6,4,6} {-10,4,6} A
7 {-9,10,15} {-9,10,-1} C
8 {-8,16,8} {-8,0,8} B
9 {-7,6,17} {-7,6,1} C
10 {-6,12,10} {-6,-4,10} B
11 {-5,2,19} {-5,2,3} C
12 {-4,8,12} {-4,8,-4} C
13 {-3,14,5} {-3,-2,5} B
14 {-2,4,14} {-2,4,-2} C
15 {-1,10,7} {-1,-6,7} B
16 {0,0,16} {0,0,0} C

從這16次的負載均衡來看,A被調用了1次,B被調用了6次,C被調用了9次。符合權重輪詢的策略,因爲他們的權重比是1:6:9。此外,C並沒有被頻繁地一直調用,其中會穿插B和A的調用。

LeastActive負載均衡

LeastActive負載均衡稱爲最少活躍調用數負載均衡,即框架會記下每個Invoker的活躍數,每次只從活躍數最少的Invoker裏選一個節點。這個負載均衡算法需要配合ActiveLimitFilter過濾器來計算每個接口方法的活躍數。最少活躍負載均衡可以看作Random負載均衡的“加強版”,因爲最後根據權重做負載均衡的時候,使用的算法和Random的一樣。

在這裏插入圖片描述
在這裏插入圖片描述

遍歷所有Invoker,不斷尋找最小的活躍數(leastActive),如果有多個Invoker的活躍數都等於leastActive,則把它們保存到同一個集合中,最後在這個Invoker集合中再通過隨機的方式選出一個Invoker。

那最少活躍的計數又是如何知道的呢?

在ActiveLimitFilter中,只要進來一個請求,該方法的調用的計數就會原子性+1.整個Invoker調用過程會包在try-catch-finally中,無論調用或結束或出現異常,finally中都會把計數原子-1.該原子計數就是最少活躍數。

一致性Hash負載均衡

一致性 Hash負載均衡可以讓參數相同的請求每次都路由到相同的機器上。這種負載均衡的方式可以讓請求相對平均,相比直接使用Hash而言,當某些節點下線時,請求會平攤到其他服務提供者,不會引起劇烈變動。

								區域1
		服務A- - - - - - - - - - - - - - - 服務B
		  	|												 |
	區  	|												 |  區
	域  	|												 |  域
	4	  	|												 |   2
		  	|												 |
		  	|												 |
	  服務C- - - - - - - - - - - - - - - 服務D		
						 區域3

普通一致性Hash 會把每個服務節點散列到環形上,然後把請求的客戶端散列到環上,順時前找到的第 一個節點就是要調用的節點。假設客戶端落在區域2,則順時針找到的服務C這程調用的節點。當服務C宕機下線,則落在區域2部分的客戶端會自動遷移到服務D上。這樣就詔 避免了全部重新散列的問題。

普通的一致性Hash也有一定的侷限性,它的散列不一定均勻, 容易造成某些節點壓力大。因此Dubbo框架使用了優化過的 Ketama一致性Hash。這種算法會爲每個真實節點再創建多個節點, 讓節點在環形上的分佈更加均勻,後續的調用也會隨之更加均勻。

在這裏插入圖片描述

整個邏輯的核心在ConsistentHashSelector中,因此我們繼續來看ConsistentHashSelector是如何初始化的。ConsistentHashSelector初始化的時候會對節點進行散列,散列的環形是使用一個TreeMap實現的,所有的真實、虛擬節點都會放入TreeMap。把節點的IP+遞增數字做“MD5",以此作爲節點標識,再對標識做“Hash" 得到TreeMap 的key, 最後把可以調用的節點作爲TreeMap的value,如代碼所示。

在這裏插入圖片描述

TreeMap實現一致性Hash:在客戶端調用時候,只要對請求的參數也做"MD5"即可。雖然此時得到的MD5值不一定能對應到TreeMap中的一個 key,因爲每次的請求參數不同。但是由於TreeMap 是有序的樹形結構,所以我們可以調用TeeMap的ceilingEntry方法,用於返回一個至少大於或等於當前給定key的Entry,從而達到順時針往前找的效果。如果找不到,則使用firstEntry返回第一個節點。

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