阿里面試:dubbo的服務引用過程

點贊再看,養成習慣,微信搜一搜【三太子敖丙】關注這個喜歡寫情懷的程序員。

本文 GitHub https://github.com/JavaFamily 已收錄,有一線大廠面試完整考點、資料以及我的系列文章。

上篇文章我們已經瞭解了 Dubbo 服務暴露全過程,這篇文章我就帶着大家再來看看 Dubbo 服務引入全流程,這篇服務引入寫完下一篇就要來個全鏈路打通了,看看大家看完會不會有種任督二脈都被打通的感覺。

在寫文章的過程中丙還發現官網的一點小問題,下文中會提到。

話不多說,咱們直接進入正題。

服務引用大致流程

我們已經得知 Provider將自己的服務暴露出來,註冊到註冊中心,而 Consumer無非就是通過一波操作從註冊中心得知 Provider 的信息,然後自己封裝一個調用類和 Provider 進行深入地交流。

而之前的文章我都已經提到在 Dubbo中一個可執行體就是 Invoker,所有調用都要向 Invoker 靠攏,因此可以推斷出應該要先生成一個 Invoker,然後又因爲框架需要往不侵入業務代碼的方向發展,那我們的 Consumer 需要無感知的調用遠程接口,因此需要搞個代理類,包裝一下屏蔽底層的細節。

整體大致流程如下:

服務引入的時機

服務的引入和服務的暴露一樣,也是通過 spring 自定義標籤機制解析生成對應的 Bean,Provider Service 對應解析的是 ServiceBean 而 Consumer Reference 對應的是 ReferenceBean

前面服務暴露的時機我們上篇文章分析過了,在 Spring 容器刷新完成之後開始暴露,而服務的引入時機有兩種,第一種是餓漢式,第二種是懶漢式。

餓漢式是通過實現 Spring 的InitializingBean接口中的 afterPropertiesSet方法,容器通過調用 ReferenceBeanafterPropertiesSet方法時引入服務。

懶漢式是隻有當這個服務被注入到其他類中時啓動引入流程,也就是說用到了纔會開始服務引入。

默認情況下,Dubbo 使用懶漢式引入服務,如果需要使用餓漢式,可通過配置 dubbo:reference 的 init 屬性開啓。

我們可以看到 ReferenceBean還實現了FactoryBean接口,這裏有個關於 Spring 的面試點我帶大家分析一波。

BeanFactory 、FactoryBean、ObjectFactory

就是這三個玩意,我單獨拿出來說一下,從字面上來看其實可以得知BeanFactoryObjectFactory是個工廠而FactoryBean是個 Bean。

BeanFactory 其實就是 IOC 容器,有多種實現類我就不分析了,簡單的說就是 Spring 裏面的 Bean 都歸它管,而FactoryBean也是 Bean 所以說也是歸 BeanFactory 管理的。

FactoryBean 到底是個什麼 Bean 呢?它其實就是把你真實想要的 Bean 封裝了一層,在真正要獲取這個 Bean 的時候容器會調用 FactoryBean#getObject() 方法,而在這個方法裏面你可以進行一些複雜的組裝操作。

這個方法就封裝了真實想要的對象複雜的創建過程

到這裏其實就很清楚了,就是在真實想要的 Bean 創建比較複雜的情況下,或者是一些第三方 Bean 難以修改的情形,使用 FactoryBean 封裝了一層,屏蔽了底層創建的細節,便於 Bean 的使用。

而 ObjectFactory 這個是用於延遲查找的場景,它就是一個普通工廠,當得到 ObjectFactory 對象時,相當於 Bean 沒有被創建,只有當 getObject() 方法時,纔會觸發 Bean 實例化等生命週期。

主要用於暫時性地獲取某個 Bean Holder 對象,如果過早的加載,可能會引起一些意外的情況,比如當 Bean A 依賴 Bean B 時,如果過早地初始化 A,那麼 B 裏面的狀態可能是中間狀態,這時候使用 A 容易導致一些錯誤。

總結的說 BeanFactory 就是 IOC 容器,FactoryBean 是特殊的 Bean, 用來封裝創建比較複雜的對象,而 ObjectFactory 主要用於延遲查找的場景,延遲實例化對象

服務引入的三種方式

服務的引入又分爲了三種,第一種是本地引入、第二種是直接連接引入遠程服務、第三種是通過註冊中心引入遠程服務。

本地引入不知道大家是否還有印象,之前服務暴露的流程每個服務都會通過搞一個本地暴露,走 injvm 協議(當然你要是 scope = remote 就沒本地引用了),因爲存在一個服務端既是 Provider 又是 Consumer 的情況,然後有可能自己會調用自己的服務,因此就弄了一個本地引入,這樣就避免了遠程網絡調用的開銷。

