Dubbo技術知識總結之五——Dubbo遠程調用

接上篇《Dubbo技術知識總結之四——Dubbo集羣容錯》

五. 遠程調用

上篇《Dubbo技術知識總結之四——Dubbo集羣容錯》的七個步驟中,前四個步驟是 Cluster 層的工作。遠程調用是後續步驟 5, 6, 7 的內容,同時也是 Cluster 層以下的工作。該部分對 Dubbo 遠程調用的基礎與實現進行總結,包括 Dubbo 協議,編解碼器,Dubbo 線程模型。

5.1 Dubbo 協議

Dubbo 協議設計參考了 TCP/IP 協議,每次 RPC 調用,報文都會包括協議頭協議體兩個部分。
協議報文頭部共 16 字節,攜帶信息有:

  1. 魔數:標識該報文是 Dubbo 協議類型;
  2. 數據包類型:標識該報文是請求或響應;
  3. 調用方式:單向/雙向;
  4. 事件標識:0 表示當前數據包是請求或響應包;1 表示當前數據包是心跳包;
  5. 序列化器編號:標識該數據用哪種方式序列化;包括 Hassian, FastJson, Kryo 等七種方式;
  6. 狀態:OK, CLIENT_TIMEOUT, SERVER_TIMEOUT, BAD_REQUEST, BAD_RESPONSE 等;
  7. 請求編號:RPC 請求的唯一 ID,用來將請求和響應作關聯;
  8. 消息體長度:用 4 個字節表示消息體的長度;

注:Dubbo 協議屬於 Dubbo 框架的 Protocol 層。

5.2 編解碼器

編解碼器有三種場景,請求、響應、Telnet 調用。主要對請求和響應場景進行總結。

注:編解碼屬於 Dubbo 框架的 Exchange, Transport 層。

5.2.1 編碼器

Dubbo 編碼器主要是將 Java 對象編碼成字節流,返回給客戶端。所有的編解碼層實現類都應該繼承於 ExchangeCodec 類。
Dubbo 協議請求的編碼方法 ExchangeCodec#encodeRequest() 中,按照 Dubbo 協議的內容編碼成字節流,其中關鍵方法 ExchangeCodec#encodeRequestData() 將請求內容序列化。在該方法中對接口、方法、方法參數類型、方法參數進行編碼,並寫入字節流中。

Dubbo 協議響應的編碼方法 ExchangeCodec#encodeResponse() 基本類似,核心內容在於序列化響應調用方法 encodeResponseData(),以及編碼異常處理部分

  1. ExchangeCodec#encodeResponseData 方法編碼思路比較簡單,編碼內容可以分爲正常 Java 類異常信息兩類,分別對其進行序列化操作。
  2. ExchangeCodec#encodeResponse() 方法中,出現編碼異常處理的情況,首先將 ChannelBuffer 復位,避免造成緩衝區中的數據錯亂;然後將異常信息通過 Channel 發送給客戶端,防止客戶端只有等到超時才感知到服務調用返回。

5.2.2 解碼器

5.2.2.1 粘包、半包

注:參考地址:《Dubbo源碼解析(十七)Dubbo 處理TCP粘包拆包》

解碼相較於編碼比較複雜,因爲在解碼過程中涉及粘包半包問題。Dubbo 是基於 TCP 協議進行數據傳輸的,粘包和半包問題就是 TCP 流協議的典型問題。
流就像是河裏的流水,是連成一片的,中間並沒有分界線,TCP 底層並不瞭解上層業務數據的具體含義,它會根據 TCP 緩衝區的實際情況進行包的劃分。所以在業務上,一個完整的包可能會被 TCP 拆分成多個包發送,這種情況會導致半包;也可能把多個小包封裝成一個大的數據包發送,這種情況會導致粘包

由於底層 TCP 無法理解上層的業務數據,所以底層是無法保證數據包不被拆分和重組。這個問題只能通過上層應用設計協議的方式來解決。業界主流協議解決方案如下:

  1. 消息定長:比如每個報文固定長度 200 字節,長度不夠的用空格補位;
  2. 字符分割:比如 FTP 協議,在包尾增加回車換行符作爲分割;
  3. 將消息分爲消息頭與消息體:消息頭中包含表示該消息總長度的字段,通常會放到第一個字段,用 int32 表示消息總長度;
  4. 規定應用層協議

