Dubbo 2.7.5在線程模型上的優化

Dubbo 2.7.5在線程模型上的優化

這可能是全網第一篇解析Dubbo 2.7.5里程碑版本中的改進點之一:客戶端線程模型優化的文章。

先勸退:文本共計8190字,54張圖。閱讀之前需要對Dubbo相關知識點有一定的基礎。內容比較硬核,勸君謹慎閱讀。

Dubbo 2.7.5在線程模型上的優化

讀不下去不要緊,我寫的真的很辛苦的,幫忙拉到最後點個贊吧。

本文目錄

第一節:官方發佈

本小節主要是通過官方發佈的一篇名為《Dubbo 發佈里程碑版本,性能提升30%》的文章作為引子,引出本文所要分享的內容:客戶端線程模型優化。

第二節:官網上的介紹

在介紹優化後的消費端線程模型之前,先簡單的介紹一下Dubbo的線程模型是什麼。同時發現官方文檔對於該部分的介紹十分簡略,所以結合代碼對其進行補充說明。

第三節:2.7.5版本之前的線程模型的問題

通過一個issue串聯本小節,道出並分析一些消費端應用,當面臨需要消費大量服務且併發數比較大的大流量場景時(典型如網關類場景),經常會出現消費端線程數分配過多的問題。

第四節:thredless是什麼

通過第三節引出了新版本的解決方案,thredless。並對其進行一個簡單的介紹。

第五節:場景復現

由於條件有限,場景復現起來比較麻煩,但是我在issues#890中發現了一個很好的終結,所以我搬過來了。

第六節:新舊線程模型對比

本小節通過對比新老線程模型的調用流程,並對比2.7.4.1版本和2.7.5版本關鍵的代碼,起到一個導讀的作用。

第七節:Dubbo版本介紹。

趁著這次的版本升級,也趁機介紹一下Dubbo目前的兩個主要版本:2.6.X和2.7.X。

官方發佈

2020年1月9日,阿里巴巴中間件發佈名為《Dubbo 發佈里程碑版本,性能提升30%》的文章:

Dubbo 2.7.5在線程模型上的優化

文章中說這是Dubbo的一個里程碑式的版本。

在閱讀了相關內容後,我發現這確實是一個里程碑式的跨域,對於Dubbo坎坷的一生來說,這是展現其強大的生命力和積極探索精神的一個版本。

強大的生命力體現在新版本發佈後眾多的或讚揚、或吐槽的社區反饋。

探索精神體現在Dubbo在多語言和協議穿透性上的探索。

在文章中列舉了9大改造點,本文僅介紹2.7.5版本中的一個改造點:優化後的消費端線程模型。

本文大部分源碼為2.7.5版本,同時也會有2.7.4.1版本的源碼作為對比。

官網上的介紹

在介紹優化後的消費端線程模型之前,先簡單的介紹一下Dubbo的線程模型是什麼。

直接看官方文檔中的描述,Dubbo官方文檔是一份非常不錯的入門學習的文檔,很多知識點都寫的非常詳細。

可惜,在線程模型這塊,差強人意,寥寥數語,圖不達意:

Dubbo 2.7.5在線程模型上的優化

官方的配圖中,完全沒有體現出線程"池"的概念,也沒有體現出同步轉異步的調用鏈路。僅僅是一個遠程調用請求的發送與接收過程,至於響應的發送與接收過程,這張圖中也沒有表現出來。

所以我結合官方文檔和2.7.5版本的源碼進行一個簡要的介紹,在閱讀源碼的過程中你會發現:

在客戶端,除了用戶線程外,還會有一個線程名稱為DubboClientHandler-ip:port的線程池,其默認實現是cache線程池。

Dubbo 2.7.5在線程模型上的優化

上圖的第93行代碼的含義是,當客戶端沒有指定threadpool時,採用cached實現方式。

上圖中的setThreadName方法,就是設置線程名稱:

org.apache.dubbo.common.utils.ExecutorUtil#setThreadName

Dubbo 2.7.5在線程模型上的優化

可以清楚的看到,線程名稱如果沒有指定時,默認是DubboClientHandler-ip:port。

