本文已同步至我的公眾號(hào) Code4j,歡迎各位看官老爺來玩昆庇。
1. 什么是遠(yuǎn)程過程調(diào)用
在講述 Dubbo 的服務(wù)調(diào)用過程之前扎阶,讓我們先來了解一下什么是遠(yuǎn)程過程調(diào)用。
遠(yuǎn)程過程調(diào)用即 Remote Producedure Call
蜓洪,簡單來說就是跨進(jìn)程調(diào)用,通過網(wǎng)絡(luò)傳輸坯苹,使得 A 機(jī)器上的應(yīng)用可以像調(diào)用本地的服務(wù)一樣去調(diào)用 B 機(jī)器上的服務(wù)隆檀。
舉個(gè)最簡單的栗子,假設(shè)現(xiàn)在有一個(gè)電商系統(tǒng)北滥,其中有著用戶服務(wù)刚操,優(yōu)惠券服務(wù)闸翅,訂單服務(wù)等服務(wù)模塊再芋,這些不同的服務(wù)并不是運(yùn)行在同一個(gè) JVM 中,而是分開運(yùn)行在不同的 JVM 中坚冀。因此济赎,當(dāng)訂單服務(wù)想要調(diào)用優(yōu)惠券服務(wù)時(shí),就不能像以前的單體應(yīng)用一樣记某,直接向?qū)?yīng)服務(wù)發(fā)起本地調(diào)用司训,只能夠通過網(wǎng)絡(luò)來發(fā)起調(diào)用。
那么液南,一個(gè)最簡單的遠(yuǎn)程過程調(diào)用是怎么樣的呢壳猜?來看下面這張圖。
也就是說滑凉,一次最簡單的 RPC 調(diào)用统扳,無非就是調(diào)用方通過網(wǎng)絡(luò),將調(diào)用的參數(shù)傳送到服務(wù)方畅姊,服務(wù)方收到調(diào)用請(qǐng)求后咒钟,根據(jù)參數(shù)完成本地調(diào)用,并且將結(jié)果通過網(wǎng)絡(luò)傳送回調(diào)用方若未。
在這個(gè)過程中朱嘴,像參數(shù)的封裝,網(wǎng)絡(luò)傳輸?shù)燃?xì)節(jié)會(huì)由 RPC 框架來完成粗合,把上面的圖片完善一下萍嬉,一個(gè)完整的 RPC 調(diào)用的流程是這樣的:
- 客戶端(Client)以本地調(diào)用的方式調(diào)用遠(yuǎn)程服務(wù)乌昔。
- 客戶端代理對(duì)象(Client Stub)將本次請(qǐng)求的相關(guān)信息(要調(diào)用的類名、方法名壤追、方法參數(shù)等)封裝成
Request
玫荣,并且對(duì)其序列化,為網(wǎng)絡(luò)通信做準(zhǔn)備大诸。 - 客戶端代理對(duì)象(Client Stub)找到服務(wù)端(Server)的地址捅厂,通過網(wǎng)絡(luò)(Socket 通信)將
Request
發(fā)送到服務(wù)端。 - 服務(wù)端代理對(duì)象(Server Stub)接收到客戶端(Client)的請(qǐng)求后资柔,將二進(jìn)制數(shù)據(jù)反序列化為
Request
焙贷。 - 服務(wù)端代理對(duì)象(Server Stub)根據(jù)調(diào)用信息向本地的方法發(fā)起調(diào)用。
- 服務(wù)端代理對(duì)象(Server Stub)將調(diào)用后的結(jié)果封裝到
Response
中贿堰,并且對(duì)其序列化辙芍,通過網(wǎng)絡(luò)發(fā)送給客戶端。 - 客戶端代理對(duì)象(Client Stub)收到響應(yīng)后羹与,將其反序列化為
Response
故硅,遠(yuǎn)程調(diào)用結(jié)束。
2. Dubbo 的遠(yuǎn)程調(diào)用過程
本節(jié)內(nèi)容基于 Dubbo 2.6.x 版本纵搁,并且使用官網(wǎng)提供的 Demo 對(duì)同步調(diào)用進(jìn)行分析吃衅。
在上一節(jié)內(nèi)容中,我們已經(jīng)對(duì)服務(wù)調(diào)用的過程有了一定的了解腾誉。實(shí)際上徘层,Dubbo 在實(shí)現(xiàn)遠(yuǎn)程調(diào)用的時(shí)候,核心流程和上面的圖片是完全一樣的利职,只不過 Dubbo 在此基礎(chǔ)上增加了一些額外的流程趣效,例如集群容錯(cuò)、負(fù)載均衡猪贪、過濾器鏈等跷敬。
本篇文章只分析核心的調(diào)用流程,其它的額外流程可以自行了解热押。
在講解 Dubbo 的調(diào)用過程之前西傀,我們先來了解一下 Dubbo 的一些概念。
-
Invoker
:在 Dubbo 中作為實(shí)體域楞黄,也就是代表了要操作的對(duì)象模型池凄,這有點(diǎn)像 Spring 中的 Bean,所有的操作都是圍繞著這個(gè)實(shí)體域來進(jìn)行鬼廓。- 代表了一個(gè)可執(zhí)行體肿仑,可以向它發(fā)起
invoke
調(diào)用。它有可能是一個(gè)本地實(shí)現(xiàn),也有可能是一個(gè)遠(yuǎn)程實(shí)現(xiàn)尤慰,也有可能是一個(gè)集群實(shí)現(xiàn)馏锡。
- 代表了一個(gè)可執(zhí)行體肿仑,可以向它發(fā)起
-
Invocation
:在 Dubbo 中作為會(huì)話域,表示每次操作的瞬時(shí)狀態(tài)伟端,操作前創(chuàng)建杯道,操作后銷毀。- 其實(shí)就是調(diào)用信息责蝠,存放了調(diào)用的類名党巾、方法名、參數(shù)等信息霜医。
-
Protocol
:在 Dubbo 作為服務(wù)域齿拂,負(fù)責(zé)實(shí)體域和會(huì)話域的生命周期管理。- 可以理解為 Spring 中的 BeanFactory肴敛,是產(chǎn)品的入口署海。
2.1 遠(yuǎn)程調(diào)用的開端 —— 動(dòng)態(tài)代理
在了解以上基本概念后,我們開始來跟蹤 Dubbo 的遠(yuǎn)程調(diào)用流程医男。在 RPC 框架中砸狞,想要實(shí)現(xiàn)遠(yuǎn)程調(diào)用,代理對(duì)象是不可或缺的镀梭,因?yàn)樗梢詭臀覀兤帘魏芏嗟讓蛹?xì)節(jié)刀森,使得我們對(duì)遠(yuǎn)程調(diào)用無感知。
如果用過 JDK 的動(dòng)態(tài)代理或者是 CGLIB 的動(dòng)態(tài)代理丰辣,那么應(yīng)該都知道每個(gè)代理對(duì)象都會(huì)有對(duì)應(yīng)的一個(gè)處理器撒强,用于處理動(dòng)態(tài)代理時(shí)的增強(qiáng),例如 JDK 使用的 InvacationHandler
或者 CGLIB 的 MethodInterceptor
笙什。在 Dubbo 中,默認(rèn)是使用 javasisst 來實(shí)現(xiàn)動(dòng)態(tài)代理的胚想,它與 JDK 動(dòng)態(tài)一樣使用 InvocationHandler
來進(jìn)行代理增強(qiáng)琐凭。
下面分別是使用 javasisst 和使用 JDK 動(dòng)態(tài)代理時(shí)對(duì)代理類進(jìn)行反編譯后的結(jié)果。
從上面可以看出浊服,InvacationHandler
要做的事無非就是根據(jù)本次調(diào)用的方法名和方法參數(shù)统屈,將其封裝成調(diào)用信息 Invacation
,然后將其傳遞給持有的 Invoker
對(duì)象牙躺。從這里開始愁憔,才算是真正進(jìn)入到了 Dubbo 的核心模型中。
2.2 客戶端的調(diào)用鏈路
在了解客戶端的調(diào)用鏈路之前孽拷,我們需要先看一下 Dubbo 的整體設(shè)計(jì)吨掌,下圖是來自于 Dubbo 官網(wǎng)的一張框架設(shè)計(jì)圖,很好地展示了整個(gè)框架的結(jié)構(gòu)。
為了容易理解膜宋,我把上圖中的 Proxy
代理層窿侈、Cluster
集群層以及 Protocol
協(xié)議層進(jìn)行了一個(gè)抽象。
如下圖所示秋茫, Dubbo 的 Proxy
代理層先與下層的 Cluster
集群層進(jìn)行交互史简。Cluster
這一層的作用就是將多個(gè) Invoker
偽裝成一個(gè) ClusterInvoker
后暴露給上層使用,由該 ClusterInvoker
來負(fù)責(zé)容錯(cuò)的相關(guān)邏輯肛著,例如快速失敗圆兵,失敗重試等等。對(duì)于上層的 Proxy
來說枢贿,這一層的容錯(cuò)邏輯是透明的衙傀。
因此,當(dāng) Proxy
層的 InvocationHandler
將調(diào)用請(qǐng)求委托給持有的 Invoker
時(shí)萨咕,其實(shí)就是向下傳遞給對(duì)應(yīng)的 ClusterInvoker
统抬,并且經(jīng)過獲取可用 Invoker
,根據(jù)路由規(guī)則過濾 Invoker
危队,以及負(fù)載均衡選中要調(diào)用的 Invoker
等一系列操作后聪建,就會(huì)得到一個(gè)具體協(xié)議的 Invoker
。
這個(gè)具體的 Invoker
可能是一個(gè)遠(yuǎn)程實(shí)現(xiàn)茫陆,例如默認(rèn)的 Dubbo 協(xié)議對(duì)應(yīng)的 DubboInvoker
金麸,也有可能是一個(gè)本地實(shí)現(xiàn),例如 Injvm 協(xié)議對(duì)應(yīng)的 InjvmInvoker
等簿盅。
關(guān)于集群相關(guān)的
Invoker
挥下,如果有興趣的話可以看一下用于服務(wù)降級(jí)的MockClusterInvoker
,集群策略抽象父類AbstractClusterInvoker
以及默認(rèn)的也是最常用的失敗重試集群策略FailoverClusterInvoker
桨醋,實(shí)際上默認(rèn)情況下的集群調(diào)用鏈路就是逐個(gè)經(jīng)過這三個(gè)類的棚瘟。順帶提一句,在獲取到具體的協(xié)議
Invoker
之前會(huì)經(jīng)過一個(gè)過濾器鏈喜最,對(duì)于每一個(gè)過濾器對(duì)于本次請(qǐng)求都會(huì)做一些處理偎蘸,比如用于統(tǒng)計(jì)的MonitorFilter
,用于處理當(dāng)前上下文信息的ConsumerContextFilter
等等瞬内。過濾器這一部分給用戶提供了很大的擴(kuò)展空間迷雪,有興趣的話可以自行了解。
拿到具體的 Invoker
之后虫蝶,此時(shí)所處的位置為上圖中的 Protocol
層章咧,這時(shí)候就可以通過下層的網(wǎng)絡(luò)層來完成遠(yuǎn)程過程調(diào)用了,先來看一下 DubboInvoker
的源碼能真。
可以看到赁严,Dubbo 對(duì)于調(diào)用方式做了一些區(qū)分扰柠,分別為同步調(diào)用,異步調(diào)用以及單次調(diào)用误澳。
首先有一點(diǎn)要明確的是耻矮,同步調(diào)用也好,異步調(diào)用也好忆谓,這都是站在用戶的角度來看的裆装,但是在網(wǎng)絡(luò)這一層面的話,所有的交互都是異步的倡缠,網(wǎng)絡(luò)框架只負(fù)責(zé)將數(shù)據(jù)發(fā)送出去哨免,或者將收到的數(shù)據(jù)向上傳遞,網(wǎng)絡(luò)框架并不知道本次發(fā)送出去的二進(jìn)制數(shù)據(jù)和收到的二進(jìn)制的數(shù)據(jù)是否是一一對(duì)應(yīng)的昙沦。
因此琢唾,當(dāng)用戶選擇同步調(diào)用的時(shí)候,為了將底層的異步通信轉(zhuǎn)化為同步操作盾饮,這里 Dubbo 需要調(diào)用某個(gè)阻塞操作采桃,使用戶線程阻塞在這里,直到本次調(diào)用的結(jié)果返回丘损。
2.3 遠(yuǎn)程調(diào)用的基石 —— 網(wǎng)絡(luò)層
在上一小節(jié)的 DubboInvoker
當(dāng)中普办,我們可以看到遠(yuǎn)程調(diào)用的請(qǐng)求是通過一個(gè) ExchangeClient
的類發(fā)送出去的,這個(gè) ExchangeClient
類處于 Dubbo 框架的遠(yuǎn)程通信模塊中的 Exchange
信息交換層徘钥。
從前面出現(xiàn)過的架構(gòu)圖中可以看到衔蹲,遠(yuǎn)程通信模塊共分為三層,從上到下分別是 Exchange
信息交換層呈础,Transport
網(wǎng)絡(luò)傳輸層以及 Serialize
序列化層舆驶,每一層都有其特定的作用。
從最底層的 Serialize
層說起而钞,這一層的作用就是負(fù)責(zé)序列化/反序列化沙廉,它對(duì)多種序列化方式進(jìn)行了抽象,如 JDK 序列化笨忌,Hessian 序列化蓝仲,JSON 序列化等。
往上則是 Transport
層官疲,這一層負(fù)責(zé)的單向的消息傳輸,強(qiáng)調(diào)的是一種 Message
的語義亮隙,不體現(xiàn)交互的概念途凫。同時(shí)這一層也對(duì)各種 NIO 框架進(jìn)行了抽象,例如 Netty溢吻,Mina 等等维费。
再往上就是 Exhange
層果元,和 Transport
層不同,這一層負(fù)責(zé)的是請(qǐng)求/響應(yīng)的交互犀盟,強(qiáng)調(diào)的一種 Request
和 Reponse
的語義而晒,也正是由于請(qǐng)求響應(yīng)的存在,才會(huì)有 Client
和 Server
的區(qū)分阅畴。
了解完遠(yuǎn)程通信模塊的分層結(jié)構(gòu)后倡怎,我們?cè)賮砜匆幌略撃K中的核心概念。
Dubbo 在這個(gè)模塊中抽取出了一個(gè)端點(diǎn) Endpoint
的概念贱枣,通過一個(gè) IP 和 一個(gè) Port监署,就可以唯一確定一個(gè)端點(diǎn)。在這兩個(gè)端點(diǎn)之間纽哥,我們可以建立 TCP 連接钠乏,而這個(gè)連接被 Dubbo 抽象成了通道 Channel
,通道處理器 ChannelHandler
則負(fù)責(zé)對(duì)通道進(jìn)行處理春塌,例如處理通道的連接建立事件晓避、連接斷開事件,處理讀取到的數(shù)據(jù)只壳、發(fā)送的數(shù)據(jù)以及捕獲到的異常等俏拱。
同時(shí),為了在語義上對(duì)端點(diǎn)進(jìn)行區(qū)分吕世,Dubbo 將發(fā)起請(qǐng)求的端點(diǎn)抽象為客戶端 Client
彰触,而發(fā)送響應(yīng)的端點(diǎn)則抽象成服務(wù)端 Server
。由于不同的 NIO 框架對(duì)外接口和使用方式不一樣命辖,所以為了避免上層接口直接依賴具體的 NIO 庫况毅,Dubbo 在 Client
和 Server
之上又抽象出了一個(gè) Transporter
接口,該接口用于獲取 Client
和 Server
尔艇,后續(xù)如果需要更換使用的 NIO 庫尔许,那么只需要替換相關(guān)實(shí)現(xiàn)類即可。
Dubbo 將負(fù)責(zé)數(shù)據(jù)編解碼功能的處理器抽象成了
Codec
接口终娃,有興趣的話可以自行了解味廊。
Endpoint
主要的作用就是發(fā)送數(shù)據(jù),因此 Dubbo 為其定義了 send()
方法棠耕;同時(shí)余佛,讓 Channel
繼承 Endpoint
,使其在發(fā)送數(shù)據(jù)的基礎(chǔ)上擁有添加 K/V
屬性的功能窍荧。
對(duì)于客戶端來說辉巡,一個(gè) Cleint
只會(huì)關(guān)聯(lián)著一個(gè) Channel
,因此直接繼承 Channel
使其也具備發(fā)送數(shù)據(jù)的功能即可蕊退,而 Server
可以接受多個(gè) Cleint
建立的 Channel
連接郊楣,所以 Dubbo 沒有讓其繼承 Channel
憔恳,而是選擇讓其直接繼承 Endpoint
,并且提供了 getChannels()
方法用于獲取關(guān)聯(lián)的連接净蚤。
為了體現(xiàn)了請(qǐng)求/響應(yīng)的交互模式钥组,在 Channel
、Server
以及 Client
的基礎(chǔ)上進(jìn)一步抽象出 ExchangeChannel
今瀑、ExchangeServer
以及 ExchangeClient
接口程梦,并為 ExchangeChannel
接口添加 request()
方法,具體類圖如下放椰。
了解完網(wǎng)絡(luò)層的相關(guān)概念后作烟,讓我們看回 DubboInvoker
,當(dāng)同步調(diào)用時(shí)砾医,DubboInvoker
會(huì)通過持有的 ExchangeClient
來發(fā)起請(qǐng)求拿撩。實(shí)際上,這個(gè)調(diào)用最后會(huì)被 HeaderExchangeChannel
類所接收如蚜,這是一個(gè)實(shí)現(xiàn)了 ExchangeChannel
的類压恒,因此也具備請(qǐng)求的功能。
可以看到错邦,其實(shí) request()
方法只不過是將數(shù)據(jù)封裝成 Request
對(duì)象探赫,構(gòu)造一個(gè)請(qǐng)求的語義,最終還是通過 send()
方法將數(shù)據(jù)單向發(fā)送出去撬呢。下面是一張關(guān)于客戶端發(fā)送請(qǐng)求的調(diào)用鏈路圖伦吠。
這里值得注意的是 DefaultFuture
對(duì)象的創(chuàng)建。DefaultFuture
類是 Dubbo 參照 Java 中的 Future
類所設(shè)計(jì)的魂拦,這意味著它可以用于異步操作毛仪。每個(gè) Request
對(duì)象都有一個(gè) ID,當(dāng)創(chuàng)建 DefaultFuture
時(shí)芯勘,會(huì)將請(qǐng)求 ID 和創(chuàng)建的 DefaultFutrue
映射給保存起來箱靴,同時(shí)設(shè)置超時(shí)時(shí)間。
保存映射的目的是因?yàn)樵诋惒角闆r下荷愕,請(qǐng)求和響應(yīng)并不是一一對(duì)應(yīng)的衡怀。為了使得后面接收到的響應(yīng)可以正確被處理,Dubbo 會(huì)在響應(yīng)中帶上對(duì)應(yīng)的請(qǐng)求 ID安疗,當(dāng)接收到響應(yīng)后抛杨,根據(jù)其中的請(qǐng)求 ID 就可以找到對(duì)應(yīng)的 DefaultFuture
,并將響應(yīng)結(jié)果設(shè)置到 DefaultFuture
荐类,使得阻塞在 get()
操作的用戶線程可以及時(shí)返回蝶桶。
整個(gè)過程可以抽象為下面的時(shí)序圖。
當(dāng) ExchangeChannel
調(diào)用 send()
后掉冶,數(shù)據(jù)就會(huì)通過底層的 NIO 框架發(fā)送出去真竖,不過在將數(shù)據(jù)通過網(wǎng)絡(luò)傳輸之前,還有最后一步需要做的厌小,那就是序列化和編碼恢共。
注意,在調(diào)用 send() 方法之前璧亚,所有的邏輯都是用戶線程在處理的讨韭,而編碼工作則是由 Netty 的 I/O 線程處理,有興趣的話可以了解一下 Netty 的線程模型癣蟋。
2.4 協(xié)議和編碼
上文提到過很多次協(xié)議(Protocol)和編碼透硝,那么到底什么是協(xié)議,什么又是編碼呢?
其實(shí)疯搅,通俗一點(diǎn)講濒生,協(xié)議就是一套約定好的通信規(guī)則。打個(gè)比方幔欧,張三和李四要進(jìn)行交流罪治,那么他們之間在交流之前就需要先約定好如何交流,比如雙方約定礁蔗,當(dāng)聽到“Hello World”的時(shí)候觉义,就代表對(duì)方要開始講話了。此時(shí)浴井,張三和李四之間的這種約定就是他們的通信協(xié)議晒骇。
而對(duì)于編碼的話,其實(shí)就是根據(jù)約定好的協(xié)議磺浙,將數(shù)據(jù)組裝成協(xié)議規(guī)定的格式洪囤。當(dāng)張三想和李四說“早上好”的時(shí)候,那么張三只需要在“早上好”之前加上約定好的“Hello World”屠缭,也就是最終的消息為“Hello World 早上好”箍鼓。李四一聽到“Hello World”,就知道隨后的內(nèi)容是張三想說的呵曹,通過這種形式款咖,張三和李四之間就可以完成正常的交流了。
具體到實(shí)際的 RPC 通信中奄喂,所謂的 Dubbo 協(xié)議铐殃,RMI 協(xié)議,HTTP 協(xié)議等等跨新,它們只不過是對(duì)應(yīng)的通信規(guī)則不一樣富腊,但最終的作用都是一樣的,就是提供給組裝通信數(shù)據(jù)的一套規(guī)則域帐,僅此而已赘被。
這里借用一張官網(wǎng)的圖是整,展示了默認(rèn)的 Dubbo 協(xié)議數(shù)據(jù)包格式。
Dubbo 數(shù)據(jù)包分為消息頭和消息體。消息頭為定長格式,共 16 字節(jié)党瓮,用于存儲(chǔ)一些元信息录肯,例如消息的起始標(biāo)識(shí) Magic Number
,數(shù)據(jù)包的類型,使用的序列化方式 ID,消息體長度等。消息體則為變長格式易迹,具體長度存儲(chǔ)在消息頭中,這部分是用于存儲(chǔ)了具體的調(diào)用信息或調(diào)用結(jié)果平道,也就是 Invocation
序列化后的字節(jié)序列或遠(yuǎn)程調(diào)用返回的對(duì)象的字節(jié)序列睹欲,消息體這部分的數(shù)據(jù)是由序列化/反序列化來處理的。
之前提到過巢掺,Dubbo 將用于編解碼數(shù)據(jù)的通道處理器抽象為了 Codec
接口句伶,所以在消息發(fā)送出去之前,Dubbo 會(huì)調(diào)用該接口的 encode()
方法進(jìn)行編碼陆淀。其中考余,對(duì)于消息體,也就是本次調(diào)用的調(diào)用信息 Invacation
轧苫,會(huì)通過 Serialization
接口來進(jìn)行序列化楚堤。
Dubbo 在啟動(dòng)客戶端和服務(wù)端的時(shí)候,會(huì)通過適配器模式含懊,將
Codec
相關(guān)的編解碼器與 Netty 進(jìn)行適配身冬,將其添加到 Netty 的 pipeline 中,參見NettyCodecAdapter
岔乔、NettyClient
和NettyServer
酥筝。
下面是相關(guān)的編碼邏輯,對(duì)照上圖食用更佳雏门。
編碼完成之后嘿歌,數(shù)據(jù)就會(huì)被 NIO 框架所發(fā)出,通過網(wǎng)絡(luò)到達(dá)服務(wù)端茁影。
2.5 服務(wù)端的調(diào)用鏈路
當(dāng)服務(wù)端接收到數(shù)據(jù)的時(shí)候宙帝,因?yàn)榻邮盏降亩际亲止?jié)序列,所以第一步應(yīng)該是對(duì)其解碼募闲,這一步最終會(huì)交給 Codec
接口的 decode
方法處理步脓。
解碼的時(shí)候會(huì)先解析得到消息頭,然后再根據(jù)消息頭中的元信息,例如消息頭長度靴患,消息類型仍侥,將消息體反序列化為 DecodeableRpcInvocation
對(duì)象(也就是調(diào)用信息)。
此時(shí)的線程為 Netty 的 I/O 線程蚁廓,不一定會(huì)在當(dāng)前線程解碼访圃,所以有可能會(huì)得到部分解碼的 Request 對(duì)象,具體分析見下文相嵌。
值得注意的是,在 2.6.x 版本中况脆,默認(rèn)情況下對(duì)于請(qǐng)求的解碼會(huì)在 I/O 線程中執(zhí)行饭宾,而 2.7.x 之后的版本則是交給業(yè)務(wù)線程執(zhí)行。
這里的 I/O 線程指的是底層通信框架中接收請(qǐng)求的線程(其實(shí)就是 Netty 中的 Worker 線程)格了,業(yè)務(wù)線程則是 Dubbo 內(nèi)部用于處理請(qǐng)求/響應(yīng)的線程池中的線程看铆。如果某個(gè)事件可能比較耗時(shí),不能在 I/O 線程上執(zhí)行盛末,那么就需要通過線程派發(fā)器將線程派發(fā)到線程池中去執(zhí)行弹惦。
再次借用官網(wǎng)的一張圖,當(dāng)服務(wù)端接收到請(qǐng)求時(shí)悄但,會(huì)根據(jù)不同的線程派發(fā)策略棠隐,將請(qǐng)求派發(fā)到線程池中執(zhí)行。線程派發(fā)器 Dispatcher
本身并不具備線程派發(fā)的能力檐嚣,它只是用于創(chuàng)建具有線程派發(fā)能力的 ChannelHandler
助泽。
Dubbo 擁有 5 種線程派發(fā)策略,默認(rèn)使用的策略為 all
嚎京,具體策略差別見下表嗡贺。
策略 | 用途 |
---|---|
all | 所有消息都派發(fā)到線程池,包括請(qǐng)求鞍帝,響應(yīng)诫睬,連接事件,斷開事件等 |
direct | 所有消息都不派發(fā)到線程池帕涌,全部在 IO 線程上直接執(zhí)行 |
message | 只有請(qǐng)求和響應(yīng)消息派發(fā)到線程池摄凡,其它消息均在 IO 線程上執(zhí)行 |
execution | 只有請(qǐng)求消息派發(fā)到線程池,不含響應(yīng)宵膨。其它消息均在 IO 線程上執(zhí)行 |
Connection | 在 IO 線程上架谎,將連接斷開事件放入隊(duì)列,有序逐個(gè)執(zhí)行辟躏,其它消息派發(fā)到線程池 |
經(jīng)過 DubboCodec
解碼器處理過的數(shù)據(jù)會(huì)被 Netty 傳遞給下一個(gè)入站處理器谷扣,最終根據(jù)配置的線程派發(fā)策略來到對(duì)應(yīng)的 ChannelHandler
,例如默認(rèn)的 AllChannelHandler
。
可以看到会涎,對(duì)于每種事件裹匙,AllChannelHandler
只是創(chuàng)建了一個(gè) ChannelEventRunnable
對(duì)象并提交到業(yè)務(wù)線程池中去執(zhí)行,這個(gè) Runnable
對(duì)象其實(shí)只是一個(gè)中轉(zhuǎn)站末秃,它是為了避免在 I/O 線程中執(zhí)行具體的操作概页,最終真正的操作它會(huì)委托給持有的 ChannelHandler
去處理。
服務(wù)端對(duì)請(qǐng)求進(jìn)行派發(fā)的過程如下圖所示练慕。
上面說過惰匙,解碼操作也有可能在業(yè)務(wù)線程中執(zhí)行,因?yàn)?ChannelEventRunnable
中直接持有的 ChannelHandler
就是一個(gè)用于解碼的 DecodeHandler
铃将。
如果需要解碼项鬼,那么這個(gè)通道處理器會(huì)調(diào)用在 I/O 線程中創(chuàng)建的 DecodeableRpcInvocation
對(duì)象的 decode
方法,從字節(jié)序列中反序列化得到本次調(diào)用的類名劲阎,方法名绘盟,參數(shù)信息等。
解碼完成后悯仙,DecodeHandler
會(huì)將完全解碼的 Request
對(duì)象繼續(xù)傳遞到下一個(gè)通道處理器即 HeaderExchangeHandler
龄毡。
到這里其實(shí)已經(jīng)可以體會(huì)到 Dubbo 抽取出 ChannelHandler
的好處了,可以避免和特定 NIO 庫耦合锡垄,同時(shí)使用裝飾者模式一層層地處理請(qǐng)求沦零,最終對(duì) NIO 庫只暴露出一個(gè)特定的 Handler,更加靈活偎捎。
這里附上一張服務(wù)端 ChannelHandler
的結(jié)構(gòu)圖蠢终。
HeaderExchangeHandler
會(huì)根據(jù)本次請(qǐng)求的類型決定如何處理。如果是單向調(diào)用茴她,那么只需向后調(diào)用即可寻拂,不需要返回響應(yīng)。如果是雙向調(diào)用丈牢,那么就需要在得到具體的調(diào)用結(jié)果后祭钉,封裝成 Response
對(duì)象,并通過持有的 Channel
對(duì)象將本次調(diào)用的響應(yīng)發(fā)送回客戶端己沛。
HeaderExchangeHandler
將調(diào)用委托給持有的 ExchangeHandler
處理器慌核,這個(gè)處理器是和服務(wù)暴露時(shí)使用的協(xié)議有關(guān)的,一般來說都是某個(gè)協(xié)議的內(nèi)部類申尼。
由于默認(rèn)情況下都是使用的 Dubbo 協(xié)議垮卓,所以接下來對(duì) Dubbo 協(xié)議中的處理器進(jìn)行分析。
Dubbo 協(xié)議內(nèi)部的 ExchangeHandler
會(huì)從已經(jīng)暴露的服務(wù)列表中找到本次調(diào)用的 Invoker
师幕,并且向其發(fā)起本地調(diào)用粟按。不過要注意的是诬滩,這里的 Invoker
是一個(gè)動(dòng)態(tài)生成的代理對(duì)象,類型為 AbstractProxyInvoker
灭将,它持有了處理業(yè)務(wù)的真實(shí)對(duì)象疼鸟。
當(dāng)發(fā)起 invoke
調(diào)用時(shí),它會(huì)通過持有的真實(shí)對(duì)象完成調(diào)用庙曙,并將其封裝到 RpcResult
對(duì)象中并且返回給下層空镜。
關(guān)于
RpcResult
有興趣的話可以了解一下 2.7.x 異步化改造后的變化。簡單來說就是RpcResult
被AppResonse
所替代捌朴,用來保存調(diào)用結(jié)果或調(diào)用異常吴攒,同時(shí)引入了一個(gè)新的中間狀態(tài)類AsyncRpcResult
用于代表未完成的 RPC 調(diào)用。
這個(gè)代理對(duì)象是在服務(wù)端進(jìn)行服務(wù)暴露的時(shí)候生成的男旗,javassist 會(huì)動(dòng)態(tài)生成一個(gè) Wrapper
類舶斧,并且創(chuàng)建一個(gè)匿名內(nèi)部對(duì)象,將調(diào)用操作委托給 Wrapper
察皇。
下面是反編譯得到的 Wrapper
類,可以看到具體的處理邏輯和客戶端的 InvocationHandler
類似泽台,都是根據(jù)本次調(diào)用的方法名來向真實(shí)對(duì)象發(fā)起調(diào)用什荣。
至此,服務(wù)端已完成了調(diào)用過程怀酷。下層 ChannelHandler
收到調(diào)用結(jié)果后稻爬,就會(huì)通過 Channel
將響應(yīng)發(fā)送回客戶端,期間又會(huì)經(jīng)過編碼序列化等操作蜕依,由于和請(qǐng)求的編碼序列化過程類似桅锄,這里不再贅述,感興趣的話可以自行查看 ExchangeCodec#encodeResponse()
以及 DubboCodec#encodeResponseData()
样眠。
這里再附上一張服務(wù)端處理請(qǐng)求的時(shí)序圖友瘤。
2.6 客戶端處理響應(yīng)
當(dāng)客戶端收到調(diào)用的響應(yīng)后,毫無疑問依舊需要對(duì)收到的字節(jié)序列進(jìn)行解碼及反序列化檐束,這里和服務(wù)端解碼請(qǐng)求的過程是類似的辫秧,查看 ExchangeCodec#decode()
以及 DubboCodec#decodeBody()
自行了解,也可參考上面的服務(wù)端解碼請(qǐng)求的時(shí)序圖被丧,這里只附上一張客戶端處理已(部分)解碼的響應(yīng)的時(shí)序圖盟戏。
這里主要講的是客戶端對(duì)解碼后的 Reponse
對(duì)象的處理邏輯∩穑客戶端的 ChannelHandler
結(jié)構(gòu)和上面的服務(wù)端 ChnnelHandler
結(jié)構(gòu)圖沒有太大區(qū)別柿究,經(jīng)過解碼后的響應(yīng)最終也會(huì)傳遞到 HeaderExchangeHandler
處理器中進(jìn)行處理。
在客戶端發(fā)起請(qǐng)求時(shí)我們提到過黄选,每個(gè)構(gòu)造的請(qǐng)求都有一個(gè) ID 標(biāo)識(shí)蝇摸,當(dāng)對(duì)應(yīng)的響應(yīng)返回時(shí),就會(huì)把這個(gè) ID 帶上。當(dāng)接收到響應(yīng)時(shí)探入, Dubbo 會(huì)從請(qǐng)求的 Future 映射集合中狡孔,根據(jù)返回的請(qǐng)求 ID,找到對(duì)應(yīng)的 DefaultFuture
蜂嗽,并將結(jié)果設(shè)置到 DefaultFuture
中苗膝,同時(shí)喚醒阻塞的用戶線程,這樣就完成了 Dubbo 的業(yè)務(wù)線程到用戶線程的轉(zhuǎn)換植旧。
有興趣的話可以再了解一下 DefauFuture 的超時(shí)處理 以及 Dubbo 2.7 異步化改造后的線程模型變化辱揭。
最后附上一張來源官網(wǎng)的圖。
至此病附,一個(gè)完整的 RPC 調(diào)用就結(jié)束了问窃。
由于本人水平有限,可能部分細(xì)節(jié)并沒有講清楚 完沪,如果有疑問的話歡迎大家指出域庇,一起交流學(xué)習(xí)。
3. 參考鏈接
《深入理解 Apache Dubbo 與實(shí)戰(zhàn)》