Dubbo 使用的就是第四種方案,自行規定應用層的協議,即上面 [6.1](## 6.1 Dubbo 協議) 章節總結的內容。Dubbo 中 ExchangerCodec 將消息解析爲請求 Request 與響應 Response 的角色。

5.2.2.2 解碼過程

解碼過程可以分爲兩步,第一步是報文頭部解碼,第二步是報文體解碼,並將報文體轉換成 RpcInvocation。解碼過程在 ExchangeCodec#decode() 方法中,第一步解碼報文頭部的過程如下:

  1. 檢查魔數;
  2. 檢查當前請求頭是否完整,即是否大於 16 字節;如果不完整,則返回狀態 NEED_MORE_INPUT
  3. 獲取此次請求體長度,判斷請求體 + 消息體長度消息包長度大小;
    • 前者代表了一個報文的長度,後者代表此次讀取的長度;
    • 如果前者大於後者,說明這次消息不是完整的,也就是說發生了拆包現象;此時直接返回狀態 NEED_MORE_INPUT
  4. 正常狀態,進入解析消息體的步驟;

第二步報文體解碼的方法在 DubboCodec 進行了重寫,即方法 DubboCodec#decodeBody()。步驟如下:

  1. Request / Response:根據 Dubbo 報文頭中的 FLAG_REQUEST 標誌位,判斷這次消息是請求還是響應;
  2. 消息是否正常:根據解析出來的狀態碼,判斷這次消息是否正常;
  3. 反序列化:解析消息使用的序列化方式,進行反序列化;
  4. 返回:解析成功,將解析的請求(或響應)返回到上游方法;

5.2.3 Telnet

編解碼器將 Telnet 當做明文字符串處理,根據 Dubbo 的調用規範,解析成調用命令格式,查找對應的 Invoker,發起方法調用。

5.3 線程模型

參考地址:《Dubbo學習筆記8:Dubbo的線程模型與線程池策略》

Dubbo 默認底層網絡通信使用 Netty 框架。服務提供方 NettyServer 提供兩級線程池,其中 EventLoopGroup(boss) 用來接受客戶端的連接請求,並將接受的請求分發 (Dispatch) 給 EventLoopGroup(worker) 來處理。可以將 boss 和 worker 線程組稱爲 IO 線程,它的特點是不會發起新的 IO 請求,邏輯處理能迅速完成。有的包括查詢數據庫等操作的複雜操作處理慢,需要將這些複雜操作放到 Dubbo 線程池中(又稱業務線程池)。根據請求消息被 IO 線程處理,還是被業務線程處理,Dubbo 提供了幾種線程模型,不同線程模型實現不同的線程分發策略,同時各自實現了 Dispatcher 可擴展 SPI 接口。

5.3.1 分發策略

Dispatcher 是線程派發器,真正的職責是創建具有線程派發能力的 ChannelHandler,比如 AllChannelHandler, MessageOnlyChannelHandler 等。

5.3.1.1 AllDispatcher

all 策略,分發實現類 AllDispatcher,將所有消息都派發到 Dubbo 線程池,包括請求、響應、連接事件、斷開事件、心跳等,是 Dispatcher 的默認實現。
AllDispatcher

5.3.1.2 ConnectionOrderedDispatcher

connection 策略,分發實現類 ConnectionOrderedDispatcher,只將連接斷開事件放到線程池中有序執行,其他線程派發到 Dubbo 線程池處理。
ConnectionOrderedDispatcher

5.3.1.3 DirectDispatcher

direct 策略,分發實現類 DirectDispatcher,所有方法調用和事件處理都在 IO 線程池中。不推薦該策略。
DirectDispatcher

5.3.1.4 ExecutionDispatcher

execution 策略,分發實現類 ExecutionDispatcher,只將請求類派發到 Dubbo 線程池處理,其他類型的 IO 事件在 IO 線程池中。
ExecutionDispatcher

5.3.1.5 MessageOnlyChannelHandler

message 策略,分發實現類 MessageOnlyChannelHandler,只在 Dubbo 線程池中處理請求和響應事件,其他事件在 IO 線程池中處理。
MessageOnlyChannelHandler

5.3.1.6 MockDispatcher

mock 策略,分發實現類 MockDispatcher,默認返回 null。

5.3.2 線程池策略

擴展接口 ThreadPool 的 SPI 實現有如下幾種:

  • fixed:固定大小線程池,啓動時建立線程,不關閉,一直持有;默認實現;
  • cached:緩存線程池,空閒一分鐘自動刪除,需要時重建;
  • limited:可伸縮線程池,但池中的線程數只會增長不會收縮。只增長不收縮的目的是爲了避免收縮時突然帶來大流量引起性能問題。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章