在服務端,除了有boss線程、worker線程(io線程),還有一個線程名稱為DubboServerHandler-ip:port的線程池,其默認實現是fixed線程池。

Dubbo 2.7.5在線程模型上的優化

啟用線程池的dubbo.xml配置如下:

上面的xxx可以是fixed、cached、limited、eager,其中fixed是默認實現。當然由於是SPI,所以也可以自行擴展:

Dubbo 2.7.5在線程模型上的優化

所以,基於最新2.7.5版本,官方文檔下面紅框框起來的這個地方,描述的有誤導性:

Dubbo 2.7.5在線程模型上的優化

從SPI接口看來,fixed確實是缺省值。

但是由於客戶端在初始化線程池之前,加了一行代碼(之前說的93行),所以客戶端的默認實現是cached,服務端的默認實現是fixed。

我也看了之前的版本,至少在2.6.0時(更早之前的版本沒有查看),客戶端的線程池的默認實現就是cached。

關於Dispatcher部分的描述是沒有問題的:

Dubbo 2.7.5在線程模型上的優化

Dispatcher部分是線程模型中一個比較重要的點,後面會提到。

這裡配一個稍微詳細一點的2.7.5版本之前的線程模型,供大家參考:

Dubbo 2.7.5在線程模型上的優化

圖片來源:https://github.com/apache/dubbo/issues/890

2.7.5之前的線程模型的問題

那麼改進之前的線程模型到底存在什麼樣的問題呢?

在《Dubbo 發佈里程碑版本,性能提升30%》一文中,是這樣描述的:

對 2.7.5 版本之前的 Dubbo 應用,尤其是一些消費端應用,當面臨需要消費大量服務且併發數比較大的大流量場景時(典型如網關類場景),經常會出現消費端線程數分配過多的問題。

同時文章給出了一個issue的鏈接:

https://github.com/apache/dubbo/issues/2013

這一小節,我就順著這個issue#2013給大家捋一下Dubbo 2.7.5版本之前的線程模型存在的問題,準確的說,是客戶端線程模型存在的問題:

Dubbo 2.7.5在線程模型上的優化

首先,Jaskey說到,分析了issue#1932,他說在某些情況下,會創建非常多的線程,因此進程會出現OOM的問題。

在分析了這個問題之後,他發現客戶端使用了一個緩存線程池(就是我們前面說的客戶端線程實現方式是cached),它並沒有限制線程大小,這是根本原因。

接下來,我們去issue#1932看看是怎麼說的:

https://github.com/apache/dubbo/issues/1932

Dubbo 2.7.5在線程模型上的優化

可以看到issue#1932也是Jaskey提出的,他主要傳達了一個意思:為什麼我設置了actives=20,但是在客戶端卻有超過10000個線程名稱為DubboClientHandler的線程的狀態為blocked?這是不是一個Bug呢?

僅就這個issue,我先回答一下這個:不是Bug!

我們先看看actives=20的含義是什麼:

Dubbo 2.7.5在線程模型上的優化

按照官網上的解釋:

actives=20的含義是每個服務消費者每個方法最大併發調用數為20。

也就是說,服務端提供一個方法,客戶端調用該方法,同時最多允許20個請求調用,但是客戶端的線程模型是cached,接受到請求後,可以把請求都緩存到線程池中去。所以在大量的比較耗時的請求的場景下,客戶端的線程數遠遠超過20。

這個actives配置在《一文講透Dubbo負載均衡之最小活躍數算法》這篇文章中也有說明。它的生效需要配合ActiveLimitFilter過濾器,actives的默認值為0,表示不限制。當actives>0時,ActiveLimitFilter自動生效。由於不是本文重點,就不在這裡詳細說明了,有興趣的可以閱讀之前的文章。

順著issue#2013捋下去,我們可以看到issue#1896提到的這個問題:

Dubbo 2.7.5在線程模型上的優化

問題1我已經在前面解釋了,他這裡的猜測前半句對,後半句錯。不再多說。

這裡主要看問題2(可以點開大圖看看):服務提供者多了,消費端維護的線程池就多了。導致雖然服務提供者的能力大了,但是消費端有了巨大的線程消耗。他和下面issue#4467的哥們表達的是同一個意思:想要的是一個共享的線程池。

