https://mp.weixin.qq.com/s/vE_u-8aF_D4ab8lmCFNwywhtml
你們好,我是 why,歡迎來到我連續周更優質原創文章的第 63 篇。老規矩,先荒腔走板聊聊其餘的。
java
上面這張圖片是我前幾天整理相冊的時候看到的。拍攝於 2016 年 8 月 20日,北京。程序員
那個時候我剛剛去北京沒多久,住在公司的提供的宿舍裏面。宿舍位於北京二環內的一個叫作東廊下的衚衕裏。web
位置極佳,條件極差。算法
我剛剛進入宿舍的時候,房間裏面只有一張大牀、一個矮矮的電視櫃、一個不能搖頭的風扇。個人房間也沒有空調,處處都是灰濛濛的,用衛生間都是去樓下的公共衛生間。數據庫
有一次北京下暴雨,我才發現窗戶那邊有一個缺口,雨下的太大,能夠順着那個缺口流下來,把個人鞋都打溼了。apache
宿舍裏面沒有冰箱,因此節假日我在宿舍只煮麪條或者用電飯煲作乾飯,而後就着各類醬吃。記得有一次週五領導請咱們吃飯,最後菜點多了,有幾個羊蹄動都沒動,領導就叫我打包帶回家。我帶回去,掛在牆上掛鉤,準備次日中午吃。次日一聞,壞了,也就沒有吃。小程序
宿舍裏面也沒有洗衣機,因此我在超市買了一個巨大的盆子,每週末的時候我會拿出一個下午的時間,邊看電視,邊手搓衣服,四季如此。c#
剛剛去北京的前一年,過的真的仍是很艱難的。可是宿舍的好處是離公司近,因此我基本上也不怎麼在宿舍呆着,工做日在公司學習到很晚,週末也去公司學習。數組
艱苦的環境更能激發人的鬥志。
可是我仍是簡單的裝飾了一下簡陋的出租屋,買了貼畫和綠植,由於我堅信房子是租來的,可是生活是本身的。
並且每週洗完衣服後我會用洗衣服的水再拖一下地。個人房間很小,擺上一張 1.5 米的大牀以後基本上就沒有什麼空間了,因此我用不上拖把,一張帕子就夠了。
我能夠蹲在地上,把房間裏面的每一塊地磚的邊邊角角都仔仔細細的擦拭一遍,而後跳到牀上去,靜靜的坐着,開始放空本身。
當時並沒以爲有什麼困難,可是和如今的生活再對比一下,真的是天壤之別。如今回想起,才真真正正的以爲:我曾經也在北京用力的生活過,離開的時候回憶滿滿,風華正茂。
就像我以前寫過的:北漂就像在黑屋子裏洗衣服,你不知道洗乾淨了沒有,只能一遍又一遍地去洗。等到離開北京的那一刻,燈光亮了,你發現,若是你認真洗過了,那件衣服光亮如新。讓你之後每次穿這件衣服都會想起那段歲月。
因此你呢,有沒有在用力的生活?
好了,說迴文章。
前段時間一位大佬指出了我以前文章中的一處錯誤:
是的,這個大佬就是公衆號【肥朝】的號主。
文章是這篇文章《Dubbo 2.7.5在線程模型上的優化》。
錯誤具體是指下面紅框框起來的這句話的描述:
而這段話,我是引用的官方內容。而如今這部份內容已經一字不差的加入到官網中了:
http://dubbo.apache.org/zh-cn/docs/user/demos/consumer-threadpool.html
通過驗證後發現確實官網上的描述是有問題的。
先說結論:2.7.5 版本以前,業務數據返回後,默認在 IO 線程裏面進行反序列化的操做。而2.7.5 版本以後,默認是延遲到客戶端線程池裏面進行反序列化的操做。
因此,對於官網中,上面紅框框起來這個地方的描述「交由獨立的 Consumer 端線程池進行反序列化處理」是有問題。
正確的說法應該是:在老的(2.7.5 版本以前)線程池模型中,當業務數據返回後,默認在 IO 線程上進行反序列化操做,若是配置了 decode.in.io 參數爲 false(默認爲 true),則延遲到獨立的客戶端線程池進行反序列化操做。
因此本文就主要分享兩個問題:
Dubbo 協議的設計與解析。
以 Dubbo 2.7.5 版本(由於線程池模型就是在這個版本變動的)爲分界線,對比不一樣版本之間,業務數據返回後,反序列化的操做究竟是在獨立的 Consumer 端線程池裏面進行的仍是在 IO 線程裏面進行的?
須要說明的是因爲本文須要作不一樣版本之間的對比,因此會涉及到兩個 Dubbo 版本,分別是 2.7.4.1 和 2.7.5 。寫的時候我都會標註清楚,你們看的時候和本身動手的時候須要注意一下。
另外再提早說明一下,文章有點長:若是你本身看 Dubbo 源碼,能夠先看總體,忽略細節。把總體摸個遍了以後,再去扣細節,精進源碼。本文就屬於扣細節,看的似懂非懂不要緊,先一鍵三連,而後收藏起來,你本身學的時候老是會學到這個地方來的,並且本文也不是一個很是可貴技術點。
若是你沒有學到,只能說明你潛入的深度仍是差了一點,也許你差一點就走到這個地方了,而後你想:算了吧,差很少得了。
可是你要知道,越往下,越難懂。而越難懂的,越值錢。
你想一想,正在抗住流量的東西,是你寫的那幾行代碼嗎?不是的,是你係統裏面用到的 Nginx、MQ、Redis、Dubbo、SpringCloud 等等這些中間件。而這些中間件裏面,抗住流量的,除了它們的集羣功能、容錯功能、限流熔斷、調用鏈路的優化等待這些手段以外,還有底層的網絡、IO、內存、數據結構、調度算法等待這些東西。
這是值錢的。
惋惜這些值錢的,很差講清楚,要說清楚就是長篇大論。因此我經常說的勸退長文都是說說而已的,你這麼愛學習,我怎麼會勸退你呢,鼓勵你都來不及呢,你說是吧?
再說了,我寫的長文,也並無涉及到這麼底層的東西。只是我沒有想過敷衍這事,我想把它作好了,儘可能把它寫清楚了,中間再夾雜着幾句「騷話」,因此寫着寫着就長了。
總之,你要堅信三點:
一:我沒有看懂,必定是由於這個博主寫的太爛。
二:我沒有看懂,理論上大多數人也應該看不懂。
三:我沒有看懂,那我本身研究一下得讓本身懂。
程序員就應該這樣,明明每天寫着這麼普通的 crud,可是聊起技術來倒是那麼的迷之自信。
爲何要先聊一下 Dubbo 的協議呢?
由於反序列化的時候涉及到一些響應頭(head)和響應體(body)解析的相關內容,是須要先進行一下鋪墊的。
首先去官網上擼個圖片過來:
能夠看到 Dubbo 數據包分爲消息頭(head)和消息體(body)。
消息頭用於存儲一些元信息,包括:魔數、數據包類型、調用方式、事件標識、序列化器編號、狀態、請求編號、消息體長度。
消息體中用於存儲具體的調用消息,包含七部份內容:
Dubbo 版本號(Dubbo version)
服務接口名(service name)
服務接口版本(service version)
方法名(method name)
參數類型(parameter types)
方法參數值(arguments)
上下文信息(attachments)
客服端發起請求的時候嚴格按照上面的順序寫入消息,服務端按照一樣的順序讀取消息,這樣就能解析出消息體裏面的內容。
對於協議字段的解析,官網上也是有詳細說明的。擼過來:
再具體的解釋一下,首先這圖得和協議圖一塊兒看,我怕你不會,再給你搞一張示意圖:
上面的截圖只是演示了三個對應關係,可是這兩張圖就是這樣看的。
我主要再解釋一下里面的某些字段。
第一個:魔數
做爲 Java 開發者,提到魔數,你第一個想到了什麼?
0xCAFEBABY,對吧。
每一個 class 文件的頭 4 個字節就是魔數,它的惟一做用就是肯定這個文件是否爲一個能被 JVM 接受的 class 文件。
在 Dubbo 中這個魔數是用來幹什麼的呢?
也許你不太清楚,可是我但願我一說你就能恍然大悟。由於你不悟,也不是本文要講的東西,我也很差給你解釋清楚。
它是用來解決網絡粘包/解包問題的。恍然大悟有沒有?
沒有?
對不起,本文不擴展相關內容。大學上《計算機網絡》課程的時候逃課處對象去了吧?
在 Dubbo 協議中,它的魔數:0xdabb。你能夠簡單的把它理解爲一個分隔符,用來解決粘包問題的。
第二個再說說:調用方式
首先這個字段僅在第 16 位設置爲 1 的狀況下有效。
從表裏面咱們能夠知道,第 16 位爲 1 就是指:request 請求。
在 rpc 中既然是 request ,那麼就分爲兩種調用方式:有去無回(單向)、有來有回(雙向)。
熟悉嗎?
不熟悉?呸,你個假粉絲,這張圖在個人文章中至少出現過兩次:
oneway 就是單向,其餘的調用類型都是有返回的。
因此調用分爲兩種類型,所以須要一個 bit 來存放調用方式。
第三個說說事件標識字段:
事件標識沒啥說的,取值裏面的描述也說的很清楚了。只是說明一下其中的 1 (心跳包),不在本次文章的分享範圍內。
第四個說說狀態字段:
狀態裏面有個省略號,說明沒有枚舉完。可是代碼裏面確定是齊的,這些狀態對應的代碼在這個類裏面,一共 11 個,給你們補充完整:org.apache.dubbo.remoting.exchange.Response
另外,再說一下返回的類型,講到後面的時候須要知道這個點。主要依據這個類裏面定義的字段:org.apache.dubbo.rpc.protocol.dubbo.DubboCodec
對應的代碼邏輯以下:org.apache.dubbo.rpc.protocol.dubbo.DubboCodec#encodeResponseData(org.apache.dubbo.remoting.Channel, org.apache.dubbo.common.serialize.ObjectOutput, java.lang.Object, java.lang.String)
這個方法從名稱也知道,是對響應數據作解碼操做的。
標號爲①的地方是判斷當前版本是否支持上下文信息傳遞。
標號爲②的地方是判斷是不是異常返回。
標號爲③的地方代表不是異常返回,則判斷返回值是否爲 null。
標號爲④的地方代表是正常返回,根據是否支持上下文信息傳遞,從而判斷是隻返回響應結果的仍是既有響應結果,也有上下文信息的返回類型。
標號爲⑤的地方代表是異常返回,根據是否支持上下文信息傳遞,從而判斷是隻返回異常結果的仍是既有異常結果,也有上下文信息的返回類型。
好了,寫到這裏,協議就差很少說完了。其實不難發現這個協議就是一個偏理論的東西,這就是一個你們的約定。
因此我記起以前在一個分享大會上,一位嘉賓說的:
跨語言特性實際是RPC層的支持,本質是協議層面的支持。
我如今對這句話的理解更加深入了。
跨語言,也就是服務異構的一種。
爲何我用 Java 發送 http 請求的時候能夠不用關心對方使用的是什麼開發語言?
由於你們都遵照了 http 協議,協議是能夠跨語言的。
Dubbo 這種 rpc 調用的框架也同樣。我發起遠程調用以後,只要你能按照咱們約定好的協議進行報文的解析,那你就能正常的處理我發過來的請求,我無論你的開發語言是什麼。
業務數據返回後,反序列化的操做究竟是在哪一個線程裏面進行的?
是在 IO 線程裏面直接解析,仍是被派發到客戶端線程池裏面進行解析?
這個問題咱們先試着在官網的線程模型介紹中去尋找答案。http://dubbo.apache.org/zh-cn/docs/user/demos/thread-model.html
在線程模型的描述裏面,是這樣寫的:
若是事件處理的邏輯能迅速完成,而且不會發起新的 IO 請求,好比只是在內存中記個標識,則直接在 IO 線程上處理更快,由於減小了線程池調度。
但若是事件處理邏輯較慢,或者須要發起新的 IO 請求,好比須要查詢數據庫,則必須派發到線程池,不然IO 線程阻塞,將致使不能接收其它請求。
若是用 IO 線程處理事件,又在事件處理過程當中發起新的 IO 請求,好比在鏈接事件中發起登陸請求,會報「可能引起死鎖」異常,但不會真死鎖。
所以,須要經過不一樣的派發策略和不一樣的線程池配置的組合來應對不一樣的場景。
本文不關心線程池配置,咱們只看派發策略:
默認的派發策略是 all。
一看到這幾個策略,熟悉 Dubbo 的朋友確定就知道了,按照 Dubbo 的尿性,這必須得是一個 SPI 接口啊。
果不其然,源碼裏面就是這樣的,你說巧不巧:
而後官方還給出了一張描述不太清晰的圖片:
圖片中的 Dispatch 就是派發策略發揮做用的地方。
因此咱們能從這部分得出一個結論:在默認的狀況下,客戶端接收到響應後,因爲 Dubbo 使用 all 的派發策略,會把響應請求派發到客戶端線程池中去。
那咱們能夠推導出:響應的解析必定是在客戶端線程池裏面進行的嗎?
不能夠,推不出來的。
只能說響應會進入客戶端線程池中去,可是這個響應多是一個通過解析後的響應,也多是一個沒有通過解析的響應。
因此,這個響應有可能在進入線程池以前就被解析過了。被誰解析?
IO 線程。
若是 IO 線程沒有解析,那就在客戶端線程裏面去解析。
根據上面這段話。咱們能提煉出一個關鍵語句,或者說是需求:咱們如今要實現響應報文能夠在不一樣的地方進行解析的功能,請問你怎麼作?
你用腳指頭想也知道了。確定是有一個 if 判斷的,判斷到底在哪(IO線程/客戶端線程池)進行響應解析。而這個 if 判斷的判斷條件,按照 Dubbo 的尿性,確定是能夠配置的。
因此咱們找到這個地方,問題就瞭然於心了。
咱們去哪裏找答案呢?
這個類裏面,這個類是一個請求/響應解碼的很是核心的類:org.apache.dubbo.rpc.protocol.dubbo.DubboCodec
這個類的主要乾了兩件事,一個是對響應報文進行解碼,一個是對請求報文進行解碼。
接下來咱們怎麼搞?強擼源碼嗎?不可能的。直接擼確定費勁。
仍是要搞個 Demo 跑起來,而後 Debug。
我這裏的 Demo 很是簡單,服務端接口實現類以下:
消費者在測試類中進行消費:
而後 Debug 起來,注意,下面演示的代碼沒有特別說明的地方,都是 2.7.5 版本。
運行起來後先不看別的,看看當前卡在這個地方,被 Debug 的線程是什麼線程:
到這裏你先冷靜一下,你想一下這個問題:
在這個方法裏面能夠對響應和請求進行解析。那它怎麼知道當前究竟是響應仍是請求報文呢?
答案就在前面說的 Dubbo 協議裏面:
呼應上了沒有?header 裏面第 16 bit 若是是 0 表明響應,若是是 1 表明請求。
你說巧不巧,上面這個方法的入參裏面就有一個 header 數組。
讓咱們看看他裏面裝的是什麼東西:
長度是 16,和 header 的長度吻合,可是裏面裝的玩意仍是沒看出來。
可是這樣一看,看前兩個字節,你就明白了:
嘿,你說巧了嗎,這不是巧了嗎,這不是。
魔數也對上了。說明這是一個 Dubbo 的 header。
而後取出第 3 字節,進行位運算,判斷這是什麼報文:
前面,咱們解決了怎麼知道當前究竟是響應仍是請求報文這個問題。
接下來,進入分支裏面就重點關注對響應報文的解析了:
首先,上面標記爲①的地方是判斷當前數據包是否是一個心跳包,通過 Debug 咱們能夠知道這不是一個心跳包:
而後標記爲②的地方獲取 header 中的第 4 個字節,第 4 個字節表明的是狀態位:
從 Debug 的截圖裏面咱們能夠看出,當前的狀態爲 20,表示正常返回。
標記爲③的地方,是對心跳包的解析,咱們這裏不關心。
標記爲④的地方,是咱們須要重點關注的地方,也是咱們一直在尋找的代碼。
這個地方就很關鍵了,你們集中注意力了。
首先,下面代碼的截圖是 2.7.5 版本的:
這裏的 if 分支和分支裏面的判斷條件,就是咱們前面說的:
你用腳指頭想也知道了。確定是有一個 if 判斷的,判斷到底在哪(IO線程/客戶端線程池)進行響應解析。而這個分支判斷的判斷條件,按照 Dubbo 的尿性,確定是能夠配置的。
下面這張圖片對 2.7.4.1 和 2.7.5 版本這個地方的代碼進行一個對比:
你仔細看着兩個版本之間的代碼,發現如出一轍,也沒有差別啊。
這就把我幹懵逼了:咋回事?說好的差別呢?
別忘了,上面的代碼裏面是有一個變量的:
差別就差別在這個地方。
2.7.5 版本以後,這個參數的默認值從 true 變爲了 false。
換句話說就是:2.7.5 版本以前,業務數據返回後,默認在 IO 線程裏面進行反序列化的操做。而2.7.5 版本以後,默認是延遲到客戶端線程池裏面進行反序列化的操做。
同時這個參數,無論在哪一個版本里面,都是能夠配置。雖然基本上也沒有人更改過這個配置,配置方法以下:
朋友們,到這裏還跟的上不?跟不上你就再捋捋?別硬看,傷身體。
接下來咱們再看看解碼操做的代碼究竟是怎麼樣的。
首先解碼操做,解的什麼碼?
解的是響應報文的響應體,也就是咱們的返回內容:org.apache.dubbo.rpc.protocol.dubbo.DecodeableRpcResult#decode(org.apache.dubbo.remoting.Channel, java.io.InputStream)
標號爲①的地方表明序列化類型是 2 。
2 是什麼?看錶:
Hessian2Serialization,就是 Dubbo 默認的序列化器。
標號爲②的地方表明本次響應類型爲 4。
4 是什麼?前面說了,看截圖:
因此,在標號爲③的地方即處理了返回值(handleValue)也處理了上下文信息(handleAttachment)。
handleValue 就不細看了,你就關注這個地方解析出來的就是咱們的響應內容:
響應內容的解碼就是上面說的邏輯。
無論是在 IO 線程裏面解碼仍是在客戶端線程池裏面解碼,都要調用這個方法。只不過是誰先誰後的問題。
那麼問題又來了,需求又發生變化了。
由於 IO 線程和客戶端線程池都要調用這個方法進行解碼,咱們總不能解碼兩次吧,那怎麼保證只解碼一次呢?
答案就是設置標識位。
由於咱們知道若是是在 IO 線程裏面解碼,那麼該操做調用解碼方法後,確定是先於客戶端線程池調用的。
有前後順序就好辦了。咱們就能夠設置標識位:
當在 IO 線程解析後,會把標識位設置爲 true。而後客戶端線程池再走到這個邏輯的時候,發現標識位是 true 了,不進行再次操做,問題就這樣被解決了。
接下來,我給你們對比一下 decodeBody 方法在 IO 線程裏面解碼和在客戶端線程池裏面解碼時分別返回什麼。也就是這行代碼返回的時候:
這樣一對比就很清晰了:
這樣也解釋了,爲何說是「延遲」到客戶端線程池裏面解碼。
好了,到這裏你有沒有發現一個問題。前面解析的這麼多源碼,而後咔一下,直接咱們就看到了最終返回的「Hello why」了。
這個是響應消息體,是 body。
頭呢?header 呢?
別急,這不是立刻就給你講一下嘛。
前面講這個方法的時候說了:header 是做爲參數傳進來的嘛,那咱們還能夠去找一下 header 究竟是怎麼傳進來的:
怎麼看呢?
順着調用鏈往回找就行,一個調試小技巧,送給你們,不客氣:
能夠看到 header 是從 buffer 裏面取出來的,最多讀取 HEADER_LENGTH (16) 個字節。
什麼?你還問我爲何最多讀 16 個字節?
我懷疑前面講協議的時候你就在走神。別問,問就是協議規定。你們遵照就行了。
再跟着調用鏈往前一步,你會發現這裏主要是在作解碼響應頭的部分:
上面這個方法裏面就是在搞 header 的事情。
其中有一個檢查報文長度的方法:checkPayLoad。
那麼問題又來了:請問 Dubbo 默認的報文長度限制是多少呢?
帶你們去源碼裏面找答案:
答案是 8M。
另外,既然是有默認值,那必須是能夠配置的。因此上圖標號爲①的地方是從配置中獲取,獲取不到,就返回默認值。
稍微有點意思的是標號爲②的地方,我第一次看的時候愣是看了一分鐘沒反應過來。主要是前面的這個 payload > 0,我想着這不是廢話嘛,長度不都是大於 0 的。興奮的我覺得發現了一個無用代碼呢。
後來才理解到,若是當 payload 設置爲負數的時候,就表明不限制報文長度。
能夠進行以下配置:
一個基本上用不到的 Dubbo 小知識點,免費贈送給你們。
好了,header 和 body 都齊活了。
到這裏,再總結一下:2.7.5 版本以前,業務數據返回後,默認在 IO 線程裏面進行反序列化的操做。而2.7.5 版本以後,默認是延遲到客戶端線程池裏面進行反序列化的操做。
因此,對於官網中,紅框框起來這個地方的描述是有問題的:http://dubbo.apache.org/zh-cn/docs/user/demos/consumer-threadpool.html
正確的說法應該是:在老的(2.7.5 版本以前)線程池模型中,當業務數據返回後,默認在 IO 線程上進行反序列化操做,若是配置了 decode.in.io 參數爲 false,則延遲到獨立的客戶端線程池進行反序列化操做。
接下來再聊聊線程池模型的變化。這裏的線程池指的都是客戶端線程池。
先拋兩個知識點:
不管是新老線程池模型,默認的 Dispatch 策略都是 all。全部響應仍是會轉發到客戶端線程池裏面,在這個裏面進行解碼操做(若是 IO 線程沒有解碼的話)把結果返回到用戶線程中去。
對於線程池客戶端的默認實現是 cached,服務端的默認實現是 fixed。
官網這裏的 fixed 缺省,特指服務端:
下面是官網上的截圖:
首先,無論 2.7.5 版本以前仍是以後客戶端的默認實現都是 cached ,這個線程池並無限制線程數量:
因此會出現消費端線程數分配多的問題。
但官網的描述是:分配過多。多和過多還不同。
爲何會過多呢?
由於在 2.7.5 版本以前,是每個連接都對應一個客戶端線程池。至關於作了連接級別的線程隔離,可是實際上這個線程隔離是沒有必要的。反而影響了性能。
而在 2.7.5 版本里面,就是無論你多少連接,你們共用一個客戶端線程池,引入了 threadless executor 的概念。
簡單的來講,優化結果就是從多個線程池改成了共用一個線程池。
線程池模型的變化,我在《Dubbo 2.7.5在線程模型上的優化》裏面比較詳細的聊過了,就不在重複講了,有興趣的能夠去翻一下。
若是你們對 Dubbo 學習感興趣的話,能夠去仔細讀讀官方文檔和官方博客。裏面寫仍是挺全的。
另外也有人叫我推薦一下 Dubbo 相關的書籍,能夠看一下這兩本,我都看過:
廣告
做者:詣極
噹噹
廣告
做者:翟陸續(加多)
京東
跟着書進行一個系統學習也是不錯的。
另外最近買書的話能夠看看噹噹網,最近開學季搞活動,絕大部分書籍都是滿 100 減 50 的。
我作爲一個小號主,給你們申請了一批實付滿 200 減 40 的優惠券,至關於花 160 買 400 元的書。真的很香,我本身也買了幾本。數量很少,先到先得。
推薦書單能夠看這裏:《那些在我文章中出現過的技術書籍》
優惠碼:WT9HXS
使用渠道:噹噹小程序或噹噹APP
使用方法:結算時點擊【優惠券/碼】
好了,看到了這裏安排個「一鍵三連」(轉發、在看、點贊)吧,周更很累的,不要白嫖我,須要一點正反饋。
才疏學淺,不免會有紕漏,若是你發現了錯誤的地方,能夠在問答區提出來(相似於留言功能了,可把我神氣壞了),我對其加以修改。
感謝您的閱讀,我堅持原創,十分歡迎並感謝您的關注。
我是 why,一個被代碼耽誤的文學創做者,不是大佬,可是喜歡分享,是一個又暖又有料的四川好男人。
還有,重要的事情說三遍:
歡迎關注我呀。
歡迎關注我呀。
歡迎關注我呀。