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