我們接著往下捋,可以發現issue#4467和issue#5490

Dubbo 2.7.5在線程模型上的優化

對於issue#4467,CodingSinger說:為什麼Dubbo對每一個鏈接都創建一個線程池?

Dubbo 2.7.5在線程模型上的優化

從Dubbo 2.7.4.1的源碼我們也可以看到確實是在WarppedChannelHandler構造函數里面確實是為每一個連接都創建了一個線程池:

Dubbo 2.7.5在線程模型上的優化

issue#4467想要表達的是什麼意思呢?

就是這個地方為什麼要做鏈接級別的線程隔離,一個客戶端,就算有多個連接都應該用共享線程池呀?

我個人也覺得這個地方不應該做線程隔離。線程隔離的使用場景應該是針對一些特別重要的方法或者特別慢的方法或者功能差異較大的方法。很顯然,Dubbo的客戶端就算一個方法有多個連接(配置了connections參數),也是一視同仁,不太符合線程隔離的使用場景。

然後chickenij大佬在2019年7月24日回覆了這個issue:

Dubbo 2.7.5在線程模型上的優化

現有的設計就是:provider端默認共用一個線程池。consumer端是每個鏈接共享一個線程池。

同時他也說了:對於consumer線程池,當前正在嘗試優化中。

言外之意是他也覺得現有的consumer端的線程模型也是有優化空間的。

這裡插一句:chickenlj是誰呢?

劉軍,GitHub賬號Chickenlj,Apache Dubbo PMC,項目核心維護者,見證了Dubbo從重啟開源到Apache畢業的整個流程。現任職阿里云云原生應用平臺團隊,參與服務框架、微服務相關工作,目前主要在推動Dubbo開源的雲原生化。

Dubbo 2.7.5在線程模型上的優化

他這篇文章的作者呀,他的話還是很有分量的。

之前也在Dubbo開發者日成都站聽到過他的分享:

Dubbo 2.7.5在線程模型上的優化

如果對他演講的內容有興趣的朋友可以在公眾號的後臺回覆:1026。領取講師PPT和錄播地址。

好了,我們接著往下看之前提到的issue#5490,

劉軍大佬在2019年12月16日就說了,在2.7.5版本時會引入threadless executor機制,用於優化、增強客戶端線程模型。

Dubbo 2.7.5在線程模型上的優化

threadless是什麼?

Dubbo 2.7.5在線程模型上的優化

根據類上的說明我們可以知道:

這個Executor和其他正常Executor之間最重要的區別是這個Executor不管理任何線程。

通過execute(Runnable)方法提交給這個執行器的任務不會被調度到特定線程,而其他的Executor就把Runnable交給線程去執行了。

這些任務存儲在阻塞隊列中,只有當thead調用waitAndDrain()方法時才會真正執行。簡單來說就是,執行task的thead與調用waitAndDrain()方法的thead完全相同。

其中說到的waitAndDrain()方法如下:

Dubbo 2.7.5在線程模型上的優化

execute(Runnable)方法如下:

Dubbo 2.7.5在線程模型上的優化

同時我們還可以看到,裡面還維護了一個名稱叫做sharedExecutor的線程池。見名知意,我們就知道了,這裡應該是要做線程池共享了。

場景復現

上面說了這麼多2.7.5版本之前的線程模型的問題,我們怎麼復現一次呢?

我這裡條件有限,場景復現起來比較麻煩,但是我在issues#890中發現了一個很好的終結,我搬過來即可:

Dubbo 2.7.5在線程模型上的優化

根據他接下來的描述做出思維導圖如下:

Dubbo 2.7.5在線程模型上的優化

上面說的是corethreads大於0的場景。但是根據現有的線程模型,即使核心池數(corethreads)為0,當消費者應用依賴的服務提供者處理很慢時且請求併發量比較大時,也會出現消費者線程數很多問題。大家可以對比著看一下。

新舊線程模型對比

在之前的介紹中大家已經知道了,這次升級主要是增強客戶端線程模型,所以關於2.7.5版本之前和之後的線程池模型我們主要關心Consumer部分。

老的線程模型

老的線程池模型如下,注意線條顏色:

