當前位置: 妍妍網 > 碼農

7000字+22張圖探秘Dubbo一次RPC呼叫的核心流程

2024-07-17碼農

大家好, 今天繼續探秘系列,扒一扒一次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在設計上的優秀之處。

    好了,本文就講到這裏,如果覺得本文對你有所幫助,歡迎點贊、在看、收藏、轉發分享給其他需要的人

    ·END·

    IT交流群

    組建了程式設計師,架構師,IT從業者交流群,以 交流技術 職位內推 行業探討 為主

    加小編 好友 ,備註"加群"