Dubbo服務(wù)調(diào)用過程
看完今天的服務(wù)調(diào)用流程,基本上Dubbo的核心過程就完整的串聯(lián)起來了,在腦海中應(yīng)該就有 Dubbo 整體運(yùn)行的概念,這體系就建立起來了,對 RPC 也會(huì)有進(jìn)一步的認(rèn)識(shí)。
簡單的想想大致流程
在分析Dubbo 的服務(wù)調(diào)用過程前我們先來思考一下如果讓我們自己實(shí)現(xiàn)的話一次調(diào)用過程需要經(jīng)歷哪些步驟?
首先我們已經(jīng)知曉了遠(yuǎn)程服務(wù)的地址,然后我們要做的就是把我們要
然后根據(jù)這些信息找到對應(yīng)的實(shí)現(xiàn)類,然后進(jìn)行調(diào)用,調(diào)用完了之后再原路返回,然后客戶端解析響應(yīng)再返回即可。
調(diào)用具體的信息
那客戶端告知服務(wù)端的具體信息應(yīng)該包含哪些呢?
首先客戶端肯定要告知要調(diào)用是服務(wù)端的哪個(gè)接口,當(dāng)然還需要方法名、方法的參數(shù)類型、方法的參數(shù)值,還有可能存在多個(gè)版本的情況,所以還得帶上版本號(hào)。
由這么幾個(gè)參數(shù),那么服務(wù)端就可以清晰的得知客戶端要調(diào)用的是哪個(gè)方法,可以進(jìn)行精確調(diào)用!
然后組裝響應(yīng)返回即可,我這里貼一個(gè)實(shí)際調(diào)用請求對象列子。
data 就是我所說的那些數(shù)據(jù),其他是框架的,包括協(xié)議版本、調(diào)用方式等等這個(gè)下面再分析。
到此其實(shí)大致的意思大家都清楚了,就是普通的遠(yuǎn)程調(diào)用,告知請求的參數(shù),然后服務(wù)端解析參數(shù)找到對應(yīng)的實(shí)現(xiàn)調(diào)用,再返回。
落地的調(diào)用流程
上面的是想象的調(diào)用流程,真實(shí)的落地調(diào)用流程沒有這么簡單。
首先遠(yuǎn)程調(diào)用需要定義協(xié)議,也就是互相約定我們要講什么樣的語言,要保證雙方都能聽得懂。
比如我會(huì)英語和中文,你也會(huì)英語、中文,我們之間要做約定,選定一個(gè)語言比如都用中文來談話,有人說不對啊,你中文夾著的英文我也能聽得懂啊。
那是因?yàn)槟愕拇竽X很智能,它能智能地識(shí)別到交流的語言,而計(jì)算機(jī)可不是,你想想你的代碼寫 print 1,它還能打出 2 不成?
也就是計(jì)算機(jī)是死板的,我們的程序告訴它該怎么做,它就會(huì)生硬的怎么做。
需要一個(gè)協(xié)議
所以首先需要雙方定義一個(gè)協(xié)議,這樣計(jì)算機(jī)才能解析出正確的信息。
常見的三種協(xié)議形式
應(yīng)用層一般有三種類型的協(xié)議形式,分別是:固定長度形式、特殊字符隔斷形式、header+body 形式。
固定長度形式:指的是協(xié)議的長度是固定的,比如100個(gè)字節(jié)為一個(gè)協(xié)議單元,那么讀取100個(gè)字節(jié)之后就開始解析。
優(yōu)點(diǎn)就是效率較高,無腦讀一定長度就解析。
缺點(diǎn)就是死板,每次長度只能固定,不能超過限制的長度,并且短了還得填充,在 RPC 場景中不太合適,誰曉得參數(shù)啥的要多長,定長了浪費(fèi),定短了不夠。
特殊字符隔斷形式:其實(shí)就是定義一個(gè)特殊結(jié)束符,根據(jù)特殊的結(jié)束符來判斷一個(gè)協(xié)議單元的結(jié)束,比如用換行符等等。
這個(gè)協(xié)議的優(yōu)點(diǎn)是長度自由,反正根據(jù)特殊字符來截?cái)啵秉c(diǎn)就是需要一直讀,直到讀到一個(gè)完整的協(xié)議單元之后才能開始解析,然后假如傳輸?shù)臄?shù)據(jù)里面混入了這個(gè)特殊字符就出錯(cuò)了。
header+body 形式:也就是頭部是固定長度的,然后頭部里面會(huì)填寫 body 的長度, body 是不固定長度的,這樣伸縮性就比較好了,可以先解析頭部,然后根據(jù)頭部得到 body 的 len 然后解析 body。
dubbo 協(xié)議就是屬于 header+body 形式,而且也有特殊的字符 0xdabb ,這是用來解決 TCP 網(wǎng)絡(luò)粘包問題的。
Dubbo 協(xié)議
Dubbo 支持的協(xié)議很多,我們就簡單的分析下 Dubbo 協(xié)議。
協(xié)議分為協(xié)議頭和協(xié)議體,可以看到 16 字節(jié)的頭部主要攜帶了魔法數(shù),也就是之前說的 0xdabb,然后一些請求的設(shè)置,消息體的長度等等。
16 字節(jié)之后就是協(xié)議體了,包括協(xié)議版本、接口名字、接口版本、方法名字等等。
其實(shí)協(xié)議很重要,因?yàn)閺闹锌梢缘弥芏嘈畔ⅲ抑挥卸藚f(xié)議的內(nèi)容,才能看得懂編碼器和解碼器在干嘛,我再截取一張官網(wǎng)對協(xié)議的解釋圖。
需要約定序列化器 然后對端再反序列化這些字節(jié)流變成對象。 序列化協(xié)議 序列化大致分為兩大類,一種是字符型,一種是二進(jìn)制流。 字符型的代表就是 XML、JSON,字符型的優(yōu)點(diǎn)就是調(diào)試方便,它是對人友好的,我們一看就能知道那個(gè)字段對應(yīng)的哪個(gè)參數(shù)。 缺點(diǎn)就是傳輸?shù)男实?,有很多冗余的東西,比如 JSON 的括號(hào),對于網(wǎng)絡(luò)傳輸來說傳輸?shù)臅r(shí)間變長,占用的帶寬變大。 還有一大類就是二進(jìn)制流型,這種類型是對機(jī)器友好的,它的數(shù)據(jù)更加的緊湊,所以占用的字節(jié)數(shù)更小,傳輸更快。 缺點(diǎn)就是調(diào)試很難,肉眼是無法識(shí)別的,必須借用特殊的工具轉(zhuǎn)換。 更深層次的就不深入了,序列化還是有很多門道的,以后有機(jī)會(huì)再談。 Dubbo 默認(rèn)用的是 hessian2 序列化協(xié)議。 所以實(shí)際落地還需要先約定好協(xié)議,然后再選擇好序列化方式構(gòu)造完請求之后發(fā)送。 粗略的調(diào)用流程圖 簡述一下就是客戶端發(fā)起調(diào)用,實(shí)際調(diào)用的是代理類,代理類最終調(diào)用的是 Client (默認(rèn)Netty),需要構(gòu)造好協(xié)議頭,然后將 Java 的對象序列化生成協(xié)議體,然后網(wǎng)絡(luò)調(diào)用傳輸。 服務(wù)端的 NettyServer接到這個(gè)請求之后,分發(fā)給業(yè)務(wù)線程池,由業(yè)務(wù)線程調(diào)用具體的實(shí)現(xiàn)方法。 但是這還不夠,因?yàn)?Dubbo 是一個(gè)生產(chǎn)級(jí)別的 RPC 框架,它需要更加的安全、穩(wěn)重。 詳細(xì)的調(diào)用流程 前面已經(jīng)分析過了客戶端也是要序列化構(gòu)造請求的,為了讓圖更加突出重點(diǎn),所以就省略了這一步,當(dāng)然還有響應(yīng)回來的步驟,暫時(shí)就理解為原路返回,下文會(huì)再做分析。 可以看到生產(chǎn)級(jí)別就得穩(wěn),因此服務(wù)端往往會(huì)有多個(gè),多個(gè)服務(wù)端的服務(wù)就會(huì)有多個(gè) Invoker,最終需要通過路由過濾,然后再通過負(fù)載均衡機(jī)制來選出一個(gè) Invoker 進(jìn)行調(diào)用。 當(dāng)然 Cluster 還有容錯(cuò)機(jī)制,包括重試等等。 請求會(huì)先到達(dá) Netty 的 I/O 線程池進(jìn)行讀寫和可選的序列化和反序列化,可以通過 decode.in.io控制,然后通過業(yè)務(wù)線程池處理反序列化之后的對象,找到對應(yīng) Invoker 進(jìn)行調(diào)用。 調(diào)用流程-客戶端源碼分析 調(diào)用具體的接口會(huì)調(diào)用生成的代理類,而代理類會(huì)生成一個(gè) RpcInvocation 對象調(diào)用 MockClusterInvoker#invoke方法。 此時(shí)生成的 RpcInvocation 如下圖所示,包含方法名、參數(shù)類和參數(shù)值。 然后我們再來看一下 MockClusterInvoker#invoke 代碼。 可以看到就是判斷配置里面有沒有配置 mock, mock 的話就不展開分析了,我們來看看 this.invoker.invoke 的實(shí)現(xiàn),實(shí)際上會(huì)調(diào)用 AbstractClusterInvoker#invoker 。 模板方法 模板方法其實(shí)就是在抽象類中定好代碼的執(zhí)行骨架,然后將具體的實(shí)現(xiàn)延遲到子類中,由子類來自定義個(gè)性化實(shí)現(xiàn),也就是說可以在不改變整體執(zhí)行步驟的情況下修改步驟里面的實(shí)現(xiàn),減少了重復(fù)的代碼,也利于擴(kuò)展,符合開閉原則。 在代碼中就是那個(gè) doInvoke由子類來實(shí)現(xiàn),上面的一些步驟都是每個(gè)子類都要走的,所以抽到抽象類中。 路由和負(fù)載均衡得到 Invoker 然后帶著這些 Invoker 再進(jìn)行一波 loadbalance 的挑選,得到一個(gè) Invoker,我們默認(rèn)使用的是 FailoverClusterInvoker,也就是失敗自動(dòng)切換的容錯(cuò)方式,其實(shí)關(guān)于路由、集群、負(fù)載均衡是獨(dú)立的模塊,如果展開講的話還是有很多內(nèi)容的,所以需要另起一篇講,這篇文章就把它們先作為黑盒使用。 稍微總結(jié)一下就是 FailoverClusterInvoker 拿到 Directory 返回的 Invoker 列表,并且經(jīng)過路由之后,它會(huì)讓 LoadBalance 從 Invoker 列表中選擇一個(gè) Invoker。 最后FailoverClusterInvoker會(huì)將參數(shù)傳給選擇出的那個(gè) Invoker 實(shí)例的 invoke 方法,進(jìn)行真正的遠(yuǎn)程調(diào)用,我們來簡單的看下 FailoverClusterInvoker#doInvoke,為了突出重點(diǎn)我刪除了很多方法。 發(fā)起調(diào)用的這個(gè) invoke 又是調(diào)用抽象類中的 invoke 然后再調(diào)用子類的 doInvoker,抽象類中的方法很簡單我就不展示了,影響不大,直接看子類 DubboInvoker 的 doInvoke 方法。 調(diào)用的三種方式 oneway還是很常見的,就是當(dāng)你不關(guān)心你的請求是否發(fā)送成功的情況下,就用 oneway 的方式發(fā)送,這種方式消耗最小,啥都不用記,啥都不用管。 異步調(diào)用,其實(shí) Dubbo 天然就是異步的,可以看到 client 發(fā)送請求之后會(huì)得到一個(gè) ResponseFuture,然后把 future 包裝一下塞到上下文中,這樣用戶就可以從上下文中拿到這個(gè) future,然后用戶可以做了一波操作之后再調(diào)用 future.get 等待結(jié)果。 同步調(diào)用,這是我們最常用的,也就是 Dubbo 框架幫助我們異步轉(zhuǎn)同步了,從代碼可以看到在 Dubbo 源碼中就調(diào)用了 future.get,所以給用戶的感覺就是我調(diào)用了這個(gè)接口的方法之后就阻塞住了,必須要等待結(jié)果到了之后才能返回,所以就是同步的。 可以看到 Dubbo 本質(zhì)上就是異步的,為什么有同步就是因?yàn)榭蚣軒臀覀冝D(zhuǎn)了一下,而同步和異步的區(qū)別其實(shí)就是future.get 在用戶代碼被調(diào)用還是在框架代碼被調(diào)用。 再回到源碼中來,currentClient.request 源碼如下就是組裝 request 然后構(gòu)造一個(gè) future 然后調(diào)用 NettyClient 發(fā)送請求。 我們再來看一下 DefaultFuture 的內(nèi)部,你有沒有想過一個(gè)問題,因?yàn)槭钱惒?,那么這個(gè) future 保存了之后,等響應(yīng)回來了如何找到對應(yīng)的 future 呢? 這里就揭秘了!就是利用一個(gè)唯一 ID。 可以看到 Request 會(huì)生成一個(gè)全局唯一 ID,然后 future 內(nèi)部會(huì)將自己和 ID 存儲(chǔ)到一個(gè) ConcurrentHashMap。這個(gè) ID 發(fā)送到服務(wù)端之后,服務(wù)端也會(huì)把這個(gè) ID 返回來,這樣通過這個(gè) ID 再去ConcurrentHashMap 里面就可以找到對應(yīng)的 future ,這樣整個(gè)連接就正確且完整了! 我們再來看看最終接受到響應(yīng)的代碼,應(yīng)該就很清晰了。 先看下一個(gè)響應(yīng)的 message 的樣子: 看到這個(gè) ID 了吧,最終會(huì)調(diào)用 DefaultFuture#received的方法。 為了能讓大家更加的清晰,我再畫個(gè)圖: 到這里差不多客戶端調(diào)用主流程已經(jīng)很清晰了,其實(shí)還有很多細(xì)節(jié),之后的文章再講述,不然一下太亂太雜了。 發(fā)起請求的調(diào)用鏈如下圖所示: 處理請求響應(yīng)的調(diào)用鏈如下圖所示 調(diào)用流程-服務(wù)端端源碼分析 默認(rèn)走的是 all,也就是所有消息都派發(fā)到業(yè)務(wù)線程池中,我們來看下 AllChannelHandler 的實(shí)現(xiàn)。 就是將消息封裝成一個(gè) ChannelEventRunnable 扔到業(yè)務(wù)線程池中執(zhí)行,ChannelEventRunnable 里面會(huì)根據(jù) ChannelState 調(diào)用對于的處理方法,這里是 ChannelState.RECEIVED,所以調(diào)用 handler.received,最終會(huì)調(diào)用 HeaderExchangeHandler#handleRequest,我們就來看下這個(gè)代碼。 這波關(guān)鍵點(diǎn)看到了吧,構(gòu)造的響應(yīng)先塞入請求的 ID,我們再來看看這個(gè) reply 干了啥。 最后的調(diào)用我們已經(jīng)清楚了,實(shí)際上會(huì)調(diào)用一個(gè) Javassist 生成的代理類,里面包含了真正的實(shí)現(xiàn)類,之前已經(jīng)分析過了這里就不再深入了,我們再來看看getInvoker 這個(gè)方法,看看怎么根據(jù)請求的信息找到對應(yīng)的 invoker 的。 關(guān)鍵就是那個(gè) serviceKey, 還記得之前服務(wù)暴露將invoker 封裝成 exporter 之后再構(gòu)建了一個(gè) serviceKey將其和 exporter 存入了 exporterMap 中吧,這 map 這個(gè)時(shí)候就起作用了! 這個(gè) Key 就長這樣: 找到 invoker 最終調(diào)用實(shí)現(xiàn)類具體的方法再返回響應(yīng)整個(gè)流程就完結(jié)了,我再補(bǔ)充一下之前的圖。 總結(jié) 首先客戶端調(diào)用接口的某個(gè)方法,實(shí)際調(diào)用的是代理類,代理類會(huì)通過 cluster 從 directory 中獲取一堆 invokers(如果有一堆的話),然后進(jìn)行 router 的過濾(其中看配置也會(huì)添加 mockInvoker 用于服務(wù)降級(jí)),然后再通過 SPI 得到 loadBalance 進(jìn)行一波負(fù)載均衡。 這里要強(qiáng)調(diào)一下默認(rèn)的 cluster 是 FailoverCluster ,會(huì)進(jìn)行容錯(cuò)重試處理,這個(gè)日后再詳細(xì)分析。 現(xiàn)在我們已經(jīng)得到要調(diào)用的遠(yuǎn)程服務(wù)對應(yīng)的 invoker 了,此時(shí)根據(jù)具體的協(xié)議構(gòu)造請求頭,然后將參數(shù)根據(jù)具體的序列化協(xié)議序列化之后構(gòu)造塞入請求體中,再通過 NettyClient 發(fā)起遠(yuǎn)程調(diào)用。 服務(wù)端 NettyServer 收到請求之后,根據(jù)協(xié)議得到信息并且反序列化成對象,再按照派發(fā)策略派發(fā)消息,默認(rèn)是 All,扔給業(yè)務(wù)線程池。 業(yè)務(wù)線程會(huì)根據(jù)消息類型判斷然后得到 serviceKey 從之前服務(wù)暴露生成的 exporterMap 中得到對應(yīng)的 Invoker ,然后調(diào)用真實(shí)的實(shí)現(xiàn)類。 最終將結(jié)果返回,因?yàn)檎埱蠛晚憫?yīng)都有一個(gè)統(tǒng)一的 ID, 客戶端根據(jù)響應(yīng)的 ID 找到存儲(chǔ)起來的 Future, 然后塞入響應(yīng)再喚醒等待 future 的線程,完成一次遠(yuǎn)程調(diào)用全過程。 而且還小談了下模板方法這個(gè)設(shè)計(jì)模式,當(dāng)然其實(shí)隱藏了很多設(shè)計(jì)模式在其中,比如責(zé)任鏈、裝飾器等等,沒有特意挑開來說,源碼中太常見了,基本上無處不在。
網(wǎng)絡(luò)是以字節(jié)流的形式傳輸?shù)?/strong>,相對于我們的對象來說,我們對象是多維的,而字節(jié)流是一維的,我們需要把我們的對象壓縮成一維的字節(jié)流傳輸?shù)綄Χ恕?/p>
其實(shí)從上圖的協(xié)議中可以得知 Dubbo 支持很多種序列化,我不具體分析每一種協(xié)議,就大致分析序列化的種類,萬變不離其宗。
我們來看一下官網(wǎng)的圖。
客戶端調(diào)用一下代碼。
這其實(shí)就是很常見的設(shè)計(jì)模式之一,模板方法。如果你經(jīng)常看源碼的話你知道這個(gè)設(shè)計(jì)模式真的是太常見的。
我們再來看那個(gè) list(invocation),其實(shí)就是通過方法名找 Invoker,然后服務(wù)的路由過濾一波,也有再造一個(gè) MockInvoker 的。
從上面的代碼可以看到調(diào)用一共分為三種,分別是 oneway、異步、同步。
服務(wù)端接收到請求之后就會(huì)解析請求得到消息,這消息又有五種派發(fā)策略:
今天的調(diào)用過程我再總結(jié)一遍應(yīng)該差不多了。