Dubbo 2.7.5在線程模型上的優化

1、業務線程發出請求,拿到一個 Future 實例。

2、業務線程緊接著調用 future.get 阻塞等待業務結果返回。 3、當業務數據返回後,交由獨立的 Consumer 端線程池進行反序列化等處理,並調用 future.set 將反序列化後的業務結果置回。 4、業務線程拿到結果直接返回。

新的線程模型

新的線程池模型如下,注意線條顏色:

Dubbo 2.7.5在線程模型上的優化

1、業務線程發出請求,拿到一個 Future 實例。 2、在調用 future.get() 之前,先調用 ThreadlessExecutor.wait(),wait 會使業務線程在一個阻塞隊列上等待,直到隊列中被加入元素。 3、當業務數據返回後,生成一個 Runnable Task 並放ThreadlessExecutor 隊列。 4、業務線程將 Task 取出並在本線程中執行反序列化業務數據並 set 到 Future。 5、業務線程拿到結果直接返回。

可以看到,相比於老的線程池模型,新的線程模型由業務線程自己負責監測並解析返回結果,免去了額外的消費端線程池開銷。

代碼對比

接下來我們對比一下2.7.4.1版本和2.7.5版本的代碼,來說明上面的變化。

需要注意的是,由於涉及到的變化代碼非常的多,我這裡僅僅起到一個導讀的作用,如果讀者想要詳細瞭解相關變化,還需要自己仔細閱讀源碼

首先兩個版本的第一步是一樣的:業務線程發出請求,拿到一個Future實例。

但是實現代碼卻有所差異,在2.7.4.1版本中,如下代碼所示:

Dubbo 2.7.5在線程模型上的優化

上圖圈起來的request方法最終會走到這個地方,可以看到確實是返回了一個Future實例:

Dubbo 2.7.5在線程模型上的優化

而newFuture方法源碼如下,請記住這個方法,後面會進行對比:

Dubbo 2.7.5在線程模型上的優化

同時通過源碼可以看到在獲取到Future實例後,緊接著調用了subscribeTo方法,實現方法如下:

Dubbo 2.7.5在線程模型上的優化

用了Java 8的CompletableFuture,實現異步編程。

但是在2.7.5版本中,如下代碼所示:

Dubbo 2.7.5在線程模型上的優化

在request方法中多了個executor參數,而該參數就是的實現類就是ThreadlessExecutor。

接下來,和之前的版本一樣,會通過newFuture方法去獲取一個DefaultFuture對象:

Dubbo 2.7.5在線程模型上的優化

通過和2.7.4.1版本的newFuture方法對比你會發現這個地方就大不一樣了。雖然都是獲取Future,但是Future裡面的內容不一樣了。

直接上個代碼對比圖,一目瞭然:

Dubbo 2.7.5在線程模型上的優化

第二步:業務線程緊接著調用 future.get 阻塞等待業務結果返回。

由於Dubbo默認是同步調用,而同步和異步調用的區別我在第一篇文章《Dubbo 2.7新特性之異步化改造》中就進行了詳細解析:

Dubbo 2.7.5在線程模型上的優化

我們找到異步轉同步的地方,先看2.7.4.1版本的如下代碼所示:

Dubbo 2.7.5在線程模型上的優化

而這裡的asyncResult.get()對應的源碼是,CompletableFuture.get():

Dubbo 2.7.5在線程模型上的優化

而在2.7.5版本中對應的地方發生了變化:

Dubbo 2.7.5在線程模型上的優化

變化就在這個asyncResult.get方法上。

在2.7.5版本中,該方法的實現源碼是:

Dubbo 2.7.5在線程模型上的優化

先說標號為②的地方,和2.7.4.1版本是一樣的,都是調用的CompletableFuture.get()。但是多了標號為①的代碼邏輯。而這段代碼就是之前新的線程模型裡面體現的地方,下面紅框框起來的部分:

Dubbo 2.7.5在線程模型上的優化

在調用 future.get() 之前(即調用標號為②的代碼之前),先調用 ThreadlessExecutor.wait()(即標號為①處的邏輯),wait 會使業務線程在一個阻塞隊列上等待,直到隊列中被加入元素。

