本文是對於Dubbo負載均衡策略之一的最小活躍數算法的詳細分析。文中所示源碼,沒有特別標註的地方均爲2.6.0版本。git
爲何沒有用截止目前的最新的版本號2.7.4.1呢?由於2.6.0這個版本里面有兩個bug。從bug講起來,印象更加深入。github
最後會對2.6.0/2.6.5/2.7.4.1版本進行對比,經過對比學習,加深印象。web
第一節:Demo準備。算法
本小節主要是爲了演示方便,搭建了一個Demo服務。Demo中啓動三個服務端,負載均衡策略均是最小活躍數,權重各不相同。apache
第二節:斷點打在哪?編程
本小節主要是分享我看源碼的方式。以及咱們看源碼時斷點如何設置,怎麼避免在源碼裏面"瞎逛"。服務器
第三節:模擬環境。mybatis
本小節主要是基於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把調試環境搭起來。而後帶着疑問去抽絲剝繭的Debug,不放過在這個過程當中在腦海裏面一閃而過的任何疑問。
這篇文章分享的是Dubbo負載均衡策略之一最小活躍數(LeastActiveLoadBalance)。因此我先搭建一個Dubbo的項目,並啓動三個provider供consumer調用。
三個provider的loadbalance均配置的是leastactive。權重分別是默認權重、200、300。
默認權重是多少?後面看源碼的時候,源碼會告訴你。
三個不一樣的服務提供者會給調用方返回本身是什麼權重的服務。
啓動三個實例。(注:上面的provider.xml和DemoServiceImpl其實只有一個,每次啓動的時候手動修改端口、權重便可。)
到zookeeper上檢查一下,服務提供者是否正常:
能夠看到三個服務提供者分別在20880、2088一、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的服務端爲例。另外的兩個服務端改造點相同。
客戶端的改造點以下:
一共發送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說明在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 具備最小活躍數,但它們的權重相等,此時隨機返回一個便可
因此我以爲最小活躍數負載均衡的全稱應該叫作:有最小活躍數用最小活躍數,沒有最小活躍數根據權重選擇,權重同樣則隨機返回的負載均衡算法。
Dubbo2.6.0最小活躍數算法Bug一
問題出在標號爲①和②這兩行代碼中:
標號爲①的代碼在url中取出的是沒有通過getWeight方法降權處理的權重值,這個值會被累加到權重總和(totalWeight)中。
標號爲②的代碼取的是通過getWeight方法處理後的權重值。
取值的差別會致使一個問題,標號爲②的代碼的左邊,offsetWeight是一個在[0,totalWeight)範圍內的隨機數,右邊是通過getWeight方法降權後的權重。因此在通過leastCount次的循環減法後,offsetWeight在服務啓動時間還沒到熱啓動設置(默認10分鐘)的這段時間內,極大可能仍然大於0。致使不會進入到標號爲④的代碼中。直接到標號爲⑤的代碼處,變成了隨機調用策略。這與設計不符,因此是個bug。
前面章節說的狀況就是這個Bug致使的。
這個Bug對應的issues地址和pull request分爲:
https://github.com/apache/dubbo/issues/904
https://github.com/apache/dubbo/pull/2172
那怎麼修復的呢?咱們直接對比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 https://github.com/thisiswanghy/dubbo-website.git
cd dubbo-website
git remote add upstream https://github.com/apache/dubbo-website.git
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 異步化的改造的細節,能夠看看哦。
才疏學淺,不免會有紕漏,若是你發現了錯誤的地方,還請你留言給我指出來,我對其加以修改。
若是你以爲文章還不錯,你的轉發、分享、讚揚、點贊、留言就是對我最大的鼓勵。
感謝您的閱讀,十分歡迎並感謝您的關注。
以上。
再推銷一下我公衆號:對於寫文章,其實想到寫什麼內容並不難,難的是你對內容的把控。關於技術性的語言,我是反覆推敲,查閱大量文章來進行證僞,總之慎言慎言再慎言,畢竟作技術,我認爲是一件很是嚴謹的事情,我經常想象本身就是在故宮修文物的工匠,在工匠精神的認知上,目前我可能和他們還差的有點遠,可是我時常以工匠精神要求本身。就像我以前表達的:對於技術文章(由於我偶爾也會荒腔走板的聊一聊生活,寫一寫書評,影評),我儘可能保證周推,全力保證質量。堅持輸出原創。