這是why技術的第30篇原創文章 java
這多是全網第一篇解析Dubbo 2.7.5里程碑版本中的改進點之一:客戶端線程模型優化的文章。git
先勸退:文本共計8190字,54張圖。閱讀以前須要對Dubbo相關知識點有必定的基礎。內容比較硬核,勸君謹慎閱讀。github
讀不下去沒關係,我寫的真的很辛苦的,幫忙拉到最後點個贊吧。web
本小節主要是經過官方發佈的一篇名爲《Dubbo 發佈里程碑版本,性能提高30%》的文章做爲引子,引出本文所要分享的內容:客戶端線程模型優化。算法
在介紹優化後的消費端線程模型以前,先簡單的介紹一下Dubbo的線程模型是什麼。同時發現官方文檔對於該部分的介紹十分簡略,因此結合代碼對其進行補充說明。apache
經過一個issue串聯本小節,道出並分析一些消費端應用,當面臨須要消費大量服務且併發數比較大的大流量場景時(典型如網關類場景),常常會出現消費端線程數分配過多的問題。編程
經過第三節引出了新版本的解決方案,thredless。並對其進行一個簡單的介紹。跨域
因爲條件有限,場景復現起來比較麻煩,可是我在issues#890中發現了一個很好的終結,因此我搬過來了。緩存
本小節經過對比新老線程模型的調用流程,並對比2.7.4.1版本和2.7.5版本關鍵的代碼,起到一個導讀的做用。併發
趁着此次的版本升級,也趁機介紹一下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
那麼改進以前的線程模型到底存在什麼樣的問題呢?
在《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機制,用於優化、加強客戶端線程模型。
根據類上的說明咱們能夠知道:
這個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部分。
老的線程池模型以下,注意線條顏色:
一、業務線程發出請求,拿到一個 Future 實例。
二、業務線程緊接着調用 future.get 阻塞等待業務結果返回。 三、當業務數據返回後,交由獨立的 Consumer 端線程池進行反序列化等處理,並調用 future.set 將反序列化後的業務結果置回。 四、業務線程拿到結果直接返回。
新的線程池模型以下,注意線條顏色:
一、業務線程發出請求,拿到一個 Future 實例。 二、在調用 future.get() 以前,先調用 ThreadlessExecutor.wait(),wait 會使業務線程在一個阻塞隊列上等待,直到隊列中被加入元素。 三、當業務數據返回後,生成一個 Runnable Task 並放ThreadlessExecutor 隊列。 四、業務線程將 Task 取出並在本線程中執行反序列化業務數據並 set 到 Future。 五、業務線程拿到結果直接返回。
能夠看到,相比於老的線程池模型,新的線程模型由業務線程本身負責監測並解析返回結果,免去了額外的消費端線程池開銷。
接下來咱們對比一下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 社區目前主力維護的有 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方向,用匠心敲代碼,對每一行代碼負責。偶爾也會荒腔走板的聊一聊生活,寫一寫書評,影評。願你我共同進步。