所以服務引入會先去本地緩存找找看有沒有本地服務

直連遠程引入服務,這個其實就是平日測試的情況下用用,不需要啓動註冊中心,由 Consumer 直接配置寫死 Provider 的地址,然後直連即可。

註冊中心引入遠程服務,這個就是重點了,Consumer 通過註冊中心得知 Provider 的相關信息,然後進行服務的引入,這裏還包括多註冊中心,同一個服務多個提供者的情況,如何抉擇如何封裝,如何進行負載均衡、容錯並且讓使用者無感知,這就是個技術活。

本文用的就是單註冊中心引入遠程服務,讓我們來看看 Dubbo 是如何做的吧。

服務引入流程解析

默認是懶漢式的,所以服務引入的入口就是 ReferenceBean 的 getObject 方法。

可以看到很簡單,就是調用 get 方法,如果當前還沒有這個引用那麼就執行 init 方法。

官網的一個小問題

這個問題就在 if (ref == null) 這一行,其實是一位老哥在調試的時候發現這個 ref 竟然不等於 null,因此就進不到 init 方法裏面調試了,後來他發現是因爲 IDEA 爲了顯示對象的信息,會通過 toString 方法獲取對象對應的信息。

toString 調用的是 AbstractConfig#toString,而這個方法會通過反射調用了 ReferenceBean 的 getObject 方法,觸發了引入服務動作,所以說到斷點的時候 ref != null

可以看到是通過方法名來進行反射調用的,而 getObject 就是 get 開頭的,因此會被調用。

所以這個哥們提了個 PR,但是一開始沒有被接受,一位 Member 認爲這不是 bug, idea 設置一下不讓調用 toString 就好了。

不過另一位 Member 覺得這個 PR 挺好的,並且 Dubbo 項目二代掌門人北緯30也發話了,因此這個 PR 被受理了。

至此我們已經知道這個小問題了,然後官網上其實也寫的很清楚。

但是小問題來了,之前我在文章提到我的源碼版本是 2.6.5,是在 github 的 releases 裏面下的,這個 tostring 問題其實我挺早之前就知道了,我想的是我 2.6.5 穩的一批,誰知道翻車了。

我調試的時候也沒進到 init 方法因爲 ref 也沒等於 null,我就奇怪了,我裏面去看了下 toString 方法,2.6.5版本竟然沒有修改?沒有將 getObject 做過濾,因此還是被調用了。

我又打開了2.7.5版本的代碼,發現是修改過的判斷。

我又去特意下了 2.6.6 版本的代碼,發現也是修改過的,因此這個修改並不是隨着 2.6.5 版本發佈,而是 2.6.6,除非我下的是個假包,這就是我說的小問題了,不過影響不大。

其實提到這一段主要想說的是那個 PR,作爲一個開源軟件的輸出者,很多細節也是很重要的,這個問題其實很影響源碼的調試,因爲對代碼不熟,肯定會一臉懵逼,誰知道是不是哪個後臺線程異步引入了呢。

提這個 PR 的老哥花了兩個小時才搞清楚真正的原因,所以說雖然這不是個 bug 但是很影響那些想深入瞭解 Dubbo 內部結構的同學們,這種改配置去適應的方案是不可取了,還好最終的方案是改代碼。

好了讓我們回到今天的主題,接下來分析的就是那個不讓我進去的 init 方法了。

源碼分析

init 方法很長,不過大部分就是檢查配置然後將配置構建成 map ,這一大段我就不分析了,我們直接看一下構建完的 map 長什麼樣。

然後就進入重點方法 createProxy,從名字可以得到就是要創建的一個代理,因爲代碼很長,我就一段一段的分析

如果是走本地的話,那麼直接構建個走本地協議的 URL 然後進行服務的引入,即 refprotocol.refer,這個方法之後會做分析,本地的引入就不深入了,就是去之前服務暴露的 exporterMap 拿到服務。

如果不是本地,那肯定是遠程了,接下來就是判斷是點對點直連 provider 還是通過註冊中心拿到 provider 信息再連接 provider 了,我們分析一下配置了 url 的情況,如果配置了 url 那麼不是直連的地址,就是註冊中心的地址。

然後就是沒配置 url 的情況,到這裏肯定走的就是註冊中心引入遠程服務了。

最終拼接出來的 URL 長這樣。

可以看到這一部分其實就是根據各種參數來組裝 URL ,因爲我們的自適應擴展都需要根據 URL 的參數來進行的。

至此我先畫個圖,給大家先捋一下。

這其實就是整個流程了,簡述一下就是先檢查配置,通過配置構建一個 map ,然後利用 map 來構建 URL ,再通過 URL 上的協議利用自適應擴展機制調用對應的 protocol.refer 得到相應的 invoker 。

