核心Handler和線程模型
Dubbo中Handler(ChannelHandler)的5種狀態:
狀態 | 描述 |
---|---|
connected | Channel已經被創建 |
disconnected | Channel已被斷開 |
sent | 消息被髮送 |
received | 消息被接收 |
caught | 捕獲到異常 |
Dubbo針對每個特性都會實現對應的ChannelHandler:
Handler | 作用 |
---|---|
ExchangeHandlerAdapter | 用於查找服務方法並調用 |
HeaderExchangeHandler | 封裝處理Request/Response和Telnet調用能力 |
DecodeHandler | 支持在Dubbo線程池中做解碼 |
ChannelHandlerDispatcher | 封裝多Handler廣播調用 |
AllChannelHandler | 支持Dubbo線程池調用業務方法 |
HeartbeatHandler | 支持心跳處理 |
MultiMessageHandler | 支持流中多消息報文批處理 |
ConnectionOrderedChannelHandler | 單獨線程池處理RCP的連接和斷開 |
MessageOnlyChannelHandler | 僅在線程池處理接收報文,其他事件在I/O線程處理 |
WrappedChannelHandler | 基於內存key-value存儲封裝和共享線程池能力,比如記錄線程池等 |
NettyServerHandler | 封裝Netty服務端事件,處理連接、斷開、讀取、寫入和異常等 |
NettyClientHandler | 封裝Netty客戶端事件,處理連接、斷開、讀取、寫入和異常等 |
Dubbo中提供了大量的handler去承載特性和擴展,這些Handler最終會和底層通信框架做關聯,比如Netty等。一次完整的RPC調用貫穿了一系列的Handler,如果直接掛載到底層通信框架(Netty),因爲整個鏈路比較長,則需要出發大量鏈式查找和事件,不僅低效,而且浪費資源。
在Dubbo框架中,NettyServerHandler實現了ChannelInboundHandler、NettyClientHandler實現了ChannelOutboundHandler。Dubbo通過裝飾器模式層包裝handler,從而不需要將每個Handler都追加到Pipeline中。在NettyServer和NettyClient中最多有3個Handler,分別是編碼、解碼和NettyServerHandler或NettyClientHandler。
在DubboProtocol中通過內部類繼承自ExchangeHandlerAdapter
,完成服務提供方Invoker實例的查找並進行服務的真實調用:
上面的Handler是實現是觸發業務方法調用的關鍵,在服務暴露時服務端已經按照特定的規則(端口、接口名、接口版本和接口分組
)把實例Invoker存儲到HashMap中,客戶端調用過來時必須攜帶相同信息構造的key,找到對應Exporter然後調用。
在①中查找當前已經暴露的服務。在②中主要包含實例的Filter和真實業務對象,當觸發invoker#invoke
方法時,就會執行具體的業務邏輯。在DubboProtocol中,跟蹤getInvoker調用,會發現在服務端唯一標識的服務是由4部分組成的:端口、接口名、接口版本和接口分組
。
在①中主要獲取協議暴露的端口,比如Dubbo協議默認的端口爲20880。在②中獲取客戶端傳遞過來的接口名稱(大部分場景都是接口名)。在③中主要根據服務端口、接口名、接口分組和接口版本構造唯一的key。在④中簡單地從HashMap中取出對應的Exporter並調用Invoker屬性值。
Dubbo爲了編織這些Handler,適應不同的場景,提供了一套可以定製的線程模型。爲了使概念更氫氣,我麼描述的I/O線程是指底層直接負責讀寫報文,比如Netty線程池。Dubbo中提供的線程池負責業務方法調用,稱爲業務線程。如果一些事件邏輯可以很快執行完成,比如做個標記而已,則可以直接在I/O線程中處理。如果事件處理耗時或阻塞,比如讀寫數據庫操作等,則應該將耗時或阻塞的任務轉到業務線程池執行。因爲I/O線程用於接收請求,如果I/O線程飽和,則不會接收新的請求。
Dubbo線程模型:
Dispatcher就是線程池派發器。這裏需要注意的是,Dispatcher真實的職責是創建具有線程派發能力的ChannelHandler,比如AllChannelHandler、MessageOnlyChannelHandler和ExecutionChannelHandler
等,其本身並不具備線程派發能力。
Dispatcher術語Dubbo中的擴展點,這個擴展點用來動態產生Handler,以滿足不同額場景。目前Dubbo支持6種策略調用:
線程分發策略:
分發策略 | 分發實現 | 作用 |
---|---|---|
all | Alldispatcher | 將左右I/O事件交給Dubbo線程池處理,Dubbo默認啓用 |
connection | ConnectionOrderedDiispatcher | 單獨線程池處理連接斷開事件,和Dubbo線程池分開 |
direct | DirectDispatcher | 所有方法調用和事件處理在I/O線程中,不推薦 |
execution | ExecutionDispatcher | 只在線程池處理接收請求,其他事件在I/O線程池中 |
message | MessageOnlyChannelHandler | 只在線程池處理請求和響應事件,其他事件在I/O線程池中 |
mockdispatcher | MockDispatcher | 默認返回Null |
具體業務方需要根據使用場景的不同的策略。建議使用默認策略即可,如果在TCP連接中需要做安全加密或校驗,則可以使用ConnectionOrderDispatcher
策略。如果引入新的線程池,則不可避免地導致額外的線程切換,用戶可以在Dubbo配置總指定dispatcher屬性讓具體策略生效。
Dubbo請求響應Handler
在Dubbo框架內部,所有方法都會被抽象成Request/Response,每次調用(一次會話)都會創建一個請求Request,如果是方法調用則會返回一個Response對象。
HeaderExchangeHandler
用來處理這種場景,它主要負責以下4種事情:
- 更新發送和讀取請求時間戳
- 判斷請求格式或編解碼是否出錯,並響應客戶端失則的具體原因。
- 處理Request請求和Response正常響應。
- 支持Telnet調用
- ①負責響應讀取時間並更新時間戳,在Dubbo心跳處理中會使用當前值並判斷是否超過空閒時間。
- ②主要處理事件類型,目前主要處理readonly事件,用於Dubbo優雅停機。當註冊中心反註冊元數據時,因爲網絡原因,客戶端不能及時感知註冊中心事件,服務端會發送readonly報文告知下線。
- ④處理收到的Response響應,告知業務調用方。
- ⑤校驗客端不支持Telnet調用,因爲只有服務提供方暴露服務纔有意義。這裏有個小改進,因爲客戶端支持異步參數回調,但爲什麼這裏不能支持Telne調用呢?異步參數回調客戶端實際上也會暴露一個服務,因此針對這種場景Telnet應該是允許調用的。
- ⑥觸發Telnet調用,並將字符串返回給Telnet客戶端。
處理請求和響應(HeaderExchangeHandler#handleRequest,handleResponse):
處理請求報文:
在處理請求時,因爲在編解碼層報錯會透傳到Handler,所以在①中首先會判斷是否因爲請求報文不正確,如果發生錯誤,則服務端會將具體異常包裝成字符串返回,如果直接使用異常對象,則可能造成無法序列化的錯誤。在②中觸發Dubbo協議方法調用,並且把方法調用返回值發送給客戶端。如果調用發生未知錯誤,則會通過③做容錯並返回。當發送請求時,會在DefaultFuture中保存請求對象並阻塞請求線程,在④中會喚醒阻塞線程並將Response中的結果通知調用方。
Dubbo心跳Handler
Dubbo默認客戶端和服務端都會發送心跳報文,用來保持TCP長連接狀態。在客戶端和服務端,Dubbo內部開啓一個線程循環掃描並檢測連接是否超時,在服務端如果發現超時則會主動關閉客戶端連接,在客戶端發現超時則會主動重新創建連接。默認心跳檢測時間是60秒
,具體應用可以通過heartbeat進行配置。
Dubbo在服務端和客戶端都複用心跳實現代碼,抽象成HeartBeatTask
任務進行處理:
- ①遍歷所有的Channel,在服務端對應的是所有客戶端連接,在客戶端對應的是服務端連接
- ②主要忽略已經關閉的Socket連接。
- ③判斷當前TCP連接是否空閒,如果空閒就發送心跳報文。目前判斷是否空閒的,根據Channel是否有讀或寫來決定,比如1分鐘內沒有讀或寫就發送心跳報文。
- ④處理客戶端超時重新建立TCP連接,目前的策略是檢查是否在3分鐘內(用戶可以設置)都沒有成功接收或發送報文。如果在服務端檢測則會通過⑤主動關閉遠程連接。