接下來再對比兩個地方:

第一個地方:之前提到的WrappedChannelHandler,可以看到2.7.5版本其構造函數的改造非常大:

Dubbo 2.7.5在線程模型上的優化

第二個地方:之前提到的Dispatcher,是需要再寫一篇文章才能說的清楚的,我這僅僅是做一個拋磚引玉,提一下:

Dubbo 2.7.5在線程模型上的優化

AllChannelHandler是默認的策略,證明代碼如下:

Dubbo 2.7.5在線程模型上的優化

首先還是看標號為②的地方,看起來變化很大,其實就是對代碼進行了一個抽離,封裝。sendFeedback方法如下,和2.7.4.1版本中標號為②的地方的代碼是一樣的:

Dubbo 2.7.5在線程模型上的優化

所以我們重點對比一下兩個標號為①的地方,它們獲取executor的方法變了:

2.7.4.1版本的方法是getExecutorService()2.7.5版本的方法是getPreferredExecutorService()

代碼如下,大家品一品兩個版本之前的差異:

Dubbo 2.7.5在線程模型上的優化

主要翻譯一下getPreferredExecutorService方法上的註釋:

 
Currently, this method is mainly customized to facilitate the thread model on consumer side.1. Use ThreadlessExecutor, aka., delegate callback directly to the thread initiating the call. 2. Use shared executor to execute the callback.

目前,使用這種方法主要是為了客戶端的線程模型而定製的。

1.使用ThreadlessExceutor,aka.,將回調直接委託給發起調用的線程。 2.使用shared executor執行回調。

小聲說一句:這裡這個aka怎麼翻譯,我實在是不知道了。難道是嘻哈里面的AKA?大家好,我是寶石GEM,aka(又名) 你的老舅。又畫彩虹又畫龍的。

Dubbo 2.7.5在線程模型上的優化

好了,導讀就到這裡了。能看到這個地方的人我相信已經不多了。還是之前那句話由於涉及到的變化代碼非常的多,我這裡僅僅起到一個導讀的作用,如果讀者想要詳細瞭解相關變化,還需要自己仔細閱讀源碼。希望你能自己搭個Demo跑一跑,對比一下兩個版本的差異。

Dubbo版本介紹

趁著這次的版本升級,也趁機介紹一下Dubbo目前的主要版本吧。

據劉軍大佬的分享:Dubbo 社區目前主力維護的有 2.6.x 和 2.7.x 兩大版本,其中:

2.6.x 主要以 bugfix 和少量 enhancements 為主,因此能完全保證穩定性。

2.7.x 作為社區的主要開發版本,得到持續更新並增加了大量新 feature 和優化,同時也帶來了一些穩定性挑戰。

為方便 Dubbo 用戶升級,社區在以下表格對 Dubbo 的各個版本進行了總結,包括主要功能、穩定性和兼容性等,從多個方面評估每個版本,以期能幫助用戶完成升級評估:

Dubbo 2.7.5在線程模型上的優化

Dubbo 2.7.5在線程模型上的優化

可以看到社區對於最新的2.7.5版本的升級建議是:不建議大規模生產使用。

同時你去看Dubbo最新的issue,有很多都是對於2.7.5版本的"吐槽"。

但是我倒是覺得2.7.5是Dubbo發展進程中濃墨重彩的一筆,該版本打響了對於 Dubbo向整個微服務雲原生體系靠齊的第一槍。對於多語言的支持方向的探索。實現了對 HTTP/2 協議的支持,同時增加了與 Protobuf 的結合。

開源項目,共同維護。我們當然知道Dubbo不是一個完美的框架,但是我們也知道,它的背後有一群知道它不完美,但是仍然不言乏力、不言放棄的工程師,他們在努力改造它,讓它趨於完美。我們作為使用者,我們少一點"吐槽",多一點鼓勵。只有這樣我們才能驕傲的說,我們為開源世界貢獻了一點點的力量,我們相信它的明天會更好。

向開源致敬,向開源工程師致敬。

總之,牛逼。

最後說一句

才疏學淺,難免會有紕漏,如果你發現了錯誤的地方,還請你留言給我指出來,我對其加以修改。

以上。


分享到:


相關文章: