天天看點

Dubbo 編解碼那些事

本文基于實際問題,梳理dubbo編解碼鍊路,以及Hessian2架構的序列化邏輯。有助于提高對于Dubbo架構的學習、使用和問題排查。

筆者在一次維護基礎公共元件的過程中,不小心修改了類的包路徑。糟糕的是,這個類被各業務在facade中進行了引用、傳遞。幸運的是,同一個類,在提供者和消費者的包路徑不一緻,沒有引起各業務報錯。

懷揣着好奇,對于Dubbo的編解碼做了幾次的Debug學習,在此分享一些學習經驗。

Dubbo作為Java語言的RPC架構,優勢之一在于屏蔽了調用細節,能夠像調用本地方法一樣調用遠端服務,不必為資料格式抓耳饒腮。正是這一特性,也引入來了一些問題。

比如引入facade包後出現jar包沖突、服務無法啟動,更新facade包後某個類找不到等等問題。引入jar包,導緻消費方和提供方在某種程度上有了一定耦合。

正是這種耦合,在提供者修改了Facade包類的路徑後,習慣性認為會引發報錯,而實際上并沒有。最初認為很奇怪,仔細思考後才認為理應這樣,調用方在按照約定的格式和協定基礎上,即可與提供方完成通信。并不應該關注提供方本身上下文資訊。(認為類的路徑屬于上下文資訊)接下來揭秘Dubbo的編碼解碼過程。

Dubbo預設用的netty作為通信架構,所有分析都是以netty作為前提。涉及的源碼均為Dubbo - 2.7.x版本。在實際過程中,一個服務很有可能既是消費者,也是提供者。為了簡化梳理流程,假定都是純粹的消費者、提供者。

借用Dubbo官方文檔的一張圖,文檔内,定義了通信和序列化層,并沒有定義"編解碼"含義,在此對"編解碼"做簡單解釋。

編解碼 = dubbo内部編解碼鍊路 + 序列化層

本文旨在梳理從Java對象到二進制流,以及二進制流到Java對象兩種資料格式之間的互相轉換。在此目的上,為了便于了解,附加通信層内容,以encode,decode為入口,梳理dubbo處理鍊路。又因Dubbo内部定義為Encoder,Decoder,故在此定義為"編解碼"。

Dubbo 編解碼那些事

無論是序列化層,還是通信層,都是Dubbo高效、穩定運作的基石,了解底層實作邏輯,能夠幫助我們更好的學習和使用Dubbo架構。

消費者口在NettyClient#doOpen方法發起連接配接,初始化BootStrap時,會在Netty的pipeline裡添加不同類型的ChannelHandler,其中就有編解碼器。

同理,提供者在NettyServer#doOpen方法提供服務,初始化ServerBootstrap時,會添加編解碼器。(adapter.getDecoder()- 解碼器,adapater.getEncoder() - 編碼器)。

NettyClient

NettyServer

消費者在發送消息時編碼,接收響應時解碼。

發送消息

接收響應

提供者在接收消息時解碼,回複響應時編碼。

接收消息

回複響應

Dubbo支援多種通信協定,如dubbo協定,http,rmi,webservice等等。預設為Dubbo協定。作為通信協定,有一定的協定格式和約定,而這些資訊是業務不關注的。是Dubbo架構在編碼過程中,進行添加和解析。

dubbo采用定長消息頭 + 不定長消息體進行資料傳輸。以下是消息頭的格式定義

Dubbo 編解碼那些事
2byte:magic,類似java位元組碼檔案裡的魔數,用來辨別是否是dubbo協定的資料包。 1byte:消息标志位,5位序列化id,1位心跳還是正常請求,1位雙向還是單向,1位請求還是響應; 1byte:響應狀态,具體類型見com.alibaba.dubbo.remoting.exchange.Response; 8byte:消息ID,每一個請求的唯一識别id; 4byte:消息體body長度。

以消費端發送消息為例,設定消息頭内容的代碼見ExchangeCodec#encodeRequest。

消息編碼

前節梳理了編解碼的流程,本節仔細看一看對象序列化的細節内容。

我們知道,Dubbo支援多種序列化格式,hessian2,json,jdk序列化等。hessian2是阿裡對于hessian進行了修改,也是dubbo預設的序列化架構。在此以消費端發送消息序列化對象,接收響應反序列化為案例,看看hessian2的處理細節,同時解答前言問題。

前文提到,請求編碼方法在ExchangeCodec#encodeRequest,其中對象資料的序列化為DubboCodec#encodeRequestData

DubboCodec

我們知道,在dubbo調用過程中,是以Invocation作為上下文環境存儲。這裡先寫入了版本号,服務名,方法名,方法參數,傳回值等資訊。随後循環參數清單,對每個參數進行序列化。在此,out對象即是具體序列化架構對象,預設為Hessian2ObjectOutput。這個out對象作為參數傳遞進來。

那麼是在哪裡确認實際序列化對象呢?

從頭檢視編碼的調用鍊路,ExchangeCodec#encodeRequest内有如下代碼:

ExchangeCodec

out對象來自于serialization對象,順着往下看。在CodecSupport類有如下代碼:

CodecSupport

可以看到,這裡通過URL資訊,基于Dubbo的SPI選擇Serialization對象,預設為hessian2。再看看serialization.serialize(channel.getUrl(),bos)方法:

Hessian2Serialization

至此,找到了實際序列化對象,參數序列化邏輯較為簡單,不做贅述,簡述如下:寫入請求參數類型 → 寫入參數字段名 → 疊代字段清單,字段序列化。

相對于序列化而言,反序列化會多一些限制。序列化對象時,不需要關心接收者的實際資料格式。反序列化則不然,需要保證原始資料和對象比對。(這裡的原始資料可能是二進制流,也可能是json)。

消費端解碼鍊路中有提到,發生了兩次解碼,第一次未實際解碼業務資料,而是轉換成DecodeableRpcResult。具體代碼如下:

關鍵點

1)對于解碼請求還是解碼響應做了區分,對于消費端而言,就是解碼響應。對于提供端而言,即是解碼請求。

2)為什麼會出現兩次解碼?具體見這行:

decode_in_io_thread_key - 是否在io線程内進行解碼,預設是false,避免在io線程内處理業務邏輯,這也是符合netty的推薦做法。是以才有了異步的解碼過程。

那看看解碼業務對象的代碼,還記得在哪兒嗎?DecodeableRpcResult#decode

DecodeableRpcResult

這裡出現了ObjectInput,那底層的序列化架構選擇邏輯是怎麼樣的呢?如何保持與消費端的序列化架構一緻?

每一個序列化架構有一個id見org.apache.dubbo.common.serialize.Constants;

1、請求時,序列化架構是根據Url資訊進行選擇,預設是hessian2 2、傳輸時,會将序列化架構辨別寫入協定頭,具體見ExchangeCodec#encodeRequest#218 3、提供收到消費端的請求時,會根據這個id使用對應的序列化架構。

此次實際持有對象為Hessian2ObjectInput,由于readObject反序列化邏輯處理較為複雜,流程如下:

Dubbo 編解碼那些事

問題1:提供端修改了Facade裡的類路徑,消費端反序列化為什麼沒報錯?

答:反序列化時,消費端找不到提供端方傳回的類路徑時,會catch異常,以本地的傳回類型為準做處理

問題2:編碼序列化時,沒有為什麼寫入傳回值?

答:因為在Java中,傳回值不作為辨別方法的資訊之一

問題3:反序列化流程圖中,A與B何時會出現不一緻的情況?A的資訊從何處讀取?

答:當提供端修改了類路徑時,A與B會出現不一樣;A的資訊來源于,發起請求時,Request對象裡存儲的Invocation上下文,是本地jar包裡的傳回值類型。

問題4:提供者增删傳回字段,消費者會報錯嗎?

答:不會,反序列化時,取兩者字段交集。

問題5:提供端修改對象的父類資訊,消費端會報錯嗎?

答:不會,傳輸中隻攜帶了父類的字段資訊,沒有攜帶父類類資訊。執行個體化時,以本地類做執行個體化,不關聯提供方實際代碼的父類路徑。

問題6:反序列化過程中,如果傳回對象子類和父類存在同名字段,且子類有值,父類無值,會發生什麼?

答:在dubbo - 3.0.x版本,在會出現傳回字段為空的情況。原因在于編碼側疊代傳輸字段集合時(消費端可能編碼,提供端也可能編碼),父類的字段資訊在子類後面。解碼側拿到字段集合疊代解碼時,通過字段key拿到反序列化器,此時子類和父類同名,那麼第一次反射會設定子類值,第二次反射會設定父類值進行覆寫。

在dubbo - 2.7.x版本中,該問題已解決。解決方案也比較簡單,在編碼側傳輸時,通過 Collections.reverse(fields)反轉字段順序。

JavaSerializer

編解碼過程複雜晦澀,資料類型多種多樣。筆者遇到和了解的終究有限,以最常見、最簡單的資料類型梳理編解碼的流程。如有錯誤疏漏之處,還請見諒。

作者:vivo 網際網路伺服器團隊-Sun wen

分享 vivo 網際網路技術幹貨與沙龍活動,推薦最新行業動态與熱門會議。