在有多個 URL 的時候,先遍歷構建出 invoker 然後再由 StaticDirectory 封裝一下,然後通過 cluster 進行合併,只暴露出一個 invoker 。

然後再構建代理,封裝 invoker 返回服務引用,之後 Comsumer 調用的就是這個代理類。

相信通過圖和上面總結性的簡述已經知道大致的服務引入流程了,不過還是有很多細節,比如如何從註冊中心得到 Provider 的地址,invoker 裏面到底是怎麼樣的?別急,我們繼續看。

從前面的截圖我們可以看到此時的協議是 registry 因此走的是 RegistryProtocol#refer,我們來看一下這個方法。

主要就是獲取註冊中心實例,然後調用 doRefer 進行真正的 refer。

這個方法很關鍵,可以看到生成了RegistryDirectory 這個 directory 塞了註冊中心實例,它自身也實現了NotifyListener 接口,因此註冊中心的監聽其實是靠這傢伙來處理的

然後向註冊中心註冊自身的信息,並且向註冊中心訂閱了 providers 節點、 configurators 節點 和 routers 節點,訂閱了之後 RegistryDirectory 會收到這幾個節點下的信息,就會觸發 DubboInvoker 的生成了,即用於遠程調用的 Invoker

然後通過 cluster 再包裝一下得到 Invoker,因此一個服務可能有多個提供者,最終在 ProviderConsumerRegTable 中記錄這些信息,然後返回 Invoker。

所以我們知道Conusmer 是在 RegistryProtocol#refer 中向註冊中心註冊自己的信息,並且訂閱 Provider 和配置的一些相關信息,我們看看訂閱返回的信息是怎樣的。

拿到了Provider的信息之後就可以通過監聽觸發 DubboProtocol# refer 了(具體調用哪個 protocol 還是得看 URL的協議的,我們這裏是 dubbo 協議),整個觸發流程我就不一一跟一下了,看下調用棧就清楚了。

終於我們從註冊中心拿到遠程Provider 的信息了,然後進行服務的引入。

這裏的重點在 getClients,因爲終究是要跟遠程服務進行網絡調用的,而 getClients 就是用於獲取客戶端實例,實例類型爲 ExchangeClient,底層依賴 Netty 來進行網絡通信,並且可以看到默認是共享連接。

getSharedClient 我就不分析了,就是通過遠程地址找 client ,這個 client 還有引用計數的功能,如果該遠程地址還沒有 client 則調用 initClient,我們就來看一下 initClient 方法。

而這個connect最終返回 HeaderExchangeClient裏面封裝的是 NettyClient

然後最終得到的 Invoker就是這個樣子,可以看到記錄的很多信息,基本上該有的都有了,我這裏走的是對應的服務只有一個 url 的情況,多個 url 無非也是利用 directorycluster再封裝一層。

最終將調用 return (T) proxyFactory.getProxy(invoker); 返回一個代理對象,這個就不做分析了。

到這裏,整個流程就是分析完了,不知道大家清晰了沒?我再補充前面的圖,來一個完整的流程給大家再過一遍。

小結

相信分析下來整個流程不難的,總結地說無非就是通過配置組成 URL ,然後通過自適應得到對於的實現類進行服務引入,如果是註冊中心那麼會向註冊中心註冊自己的信息,然後訂閱註冊中心相關信息,得到遠程 provider的 ip 等信息,再通過netty客戶端進行連接。

並且通過directorycluster 進行底層多個服務提供者的屏蔽、容錯和負載均衡等,這個之後文章會詳細分析,最終得到封裝好的 invoker再通過動態代理封裝得到代理類,讓接口調用者無感知的調用方法。

最後

今天這篇文章看下來相信大家對服務的引入應該有了清晰的認識,其實裏面還是很多細節我沒有展開分析,比如一些過濾鏈的組裝,這其實在服務暴露的文章裏面已經說了,同樣服務引用也有過濾鏈,不過篇幅有限就不展開了,抓住主線要緊。

至此我已經帶大家先過了一遍 Dubbo 的整體概念和大致流程,介紹了 Dubbo SPI機制,並且分析了服務的暴露流程服務引入流程,具體的細節還是得大家自己去摸索,大致的流程我都講的差不多了。

dubbo系列也快接近尾聲了,雖然我知道每次寫硬核技術看的小夥伴就少了很多,但是還是想寫完這個系列,感謝大家的支持。

我是敖丙,你知道的越多,你不知道的越多,我們下期見!

人才們的 【三連】 就是敖丙創作的最大動力,如果本篇博客有任何錯誤和建議,歡迎人才們留言!


文章持續更新,可以微信搜一搜「 三太子敖丙 」第一時間閱讀,回覆【資料】有我準備的一線大廠面試資料和簡歷模板,本文 GitHub https://github.com/JavaFamily 已經收錄,有大廠面試完整考點,歡迎Star。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章