7000字+22張圖探秘Dubbo一次RPC調用的核心流程
今天繼續探秘系列,扒一扒一次RPC請求在Dubbo中經歷的核心流程。
本文是基于Dubbo3.x版本進行講解
一個簡單的Demo
這里還是老樣子,為了保證文章的完整性和連貫性,方便那些沒有使用過的小伙伴更加容易接受文章的內容,這里快速講一講Dubbo一個簡單的Demo。
在Dubbo中RPC調用過程中主要分為以下兩個角色:
- 服務提供者:提供一個接口給消費者遠程調用。
- 服務消費者:調用生產者提供的接口。
于是一個簡單的Dubbo示例工程就如下所示:
示例工程的創建步驟、使用配置、第三方的依賴等詳細內容可參考官網:https://cn.dubbo.apache.org/zh-cn/overview/quickstart/java/spring-boot/
接口層,提供者消費者都需要依賴,服務提供者實現,服務消費者調用
圖片
服務提供者單獨一個工程,實現DemoService接口,通過@DubboService表明提供DemoService這個服務。
圖片
服務消費者單獨一個工程,這里使用單元測試,通過@DubboReference注解表明消費DemoService這個服務接口。
圖片
啟動服務提供者,運行消費者單元測試,結果如下:
成功實現遠程服務調用
服務提供者暴露
所謂的服務提供者暴露,主要就是指在項目啟動時服務提供者去做的兩件事
第一件事就是,由于需要對外提供調用服務,接受消費者的請求
所以在啟動時需要根據使用協議,以及協議對應的端口啟動一個對應的服務
就拿前面DemoService來舉例,由于@DubboService注解沒有指定任何信息
所以DemoService默認就是使用Dubbo框架自己寫的通信協議,也就是Dubbo協議,這個協議默認使用的端口就是20880
之后如果要調用DemoService的方法時,就可以按照Dubbo協議要求組裝數據格式
向20880端口發送請求,從而就實現遠程服務調用,如下圖所示:
圖片
當然除了默認的Dubbo協議之外,Dubbo還支持其它的通信協議,后面會詳細介紹:
雖然第一件事成功讓接口可以對外提供訪問,但是對于消費者來說,它其實還是無法訪問接口。
因為消費者并不知道接口使用的是哪個通信協議、端口,也不知道接口所在的服務器的ip。
于是,在啟動時就會去做第二件事。
第二件事是將每個接口的詳細信息,包括接口的全限定名、方法名稱、方法參數、服務器的ip、端口、通信協議等等按照一定的格式組裝好。
存放到元數據中心和服務提供者本地緩存中。
注意這是3.x版本時的存儲情況,跟2.x有點不同。并且元數據中心其實就是使用的Nacos或者Zookeeper來實現的,所以你可以認為就是存儲在Nacos或者Zookeeper中。
之后消費者需要調用接口時,就可以從元數據中心或者服務提供者本地緩存中獲取到接口的詳細信息(具體從哪取決于配置,默認是從本地緩存中獲取)。
圖片
這里你肯定有疑問消費者是如何從服務提供者本地緩存獲取,這就涉及到Dubbo3.x應用級服務注冊的邏輯了,所以就不詳細展開了,不過立個flag,如果本篇文章點贊達到38個,就再來一篇,單獨講一講Dubbo3.x應用級服務注冊的原理。
當需要發起調用時,就可以按照接口使用的協議組裝數據,向接口所在的服務器ip和端口發送請求。
所以總的來說,服務提供者暴露主要就是這兩件事:
- 根據接口使用協議和端口開啟服務,對外提供接口訪問。
- 將當前服務支持的接口,以及每個接口使用的協議、端口、服務器ip等信息存到元數據中心或者本地緩存,供消費者獲取。
消費者引用
前面提到,如果消費者想引用遠程服務,可以通過@DubboReference注解觸發引用的邏輯。
消費者引用也會去做兩件事。
第一件事我們都知道,那就是創建接口的動態代理。
由于消費者使用的DubboService是一個接口,所以會給DubboService創建一個動態代理。
這個動態代理最終也會發送請求RPC請求。
Dubbo支持兩種動態搭理生成方式:
- JDK動態搭理
- Javassist動態生成字節碼
默認使用的Javassist動態生成字節碼的方式。
除了創建動態搭理之外,還會去獲取服務提供者的接口詳細信息。
上面一節說了,可以從元數據中心或者是服務提供者本地緩存中獲取到。
當獲取到接口詳情數據之后,會為之后的RPC調用做一些準備工作。
比如如果接口使用的是Dubbo通信協議的話,準備工作就是消費者會跟服務提供者機器建立長連接。
好了,到這里我們就把服務者暴露和消費者引用都講完了。
接下來就會進入本文的主題,一次RPC調用,也就是調用動態代理之后在Dubbo中會經歷哪些環節。
參數封裝
熟悉JDK動態代理的同學肯定知道,當調用動態代理方法時,最終會走到InvocationHandler的實現。
圖片
在Dubbo中,調用消費者動態代理的時候,不論是JDK動態代理還是使用Javassist方式生成動態代理。
最終都會走到InvokerInvocationHandler這個InvocationHandler的實現。
圖片
所以這個整個RPC調用的起點就是invoke方法的實現;
圖片
如圖所示,首先將RPC調用的接口、方法名、參數封裝到RpcInvocation中。
接著會走到下面這行代碼。
invoker.invoke(rpcInvocation)
而這看似簡簡單單一行代碼就會觸發RPC調用的整個核心流程:
ClusterFilter過濾
當參數封裝完成之后,接下來就會走到ClusterFilter過濾環節。
圖片
ClusterFilter本質是一種責任鏈模式,是Dubbo提供的一個重要擴展點。
通過實現invoke方法對請求進行自定義預處理操作。
Dubbo默認提供了幾種實現。
圖片
比如就拿MonitorClusterFilter來說:
這個實現主要是去統計每個接口的每個方法調用成功多少次,調用失敗多少次等等調用的信息。
除了默認實現之外,很多我們熟悉的一些框架也是通過這個擴展點跟Dubbo進行整合的。
就比如常見的流控框架Sentinel!
圖片
集群調用邏輯決策
當走完ClusterFilter之后,接下來就會來到集群調用邏輯決策的環節。
這個集群調用邏輯決策是什么意思呢?
在實際生產環境中,一般服務都會以集群的方式來部署。
這就會產生一個問題,面對多服務情況下,怎么去調用?
圖片
舉個例子,按圖上所示,有三個服務
那么集群調用邏輯就是去決定。
應該每個服務都去調用一次,還是只去調用其中一個?
如果只調用其中一個,比如調用服務1,如果失敗了,那么此時是直接拋異常還是選擇繼續去調用服務2,還是做其它的事?
所以集群調用邏輯就是解決多服務實例下,應該怎樣合理地調用服務實例。
Dubbo提供了以下幾種集群調用邏輯:
- 廣播,也就是每個服務都調用(broadcast)。
- 調用前會去判斷服務是不是可用,如果可用,那么就直接進行調用(available)。
- 調用失敗,會開啟定時任務進行重試調用,最大重試3次(failback)。
- 調用失敗就直接拋出異常(failfast)。
- 調用失敗直接調用其它服務進行重試,故障轉移(failover)。
- 調用失敗不會拋異常,而是直接返回(failsafe)。
- 同時調用指定個數的服務,直接最快返回結果當做這個調用的結果(forking)。
- 調用每個服務,合并服務返回的數據作為調用的結果,結果怎么合并需要我們自定義相關邏輯(mergeable)。
在默認情況下使用的就是failover,也就是出現異常時會調用其它的服務再返回結果。
當然我們也可以按照如下的方式指定調用策略。
圖片
路由策略
上一節是解決集群中眾多實例時應該如何調用的問題。
而路由策略其實是選擇允許調用哪些服務實例。
因為并不是所有的服務實例都符合調用要求。
什么意思呢?
舉個例子,現在有個灰度發布的場景。
假設所有的服務都處于同一套環境中,有一群機器運行者之前正式版本的服務,有一群機器運行著灰度版本的服務,如下圖所示:
圖片
那么對于處于灰度的消費者肯定要調用處于灰度的服務提供者。
但是由于在同一套環境,那么處于灰度的消費者其實是能獲取到處于之前正式環境的服務接口信息。
如果就這么直接調用,那么處于灰度的消費者就可能調用非灰度的服務提供者。
這肯定是不允許的。
所以必須在調用前過濾掉非灰度發布的服務。
而這種情況就可以交給路由來過濾。
假設如果想做到灰度區分,可以使用Dubbo提供了一種叫tag的路由策略。
灰度的服務提供者可以指定自己的tag屬性為gray(灰色的意思),如下所示:
圖片
而對于處于灰度的消費者,只需要指定消費tag為gray的服務提供者,如下所示:
圖片
這樣在真正調用前就會通過tag路由的方式過濾出處于灰度的服務提供者。
所以集群調用邏輯所能使用的服務實例只能是經過路由策略選擇出來。
圖片
除了tag路由策略之外,Dubbo還提供了以下幾種路由策略:
- 條件路由,可以指定某些條件下可以調用哪些服務實例。
- 腳本路由,可以寫一段JavaScript腳本,更加靈活地選擇哪些服務實例。
順帶說一句,這個路由功能可以用來實現一個更高大上的功能,叫做流量管控。
負載均衡
所謂的負載均衡就是說,面對多個服務實例,我們應該按照何種算法選擇一個供我們調用。
Dubbo提供了以下幾種負載均衡策略:
- 隨機(random),隨機選擇一個
- 輪詢(roundrobin),每次調用按照順序選擇一個
- 最少活躍優先(leastactive),優先選擇被最少調用的服務
- 最短響應優先(shortestresponse),優先選擇響應時間斷的服務調用
- 一致性Hash(consistenthash)
在沒有指定的情況下,默認使用的就是隨機(random)算法:
如果想進行修改,可以按照如下方式:
圖片
這里你肯定有疑問;
這個負載均衡和集群調用策略有什么關系?感覺這兩者有點像,又感覺這兩者有點沖突。
其實集群調用策略的優先級會大于負載均衡。
比如說如果集群調用策略選擇默認,也就是故障轉移(failover)。
那么對于路由策略過濾出來的服務實例,會根據負載均衡算法選擇一個進行調用。
但是如果集群調用策略選擇的是廣播調用(broadcast)。
那么對于路由策略過濾出來的服務實例,實際上每個都需要去調用。
所以此時壓根不需要走負載均衡策略,因為沒有意義,即使你配置了,也不會生效。
所以需不需要負載均衡這件事,取決于使用什么集群調用策略。
總的來說,集群調用策略、路由策略、負載均衡策略它們一步一步去決定本次RPC調用具體應該調用哪個或者哪些服務實例:
三者關系入下圖所示:
圖片
Filter過濾
經過上面的幾步,終于知道本地RPC請求需要請求哪個或者哪些具體的服務實例。
接下來只需要向對應的服務實例發送請求就可以了。
不過在發送請求前,Dubbo還預留了一個擴展點,叫做Filter。
本質也是一種責任鏈模式。
圖片
通過Filter,我們可以在RPC調用前對整個請求再進行自定義擴展。
這里你肯定又會有一個疑問?
Filter和前面提到的ClusterFilter有什么區別?
的確它兩真的很像,甚至都繼承同一個接口BaseFilter,但是它兩還有一些區別。
第一點,兩者作用時機不同。
通過講解順序我們可以看出,ClusterFilter作用在路由和負載均衡前,而Filter在路由和負載均衡后。
所以只要我們愿意,我們可以通過ClusterFilter去影響后面的路由和負載均衡,而Filter是做不到的。
第二點就是Filter是跟服務實例走的。
在調用每個服務實例之前,Filter一定會都會重新調用一遍。
比如假設這次RPC最終需要選擇調用兩個服務實例,那么Filter會走兩遍。
但是對于ClusterFilter,在整個調用過程中它僅僅只會執行一次。
所以官方也是建議,在無特殊情況下,優先選擇使用ClusterFilter而不是Filter。
到這,畫一張圖總結一下前面整個調用環節用。
圖片
通信協議
當Filter責任鏈走完之后,接下來就到了向服務實例發送請求的時候了。
一旦涉及到服務與服務之間的調用,那么就離不開通信協議。
所謂的通信協議,講的簡單點就是發送方把需要發送的數據按照一定的格式組裝好之后再發送給接收方。
Dubbo需要發送數據包括調用但不限于接口全限定名、調用的方法名、調用參數等等。
而接收方在獲取到數據時再使用對應的格式去解析,從而獲取到請求數據。
前面提到,Dubbo默認使用的通信協議是Dubbo自己的寫的,叫做Dubbo協議。
除了Dubbo協議之外,Dubbo還支持以下幾種通信協議:
- Rest
- gRPC
- Triple
- ...
Rest,就是我們說的Http協議。
當使用這種協議的時候,Dubbo在啟動的時候會去創建一個Http的服務。
默認使用的是Jetty,當然也支持切換成Tomcat。
gRPC,谷歌開源的高性能RPC框架。
當然使用gRPC的時候,服務提供者會啟動一個gRPC的服務端。
這里你可能有疑問,Dubbo是RPC框架,gRPC也是RPC框架,為什么要集成gRPC。
其實這是因為Dubbo和gRPC定位不同。
Dubbo其實不僅僅是一個RPC框架,它其實是一套微服務解決方案,會承擔更多的服務治理相關的邏輯。
而gRPC的定位是通信協議與實現,是一款純粹的RPC框架。
Triple協議就比較厲害了,它是Dubbo在3.x時發布的通信協議。
Triple完全兼容gRPC協議,可同時運行在HTTP/1和HTTP/2傳輸協議之上,讓你可以直接使用curl、瀏覽器訪問后端Dubbo服務。
如果要想使用上面的這些協議,代碼可能需要進行一些改動,這里就不演示了。
序列化協議
上一節提到,數據在發送的時候需要根據通信協議按照要求去組裝數據。
但是我們都知道,數據在網絡中傳輸使用的是二進制。
所以在實際開發中,要想發送數據,一般都是先將需要傳輸的數據轉換成字節序列(數組),之后再交由操作系統轉換成二進制進行傳輸。
于是就有了一個問題,比如我們想傳輸一個對象的數據,那么我們應該按照什么樣的格式將對象的數據轉換成字節序列呢?
而這個按照什么樣的格式就被稱為序列化協議。
整個轉換過程就被稱為序列化,也可以被稱為編碼。
圖片
既然有序列化,那么就有反序列化。
所謂反序列化就是根據序列化協議將字節序列轉換成數據,也被稱為解碼。
當通信協議使用Dubbo協議時,Dubbo支持以下幾種序列化協議:
- Java原生
- Hessian2
- Fastjson2
- ...
Dubbo在3.2.0版本之前默認使用的Hessian2協議,3.2.0之后默認使用Fastjson2作為序列化協議。
到這里其實就算講完了消費者整個調用的過程了。
因為當序列化完成之后,接下來就只需要將字節序列通過網絡發送出去即可。
服務提供者處理請求
當服務提供者監聽到有請求時,會獲取到請求的字節序列。
然后根據通信協議,序列化協議反序列化出傳輸的數據。
從而獲取到消費者需要調用的、接口、方法以及入參等數據。
之后就可以找到調用接口對應的實現,通過反射進行調用,獲取結果。
然后再將結果序列化成字節數組,返回給消費者。
這樣服務提供者就處理完成了一次請求。
不過這里面有一個小細節,那就是在調用接口的實現之前,也會經過Filter過濾。
所以Filter過濾其實在提供者和消費者兩者都有。
圖片
但是需要注意的是,兩邊的Filter不一定相同,具體取決于這個Filter是作用在消費者端還是提供者端,可通過如下方式配置
圖片
總結
到這終于講完了一次RPC請求在Dubbo中經歷整個核心流程。
不知道你看完有什么感受。
這里我再來畫一張圖總結整個調用過程。
圖片
值得注意是,上面提到的所有調用環節,注意說的是所有,Dubbo都留了對應的擴展點。
也就是說,小到一個Filter,大到整個通信協議你都可以進行自定義擴展。
從這也可以看出,Dubbo在設計上的優秀之處。