持續輸出原創文章,關注我吧
本文是對於Dubbo負載均衡策略之一的加權隨機演算法的詳細分析。從2.6.4版本聊起,該版本在某些情況下存在著比較嚴重的效能問題。由問題入手,層層深入,瞭解該演算法在Dubbo中的演變過程,讀懂它的前世今生。
本文目錄
第一節:什麼是輪詢?
本小節主要是介紹輪詢演算法和其對應的優缺點。引出加權輪詢演算法。
第二節:什麼是加權輪詢?
本小節主要是介紹加權輪詢的概率,並和加權隨機演算法做對比。區分兩者之間的關係。
第三節:Dubbo 2.6.4版本的實現
本小節主要分析了Dubbo 2.6.4版本的原始碼,以及對呼叫過程進行了詳細的分析。並引出該版本的效能問題。
第四節:推翻,重建
針對Dubbo 2.6.4版本的效能問題,在對應的issue中進行了激烈的討論。並提出了第一版優化意見,時間複雜度優化到了常量級。但不久之後,又有人發現了該版本的其他問題,計算過程不夠平滑。
第五節:再推翻,再重建,平滑加權。
針對改進後的演算法還是不夠平滑的問題,最終藉助Nginx的思想,融入了平滑加權的過程,形成最終版。
什麼是輪詢?
在描述加權輪詢之前,先解釋一下什麼是輪詢演算法,如下圖所示:
假設我們有A、B、C三臺伺服器,共計處理6個請求,服務處理請求的情況如下:
第一個請求傳送給了A伺服器
第二個請求傳送給了B伺服器
第三個請求傳送給了C伺服器
第四個請求傳送給了A伺服器
第五個請求傳送給了B伺服器
第六個請求傳送給了C伺服器
......
上面這個例子演示的過程就叫做輪詢。可以看出,所謂輪詢就是將請求輪流分配給每臺伺服器。
輪詢的優點是無需記錄當前所有伺服器的連結狀態,所以它一種無狀態負載均衡演算法,實現簡單,適用於每臺伺服器效能相近的場景下。
輪詢的缺點也是顯而易見的,它的應用場景要求所有伺服器的效能都相同,非常的侷限。
大多數實際情況下,伺服器效能是各有差異,針對效能好的伺服器,我們需要讓它承擔更多的請求,即需要給它配上更高的權重。
所以加權輪詢,應運而生。
什麼是加權輪詢?
為了解決輪詢演算法應用場景的侷限性。當遇到每臺伺服器的效能不一致的情況,我們需要對輪詢過程進行加權,以調控每臺伺服器的負載。
經過加權後,每臺伺服器能夠得到的請求數比例,**接近或等於他們的權重比。**比如伺服器 A、B、C 權重比為 5:3:2。那麼在10次請求中,伺服器 A 將收到其中的5次請求,伺服器 B 會收到其中的3次請求,伺服器 C 則收到其中的2次請求。
這裡要和加權隨機演算法做區分哦。加權隨機我在《一文講透Dubbo負載均衡之最小活躍數演算法》中介紹過,直接把畫的圖拿過來:
上面這圖是按照比例畫的,可以直觀的看到,對於某一個請求,區間(權重)越大的伺服器,就越可能會承擔這個請求。所以,當請求足夠多的時候,各個伺服器承擔的請求數,應該就是區間,即權重的比值。
假設有A、B、C三臺伺服器,權重之比為5:3:2,一共處理10個請求。
那麼負載均衡採用加權隨機演算法時,很有可能A、B服務就處理完了這10個請求,因為它是隨機呼叫。
負載均衡採用輪詢加權演算法時,A、B、C服務一定是分別承擔5、3、2個請求。
Dubbo2.6.4版本的實現
對於Dubbo2.6.4版本的實現分析,可以看下圖,我加了很多註釋,其中的輸出語句都是我加的:
示例程式碼還是沿用之前文章中的Demo,不瞭解的可以檢視《一文講透Dubbo負載均衡之最小活躍數演算法》,本文分別在20881、20882、20883埠啟動三個服務,各自的權重分別為1,2,3。
客戶端呼叫8次:
輸出結果如下:
可以看到第七次呼叫後mod=0,回到了第一次呼叫的狀態。形成了一個閉環。
再看看判斷的條件是什麼:
其中mod在程式碼中扮演了極其重要的角色,mod根據一個方法的呼叫次數不同而不同,取值範圍是[0,weightSum)。
因為weightSum=6,所以列舉mod不同值時,最終的選擇結果和權重變化:
可以看到20881,20882,20883承擔的請求數量比值為1:2:3。同時我們可以看出,當 mod >= 1 後,20881埠的服務就不會被選中了,因為它的權重被減為0了。當 mod >= 4 後,20882埠的服務就不會被選中了,因為它的權重被減為0了。
結合判斷條件和輸出結果,我們詳細分析一下(下面內容稍微有點繞,如果看不懂,多結合上面的圖片看幾次):
- 第一次呼叫
mod=0,第一次迴圈就滿足程式碼塊①的條件,直接返回當前迴圈的invoker,即20881埠的服務。此時各埠的權重情況如下:
- 第二次呼叫
mod=1,需要進入程式碼塊②,對mod進行一次遞減。
第一次迴圈對20881埠的服務權重減一,mod-1=0。
第二次迴圈,mod=0,迴圈物件是20882埠的服務,權重為2,滿足程式碼塊①,返回當前迴圈的20882埠的服務,此時各埠的權重情況如下:
- 第三次呼叫
mod=2,需要進入程式碼塊②,對mod進行兩次遞減。
第一次迴圈對20881埠的服務權重減一,mod-1=1;
第二次迴圈對20882埠的服務權重減一,mod-1=0;
第三次迴圈時,mod已經為0,當前迴圈的是20883埠的服務,權重為3,滿足程式碼塊①,返回當前迴圈的20883埠的服務,此時各埠的權重情況如下:
- 第四次呼叫
mod=3,需要進入程式碼塊②,對mod進行三次遞減。
第一次迴圈對20881埠的服務權重減一,從1變為0,mod-1=2;
第二次迴圈對20882埠的服務權重減一,從2變為1,mod-1=1;
第三次迴圈對20883埠的服務權重減一,從3變為2,mod-1=0;
第四次迴圈的是20881埠的服務,此時mod已經為0,但是20881埠的服務的權重已經變為0了,不滿足程式碼塊①和程式碼塊②,進入第五次迴圈。
第五次迴圈的是20882埠的服務,當前權重為1,mod=0,滿足程式碼塊①,返回20882埠的服務,此時各埠的權重情況如下:
- 第五次呼叫
mod=4,需要進入程式碼塊②,對mod進行四次遞減。
第一次迴圈對20881埠的服務權重減一,從1變為0,mod-1=3;
第二次迴圈對20882埠的服務權重減一,從2變為1,mod-1=2;
第三次迴圈對20883埠的服務權重減一,從3變為2,mod-1=1;
第四次迴圈的是20881埠的服務,此時mod為1,但是20881埠的服務的權重已經變為0了,不滿足程式碼塊②,mod不變,進入第五次迴圈。
第五次迴圈時,mod為1,迴圈物件是20882埠的服務,權重為1,滿足程式碼塊②,權重從1變為0,mod從1變為0,進入第六次迴圈。
第六次迴圈時,mod為0,迴圈物件是20883埠的服務,權重為2,滿足條件①,返回當前20883埠的服務,此時各埠的權重情況如下:
- 第六次呼叫
第六次呼叫,mod=5,會迴圈九次,最終選擇20883埠的服務,讀者可以自行分析一波,分析出來了,就瞭解的透透的了。
- 第七次呼叫
第七次呼叫,又回到mod=0的狀態:
2.6.4版本的加權輪詢就分析完了,但是事情並沒有這麼簡單。這個版本的加權輪詢是有效能問題的。
該問題對應的issue地址如下:
問題出現在invoker返回的時機上:
擷取issue裡面的一個回答:
10分鐘才選出一個invoker,還怎麼玩?
有時間可以讀一讀這個issue,裡面各路大神針對該問題進行了激烈的討論,第一種改造方案被接受後,很快就被推翻,被第二種方案代替,可以說優化思路十分值得學習,很精彩,接下來的行文路線就是按照該issue展開的。
推翻,重建。
上面的程式碼時間複雜度是O(mod),而第一次修復之後時間複雜度降低到了常量級別。可以說是一次非常優秀的優化,值得我們學習,看一下優化之後的程式碼,我加了很多註釋:
其關鍵優化的點是這段程式碼,我加入輸出語句,便於分析。
輸出日誌如下:
把上面的輸出轉化到表格中去,7次請求的選擇過程如下:
該演算法的原理是:
把服務端都放到集合中(invokerToWeightList),然後獲取服務端個數(length),並計算出服務端權重最大的值(maxWeight)。
index表示本次請求到來時,處理該請求的服務端下標,初始值為0,取值範圍是[0,length)。
currentWeight表示當前排程的權重,初始值為0,取值範圍是[0,maxWeight)。
當請求到來時,從index(就是0)開始輪詢服務端集合(invokerToWeightList),如果是一輪迴圈的開始(index=0)時,則對currentWeight進行加一操作(不會超過maxWeight),在迴圈中找出第一個權重大於currentWeight的服務並返回。
這裡說的一輪迴圈是指index再次變為0所經歷過的迴圈,這裡可以把index=0看做是一輪迴圈的開始。每一輪迴圈的次數與Invoker的數量有關,Invoker數量通常不會太多,所以我們可以認為上面程式碼的時間複雜度為常數級。
從issue上看出,這個演算法最終被merged了。
但是很快又被推翻了:
**這個演算法不夠平滑。**什麼意思呢?
翻譯一下上面的內容就是:伺服器[A, B, C]對應權重[5, 1, 1]。進行7次負載均衡後,選擇出來的序列為[A, A, A, A, A, B, C]。前5個請求全部都落在了伺服器A上,這將會使伺服器A短時間內接收大量的請求,壓力陡增。而B和C此時無請求,處於空閒狀態。而我們期望的結果是這樣的[A, A, B, A, C, A, A],不同伺服器可以穿插獲取請求。
我們設定20881埠的權重為5,20882、20883埠的權重均為1。
進行實驗,發現確實如此:可以看到一共進行7次請求,第1次到5次請求都分發給了權重為5的20881埠的服務,前五次請求,20881和20882都處於空閒狀態:
轉化為表格如下:
從表格的最終結果一欄也可以直觀的看出,七次請求對應的伺服器埠為:
分佈確實不夠均勻。
再推翻,再重建,平滑加權。
從issue中可以看到,再次重構的加權演算法的靈感來源是Nginx的平滑加權輪詢負載均衡:
看程式碼之前,先介紹其計算過程。
假設每個伺服器有兩個權重,一個是配置的weight,不會變化,一個是currentWeight會動態調整,初始值為0。當有新的請求進來時,遍歷伺服器列表,讓它的currentWeight加上自身權重。遍歷完成後,找到最大的currentWeight,並將其減去權重總和,然後返回相應的伺服器即可。
如果你還是不知道上面的表格是如何算出來的,我再給你詳細的分析一下第1、2個請求的計算過程:
第一個請求計算過程如下:
第二個請求計算過程如下:
後面的請求你就可以自己分析了。
從表格的最終結果一欄也可以直觀的看出,七次請求對應的伺服器埠為:
可以看到,權重之比同樣是5:1:1,但是最終的請求分發的就比較的"平滑"。對比一下:
對於平滑加權演算法,我想多說一句。我覺得這個演算法非常的神奇,我是徹底的明白了它每一步的計算過程,知道它最終會形成一個閉環,但是我想了很久,我還是不知道背後的數學原理是什麼,不明白為什麼會形成一個閉環,非常的神奇。
但是我們只要能夠理解我前面所表達的平滑加權輪詢演算法的計算過程,知道其最終會形成閉環,就能理解下面的程式碼。配合程式碼中的註釋食用,效果更佳。以下程式碼以及註釋來源官網:
最後說一句
Dubbo官方提供了四種負載均衡演算法,分別是:
ConsistentHashLoadBalance 一致性雜湊演算法
LeastActiveLoadBalance 最小活躍數演算法
RandomLoadBalance 加權隨機演算法
RoundRobinLoadBalance 加權輪詢演算法
對於官方提供的加權隨機演算法,原理十分簡單。所以在《一文講透Dubbo負載均衡之最小活躍數演算法》中也提到過。
本文是Dubbo負載均衡演算法的最後一篇。前兩篇為:
《一文講透Dubbo負載均衡之最小活躍數演算法》
《Dubbo一致性雜湊負載均衡的原始碼和Bug,瞭解一下?》
至此,Dubbo的負載均衡演算法都已分享完成。
才疏學淺,難免會有紕漏,如果你發現了錯誤的地方,還請你留言給我指出來,我對其加以修改。
如果你覺得文章還不錯,你的轉發、分享、讚賞、點贊、留言就是對我最大的鼓勵。
感謝您的閱讀**,**十分歡迎並感謝您的關注。
以上。
原創不易,求個關注。