大家好,我是三友~~
今天繼續探秘系列,扒一扒一次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在設計上的優秀之處。
好了,本文就講到這裡,如果覺得本文對你有所幫助,歡迎點贊、在看、收藏、轉發分享給其他需要的人
你的支援就是我更新的最大動力,感謝感謝!
掃碼或者搜尋關注公眾號 三友的java日記 ,及時乾貨不錯過,公眾號致力於透過畫圖加上通俗易懂的語言講解技術,讓技術更加容易學習,回覆 面試 即可獲得一套面試真題。
讓我們下期再見,拜拜!
往期熱門文章推薦
如何去閱讀原始碼,我總結了18條心法
如何寫出漂亮程式碼,我總結了45個小技巧
三萬字盤點Spring/Boot的那些常用擴充套件點
三萬字盤點Spring 9大核心基礎功能
兩萬字盤點那些被玩爛了的設計模式
萬字+20張圖探秘Nacos註冊中心核心實現原理
萬字+20張圖剖析Spring啟動時12個核心步驟
1.5萬字+30張圖盤點索引常見的11個知識點