1. 程式人生 > >【原創】Dubbo 2.7.5線上程模型上的優化

【原創】Dubbo 2.7.5線上程模型上的優化

這是why技術的第30篇原創文章

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

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

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

本文目錄

第一節:官方釋出

本小節主要是通過官方釋出的一篇名為《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的一個里程碑式的版本。

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

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

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

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

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

官網上的介紹

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

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

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

官方的配圖中,完全沒有體現出執行緒"池"的概念,也沒有體現出同步轉非同步的呼叫鏈路。僅僅是一個遠端呼叫請求的傳送與接收過程,至於響應的傳送與接收過程,這張圖中也沒有表現出來。

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

在客戶端,除了使用者執行緒外,還會有一個執行緒名稱為DubboClientHandler-ip:port的執行緒池,其預設實現是cache執行緒池。

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

上圖中的setThreadName方法,就是設定執行緒名稱:

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

可以清楚的看到,執行緒名稱如果沒有指定時,預設是DubboClientHandler-ip:port。

在服務端,除了有boss執行緒、worker執行緒(io執行緒),還有一個執行緒名稱為DubboServerHandler-ip:port的執行緒池,其預設實現是fixed執行緒池。

啟用執行緒池的dubbo.xml配置如下:

<dubbo:protocol name="dubbo" threadpool="xxx"/>

上面的xxx可以是fixed、cached、limited、eager,其中fixed是預設實現。當然由於是SPI,所以也可以自行擴充套件:

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

從SPI介面看來,fixed確實是預設值。

但是由於客戶端在初始化執行緒池之前,加了一行程式碼(之前說的93行),所以客戶端的預設實現是cached,服務端的預設實現是fixed。

我也看了之前的版本,至少在2.6.0時(更早之前的版本沒有檢視),客戶端的執行緒池的預設實現就是cached。

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

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

這裡配一個稍微詳細一點的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版本之前的執行緒模型存在的問題,準確的說,是客戶端執行緒模型存在的問題:

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

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

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

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

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

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

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

按照官網上的解釋:actives=20的含義是每個服務消費者每個方法最大併發呼叫數為20。

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

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

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

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

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

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

對於issue#4467,CodingSinger說:為什麼Dubbo對每一個連結都建立一個執行緒池?

從Dubbo 2.7.4.1的原始碼我們也可以看到確實是在WarppedChannelHandler建構函式裡面確實是為每一個連線都建立了一個執行緒池:

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

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

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

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

現有的設計就是:provider端預設共用一個執行緒池。consumer端是每個連結共享一個執行緒池。

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

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

這裡插一句:chickenlj是誰呢?

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

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

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

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

好了,我們接著往下看之前提到的issue#5490,劉軍大佬在2019年12月16日就說了,在2.7.5版本時會引入threadless executor機制,用於優化、增強客戶端執行緒模型。

threadless是什麼?

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

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

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

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

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

execute(Runnable)方法如下:

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

場景復現

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

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

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

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

新舊執行緒模型對比

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

老的執行緒模型

老的執行緒池模型如下,注意線條顏色:

1、業務執行緒發出請求,拿到一個 Future 例項。

2、業務執行緒緊接著呼叫 future.get 阻塞等待業務結果返回。 3、當業務資料返回後,交由獨立的 Consumer 端執行緒池進行反序列化等處理,並呼叫 future.set 將反序列化後的業務結果置回。 4、業務執行緒拿到結果直接返回。

新的執行緒模型

新的執行緒池模型如下,注意線條顏色:

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版本中,如下程式碼所示:

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

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

同時通過原始碼可以看到在獲取到Future例項後,緊接著呼叫了subscribeTo方法,實現方法如下:

用了Java 8的CompletableFuture,實現非同步程式設計。

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

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

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

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

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

第二步:業務執行緒緊接著呼叫 future.get 阻塞等待業務結果返回。

由於Dubbo預設是同步呼叫,而同步和非同步呼叫的區別我在第一篇文章《Dubbo 2.7新特性之非同步化改造》中就進行了詳細解析:

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

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

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

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

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

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

在呼叫 future.get() 之前(即呼叫標號為②的程式碼之前),先呼叫 ThreadlessExecutor.wait()(即標號為①處的邏輯),wait 會使業務執行緒在一個阻塞佇列上等待,直到佇列中被加入元素。

接下來再對比兩個地方:

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

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

AllChannelHandler是預設的策略,證明程式碼如下:

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

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

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

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

主要翻譯一下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(又名) 你的老舅。又畫彩虹又畫龍的。

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

Dubbo版本介紹

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

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

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

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

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

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

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

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

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

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

總之,牛逼。

最後說一句

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

感謝您的閱讀,我堅持原創,十分歡迎並感謝您的關注。

以上。

歡迎關注公眾號【why技術】。在這裡我會分享一些技術相關的東西,主攻java方向,用匠心敲程式碼,對每一行程式碼負責。偶爾也會荒腔走板的聊一聊生活,寫一寫書評,影評。願你我共同進步。

公眾號-why