持續輸出原創文章,這是why技術的第16篇原創文章
本文是對於Dubbo負載均衡策略之一的最小活躍數演算法的詳細分析。文中所示原始碼,沒有特別標註的地方均為2.6.0版本。
為什麼沒有用截止目前的最新的版本號2.7.4.1呢?因為2.6.0這個版本里面有兩個bug。從bug講起來,印象更加深刻。
最後會對2.6.0/2.6.5/2.7.4.1版本進行對比,通過對比學習,加深印象。
本文目錄
第一節:Demo準備。
本小節主要是為了演示方便,搭建了一個Demo服務。Demo中啟動三個服務端,負載均衡策略均是最小活躍數,權重各不相同。
第二節:斷點打在哪?
本小節主要是分享我看原始碼的方式。以及我們看原始碼時斷點如何設定,怎麼避免在原始碼裡面"瞎逛"。
第三節:模擬環境。
本小節主要是基於Demo的改造,模擬真實環境。在此過程中發現了問題,引申出下一小節。
第四節:active為什麼是0?
本小節主要介紹了RpcStatus類中的active欄位在最小活躍數演算法中所承擔的作用,以及其什麼時候發生變化。讓讀者明白為什麼需要在customer端配置ActiveLimitFilter攔截器。
第五節:剖析原始碼
本小節對於最小活躍數演算法的實現類進行了逐行程式碼的解讀,基本上在每一行程式碼上加入了註釋。屬於全文重點部分。
第六節:Bug在哪裡?
逐行解讀完原始碼後,引出了2.6.0版本最小活躍數演算法的兩個Bug。並通過2.6.0/2.6.5/2.7.4.1三個版本的異同點進行交叉對比,加深讀者印象。
**第七節:意外收穫 **
看官方文件的時候發現了一處小小的筆誤,我對其進行了修改並被merged。主要是介紹給開源專案貢獻程式碼的流程。
PS:前一到三節主要是分享我看原始碼的一點思路和技巧,如果你不感興趣可以直接從第四節開始看起。本文的重點是第四到第六節。
另:閱讀本文需要對Dubbo有一定的瞭解。
一.Demo準備
我看原始碼的習慣是先搞個Demo把除錯環境搭起來。然後帶著疑問去抽絲剝繭的Debug,不放過在這個過程中在腦海裡面一閃而過的任何疑問。
這篇文章分享的是Dubbo負載均衡策略之一最小活躍數(LeastActiveLoadBalance)。所以我先搭建一個Dubbo的專案,並啟動三個provider供consumer呼叫。
三個provider的loadbalance均配置的是leastactive。權重分別是預設權重、200、300。
**預設權重是多少?**後面看原始碼的時候,原始碼會告訴你。
三個不同的服務提供者會給呼叫方返回自己是什麼權重的服務。
啟動三個例項。(注:上面的provider.xml和DemoServiceImpl其實只有一個,每次啟動的時候手動修改埠、權重即可。)
到zookeeper上檢查一下,服務提供者是否正常:
可以看到三個服務提供者分別在20880、20881、20882埠。(每個紅框的最後5個數字就是埠號)。
最後,我們再看服務消費者。消費者很簡單,配置consumer.xml
直接呼叫介面並列印返回值即可。
二.斷點打在哪?
相信很多朋友也很想看原始碼,但是不知道從何處下手。處於一種在原始碼裡面"亂逛"的狀態,一圈逛下來,收穫並不大。
這一小節我想分享一下我是怎麼去看原始碼。首先我會帶著問題去原始碼裡面尋找答案,即有針對性的看原始碼。
如果是這種框架類的,正如上面寫的,我會先搭建一個簡單的Demo專案,然後Debug跟進去看。Debug的時候當然需要是設定斷點的,那麼這個斷點如何設定呢?
第一個斷點,毋庸置疑,是打在呼叫方法的地方,比如本文中,第一個斷點是在這個地方:
接下來怎麼辦?
你當然可以從第一個斷點處,一步一步的跟進去。但是在這個過程中,你發現了嗎?大多數情況你都是被原始碼牽著鼻子走的。本來你就只帶著一個問題去看原始碼的,有可能你Debug了十分鐘,還沒找到關鍵的程式碼。也有可能你Debug了十分鐘,問題從一個變成了無數個。
那麼我們怎麼避免被原始碼牽著四處亂逛呢?我們得找到一個突破口,還記得我在《很開心,在使用mybatis的過程中我踩到一個坑。》這篇文章中提到的逆向排查的方法嗎?這次的文章,我再次展示一下該方法。
看原始碼之前,我們得冷靜的分析。目標要十分明確,就是想要找到Dubbo最小活躍數演算法的具體實現類以及實現類的具體邏輯是什麼。根據我們的provider.xml裡面的:
很明顯,我們知道loadbalance是關鍵字。所以我們拿著loadbalance全域性搜尋,可以看到dubbo包下面的LoadBalance。
這是一個SPI介面com.alibaba.dubbo.rpc.cluster.LoadBalance:
其實現類為:
com.alibaba.dubbo.rpc.cluster.loadbalance.AbstractLoadBalance
AbstractLoadBalance是一個抽象類,該類裡面有一個抽象方法doSelect。這個抽象方法其中的一個實現類就是我們要分析的最少活躍次數負載均衡的原始碼。
同時,到這裡。我們知道了LoadBalance是一個SPI介面,說明我們可以擴充套件自己的負載均衡策略。抽象方法doSelect有四個實現類。這個四個實現類,就是Dubbo官方提供的負載均衡策略,他們分別是:
ConsistentHashLoadBalance 一致性雜湊演算法
LeastActiveLoadBalance 最小活躍數演算法
RandomLoadBalance 加權隨機演算法
RoundRobinLoadBalance 加權輪詢演算法
我們已經找到了LeastActiveLoadBalance這個類了,那麼我們的第二個斷點打在哪裡已經很明確了。
目前看來,兩個斷點就可以支撐我們的分析了。
有的朋友可能想問,那我想知道Dubbo是怎麼識別出我們想要的是最少活躍次數演算法,而不是其他的演算法呢?其他的演算法是怎麼實現的呢?從第一個斷點到第二個斷點直接有著怎樣的呼叫鏈呢?
**在沒有徹底搞清楚最少活躍數演算法之前,這些統統先記錄在案但不予理睬。**一定要明確目標,帶著一個問題進來,就先把帶來的問題解決了。之後再去解決在這個過程中碰到的其他問題。**在這樣環環相扣解決問題的過程中,你就慢慢的把握了原始碼的精髓。**這是我個人的一點看原始碼的心得。供諸君參考。
三.模擬環境
既然叫做最小活躍數策略。那我們得讓現有的三個消費者都有一些呼叫次數。所以我們得改造一下服務提供者和消費者。
服務提供者端的改造如下:
PS:這裡以權重為300的服務端為例。另外的兩個服務端改造點相同。
客戶端的改造點如下(PS:for迴圈中i應該是<20):
一共傳送21個請求:其中前20個先發到服務端讓其hold住(因為服務端有sleep),最後一個請求就是我們需要Debug跟蹤的請求。
執行一下,讓程式停在斷點的地方,然後看看控制檯的輸出:
權重為300的服務端共計收到9個請求
權重為200的服務端共計收到6個請求
預設權重的服務端共計收到5個請求
我們還有一個請求在Debug。直接進入到我們的第二個斷點的位置,並Debug到下圖所示的一行程式碼(可以點看檢視大圖):
正如上面這圖所說的:weight=100回答了一個問題,active=0提出的一個問題。
weight=100回答了什麼問題呢?
預設權重是多少?是100。
我們服務端的活躍數分別應該是下面這樣的
權重為300的服務端,active=9
權重為200的服務端,active=6
預設權重(100)的服務端,active=5
但是這裡為什麼active會等於0呢?這是一個問題。
繼續往下Debug你會發現,每一個服務端的active都是0。所以相比之下沒有一個invoker有最小active。於是程式走到了根據權重選擇invoker的邏輯中。
四.active為什麼是0?
active為0說明在dubbo呼叫的過程中active並沒有發生變化。那active為什麼是0,其實就是在問active什麼時候發生變化?
要回答這個問題我們得知道active是在哪裡定義的,因為在其定義的地方,必有其修改的方法。
下面這圖說明了active是定義在RpcStatus類裡面的一個型別為AtomicInteger的成員變數。
在RpcStatus類中,有三處()呼叫active值的方法,一個增加、一個減少、一個獲取:
很明顯,我們需要看的是第一個,在哪裡增加。
所以我們找到了beginCount(URL,String)方法,該方法只有兩個Filter呼叫。ActiveLimitFilter,見名知意,這就是我們要找的東西。
com.alibaba.dubbo.rpc.filter.ActiveLimitFilter具體如下:
看到這裡,我們就知道怎麼去回答這個問題了:**為什麼active是0呢?因為在客戶端沒有配置ActiveLimitFilter。**所以,ActiveLimitFilter沒有生效,導致active沒有發生變化。
怎麼讓其生效呢?已經呼之欲出了。
好了,再來試驗一次:
加上Filter之後,我們通過Debug可以看到,對應權重的活躍數就和我們預期的是一致的了。
權重為300的活躍數為6
權重為200的活躍數為11
預設權重(100)的活躍數為3
根據活躍數我們可以分析出來,最後我們Debug住的這個請求,一定會選擇預設權重的invoker去執行,因為他是當前活躍數最小的invoker。如下所示:
雖然到這裡我們還沒開始進行原始碼的分析,只是把流程梳理清楚了。但是把Demo完整的搭建了起來,而且知道了最少活躍數負載均衡演算法必須配合ActiveLimitFilter使用,位於RpcStatus類的active欄位才會起作用,否則,它就是一個基於權重的演算法。
比起其他地方直接告訴你,要配置ActiveLimitFilter才行哦,我們自己實驗得出的結論,能讓我們的印象更加深刻。
我們再仔細看一下加上ActiveLimitFilter之後的各個服務的活躍數情況:
權重為300的活躍數為6
權重為200的活躍數為11
預設權重(100)的活躍數為3
你不覺得奇怪嗎,為什麼權重為200的活躍數是最高的?
其在業務上的含義是:我們有三臺效能各異的伺服器,A伺服器效能最好,所以權重為300,B伺服器效能中等,所以權重為200,C伺服器效能最差,所以權重為100。
當我們選擇最小活躍次數的負載均衡演算法時,我們期望的是效能最好的A伺服器承擔更多的請求,而真實的情況是效能中等的B伺服器承擔的請求更多。這與我們的設定相悖。
如果你說20個請求資料量太少,可能是巧合,不足以說明問題。說明你還沒被我帶偏,我們不能基於巧合程式設計。
所以為了驗證這個地方確實有問題,我把請求擴大到一萬個。
同時,記得擴大provider端的Dubbo執行緒池:
由於每個服務端執行的程式碼都是一樣的,所以我們期望的結果應該是權重最高的承擔更多的請求。但是最終的結果如圖所示:
各個伺服器均攤了請求。這就是我文章最開始的時候說的Dubbo 2.6.0版本中最小活躍數負載均衡演算法的Bug之一。
接下來,我們帶著這個問題,去分析原始碼。
五.剖析原始碼
com.alibaba.dubbo.rpc.cluster.loadbalance.LeastActiveLoadBalance的原始碼如下,我逐行進行了解讀。可以點開檢視大圖,細細品讀,非常爽:
下圖中紅框框起來的部分就是一個基於權重選擇invoker的邏輯:
我給大家畫圖分析一下:
請仔細分析圖中給出的舉例說明。同時,上面這圖也是按照比例畫的,可以直觀的看到,對於某一個請求,區間(權重)越大的伺服器,就越可能會承擔這個請求。所以,當請求足夠多的時候,各個伺服器承擔的請求數,應該就是區間,即權重的比值。
其中第81行有呼叫getWeight方法,位於抽象類AbstractLoadBalance中,也需要進行重點解讀的程式碼。
com.alibaba.dubbo.rpc.cluster.loadbalance.AbstractLoadBalance的原始碼如下,我也進行了大量的備註:
在AbstractLoadBalance類中提到了一個預熱的概念。官網中是這樣的介紹該功能的:
權重的計算過程主要用於保證當服務執行時長小於服務預熱時間時,對服務進行降權,避免讓服務在啟動之初就處於高負載狀態。服務預熱是一個優化手段,與此類似的還有 JVM 預熱。主要目的是讓服務啟動後“低功率”執行一段時間,使其效率慢慢提升至最佳狀態。
從上圖程式碼裡面的公式(演變後):計算後的權重=(uptime/warmup)*weight可以看出:隨著服務啟動時間的增加(uptime),計算後的權重會越來越接近weight。從實際場景的角度來看,隨著服務啟動時間的增加,服務承擔的流量會慢慢上升,沒有一個陡升的過程。所以這是一個優化手段。同時Dubbo介面還支援延遲暴露。
在仔細的看完上面的原始碼解析圖後,配合官網的總結加上我的靈魂畫作,相信你可以對最小活躍數負載均衡演算法有一個比較深入的理解:
1.遍歷 invokers 列表,尋找活躍數最小的 Invoker
2.如果有多個 Invoker 具有相同的最小活躍數,此時記錄下這些 Invoker 在 invokers 集合中的下標,並累加它們的權重,比較它們的權重值是否相等
3.如果只有一個 Invoker 具有最小的活躍數,此時直接返回該 Invoker 即可
4.如果有多個 Invoker 具有最小活躍數,且它們的權重不相等,此時處理方式和 RandomLoadBalance 一致
5.如果有多個 Invoker 具有最小活躍數,但它們的權重相等,此時隨機返回一個即可
所以我覺得最小活躍數負載均衡的全稱應該叫做:有最小活躍數用最小活躍數,沒有最小活躍數根據權重選擇,權重一樣則隨機返回的負載均衡演算法。
六.BUG在哪裡
Dubbo2.6.0最小活躍數演算法Bug一
問題出在標號為①和②這兩行程式碼中:
標號為①的程式碼在url中取出的是沒有經過getWeight方法降權處理的權重值,這個值會被累加到權重總和(totalWeight)中。
標號為②的程式碼取的是經過getWeight方法處理後的權重值。
取值的差異會導致一個問題,標號為②的程式碼的左邊,offsetWeight是一個在[0,totalWeight)範圍內的隨機數,右邊是經過getWeight方法降權後的權重。所以在經過leastCount次的迴圈減法後,offsetWeight在服務啟動時間還沒到熱啟動設定(預設10分鐘)的這段時間內,極大可能仍然大於0。導致不會進入到標號為③。直接到標號為④碼處,變成了隨機呼叫策略。這與設計不符,所以是個bug。
前面章節說的情況就是這個Bug導致的。
這個Bug對應的issues地址和pull request分為:
那怎麼修復的呢?我們直接對比Dubbo 2.7.4.1(目前最新版本)的程式碼:
可以看到獲取weight的方法變了:從url中直接獲取變成了通過getWeight方法獲取。獲取到的變數名稱也變了:從weight變成了afterWarmup,更加的見名知意。
還有一處變化是獲取隨機值的方法的變化,從Randmo變成了ThreadLoaclRandom,效能得到了提升。這處變化就不展開講了,有興趣的朋友可以去了解一下。
Dubbo2.6.0最小活躍數演算法Bug二
這個Bug我沒有遇到,但是我在官方文件上看了其描述(官方文件中的版本是2.6.4),引用如下:
官網上說這個問題在2.6.5版本進行修復。我對比了2.6.0/2.6.5/2.7.4.1三個版本,發現每個版本都略有不同。如下所示:
圖中標記為①的三處程式碼:
2.6.0版本的是有Bug的程式碼,原因在上面說過了。
2.6.5版本的修復方式是獲取隨機數的時候加一,所以取值範圍就從[0,totalWeight)變成了[0,totalWeight],這樣就可以避免這個問題。
2.7.4.1版本的取值範圍還是[0,totalWeight),但是它的修復方法體現在了標記為②的程式碼處。2.6.0/2.6.5版本標記為②的地方都是if(offsetWeight<=0),而2.7.4.1版本變成了if(offsetWeight<0)。
你品一品,是不是效果是一樣的,但是更加優雅了。
朋友們,魔鬼,都在細節裡啊!
七.意外收穫
在看官網文件負載均衡介紹的時候。發現了一處筆誤。所以我對其進行了修改並被merged。
可以看到,改動點也是一個非常小的地方。但是,我也為Dubbo社群貢獻了一份自己的力量。我是Dubbo文件的committer,簡稱"Dubbo committer"。
本小節主要是簡單的介紹一下給開源專案提pr的流程。
首先,fork專案到自己的倉庫中。然後執行以下命令,拉取專案並設定源:
git clone github.com/thisiswangh…
cd dubbo-website
git remote add upstream github.com/apache/dubb…
git remote set-url --push upstream no_push
建立本地分支:
git checkout -b xxxx
開發完成後提交程式碼:
git fetch upstream
git checkout master
git merge upstream/master git checkout -b xxxx git rebase master git push origin xxxx:xxxx
然後到git上建立pull request後,靜候通知。
最後說一句
之前也寫過Dubbo的文章《Dubbo 2.7新特性之非同步化改造》,通過對比Dubbo2.6.0/2.7.0/2.7.3版本的原始碼,分析Dubbo2.7 非同步化的改造的細節,可以看看哦。
才疏學淺,難免會有紕漏,如果你發現了錯誤的地方,還請你留言給我指出來,我對其加以修改。
感謝您的閱讀,我的訂閱號裡全是原創,十分歡迎並感謝您的關注。
以上。