天天看點

Dubbo 源碼分析 - 叢集容錯之 Directory

前面文章分析了服務的導出與引用過程,從本篇文章開始,我将開始分析 Dubbo 叢集容錯方面的源碼。這部分源碼包含四個部分,分别是服務目錄 Directory、服務路由 Router、叢集 Cluster 和負載均衡 LoadBalance。這幾個部分的源碼邏輯比較獨立,我會分四篇文章進行分析。本篇文章作為叢集容錯的開篇文章,将和大家一起分析服務目錄相關的源碼。在進行深入分析之前,我們先來了解一下服務目錄是什麼。服務目錄中存儲了一些和服務提供者有關的資訊,通過服務目錄,服務消費者可擷取到服務提供者的資訊,比如 ip、端口、服務協定等。通過這些資訊,服務消費者就可通過 Netty 等用戶端進行遠端調用。在一個服務叢集中,服務提供者數量并不是一成不變的,如果叢集中新增了一台機器,相應地在服務目錄中就要新增一條服務提供者記錄。或者,如果服務提供者的配置修改了,服務目錄中的記錄也要做相應的更新。如果這樣說,服務目錄和注冊中心的功能不就雷同了嗎。确實如此,這裡這麼說是為了友善大家了解。實際上服務目錄在擷取注冊中心的服務配置資訊後,會為每條配置資訊生成一個 Invoker 對象,并把這個 Invoker 對象存儲起來,這個 Invoker 才是服務目錄最終持有的對象。Invoker 有什麼用呢?看名字就知道了,這是一個具有遠端調用功能的對象。講到這大家應該知道了什麼是服務目錄了,它可以看做是 Invoker 集合,且這個集合中的元素會随注冊中心的變化而進行動态調整。

好了,關于服務目錄這裡就先介紹這些,大家先有個大緻印象即可。接下來我們通過繼承體系圖來了解一下服務目錄的家族成員都有哪些。

服務目錄目前内置的實作有兩個,分别為 StaticDirectory 和 RegistryDirectory,它們均是 AbstractDirectory 的子類。AbstractDirectory 實作了 Directory 接口,這個接口包含了一個重要的方法定義,即 list(Invocation),用于列舉 Invoker。下面我們來看一下他們的繼承體系圖。

Dubbo 源碼分析 - 叢集容錯之 Directory

如上,Directory 繼承自 Node 接口,Node 這個接口繼承者比較多,像 Registry、Monitor、Invoker 等繼承了這個接口。這個接口包含了一個擷取配置資訊的方法 getUrl,實作該接口的類可以向外提供配置資訊。另外,大家注意看 RegistryDirectory 實作了 NotifyListener 接口,當注冊中心節點資訊發生變化後,RegistryDirectory 可以通過此接口方法得到變更資訊,并根據變更資訊動态調整内部 Invoker 清單。

現在大家對服務目錄的繼承體系應該比較清楚了,下面我們深入到源碼中,探索服務目錄是如何實作的。

本章我将分析 AbstractDirectory 和它兩個子類的源碼。這裡之是以要分析 AbstractDirectory,而不是直接分析子類是有一定原因的。AbstractDirectory 封裝了 Invoker 列舉流程,具體的列舉邏輯則由子類實作,這是典型的模闆模式。是以,接下來我們先來看一下 AbstractDirectory 的源碼。

上面就是 AbstractDirectory 的 list 方法源碼,這個方法封裝了 Invoker 的列舉過程。如下:

調用 doList 擷取 Invoker 清單

根據 Router 的 getUrl 傳回值為空與否,以及 runtime 參數決定是否進行服務路由

以上步驟中,doList 是模闆方法,需由子類實作。Router 的 runtime 參數這裡簡單說明一下,這個參數決定了是否在每次調用服務時都執行路由規則。如果 runtime 為 true,那麼每次調用服務前,都需要進行服務路由。這個對性能造成影響,慎重配置。關于該參數更詳細的說明,請參考官方文檔。

介紹完 AbstractDirectory,接下來我們開始分析子類的源碼。

StaticDirectory 即靜态服務目錄,顧名思義,它内部存放的 Invoker 是不會變動的。是以,理論上它和不可變 List 的功能很相似。下面我們來看一下這個類的實作。

以上就是 StaticDirectory 的代碼邏輯,很簡單,大家都能看懂,我就不多說了。下面來看看 RegistryDirectory,這個類的邏輯比較複雜。

RegistryDirectory 是一種動态服務目錄,它實作了 NotifyListener 接口。當注冊中心服務配置發生變化後,RegistryDirectory 可收到與目前服務相關的變化。收到變更通知後,RegistryDirectory 可根據配置變更資訊重新整理 Invoker 清單。RegistryDirectory 中有幾個比較重要的邏輯,第一是 Invoker 的列舉邏輯,第二是接受服務配置變更的邏輯,第三是 Invoker 的重新整理邏輯。接下來,我将按順序對這三塊邏輯。

Invoker 列舉邏輯封裝在 doList 方法中,這是個模闆方法,前面已經介紹過了。那這裡就不過多啰嗦了,我們直入主題吧。

以上代碼進行多次嘗試,以期從 localMethodInvokerMap 中擷取到 Invoker 清單。一般情況下,普通的調用可通過方法名擷取到對應的 Invoker 清單,泛化調用可通過 * 擷取到 Invoker 清單。按現有的邏輯,不管什麼情況下,* 到 Invoker 清單的映射關系 <*, invokers> 總是存在的,也就意味着 localMethodInvokerMap.get(Constants.ANY_VALUE) 總是有值傳回。除非這個值是 null,才會通過通過疊代器擷取 Invoker 清單。至于什麼情況下為空,我暫時未完全搞清楚,我猜測是被路由規則(使用者可基于 Router 接口實作自定義路由器)處理後,可能會得到一個 null。目前僅是猜測,未做驗證。

本節的邏輯主要是從 localMethodInvokerMap 中擷取 Invoker,localMethodInvokerMap 源自 RegistryDirectory 類的成員變量 methodInvokerMap。doList 方法可以看做是對 methodInvokerMap 變量的讀操作,至于對 methodInvokerMap 變量的寫操作,這個将在後續進行分析。

RegistryDirectory 是一個動态服務目錄,它需要接受注冊中心配置進行動态調整。是以 RegistryDirectory 實作了 NotifyListener 接口,通過這個接口擷取注冊中心變更通知。下面我們來看一下具體的邏輯。

如上,notify 方法首先是根據 url 的 category 參數對 url 進行分門别類存儲,然後通過 toRouters 和 toConfigurators 将 url 清單轉成 Router 和 Configurator 清單。最後調用 refreshInvoker 方法重新整理 Invoker 清單。這裡的 toRouters 和 toConfigurators 方法邏輯不複雜,大家自行分析。接下來,我們把重點放在 refreshInvoker 方法上。

接着上一節繼續分析,refreshInvoker 方法是保證 RegistryDirectory 随注冊中心變化而變化的關鍵所在。這一塊邏輯比較多,接下來一一進行分析。

上面方法的代碼不是很多,但是邏輯卻不少。首先時根據入參 invokerUrls 的數量和協定頭判斷是否禁用所有的服務,如果禁用,則将 forbidden 設為 true,并銷毀所有的 Invoker。若不禁用,則将 url 轉成 Invoker,得到 <url, Invoker> 的映射關系。然後進一步進行轉換,得到 <methodName, Invoker 清單>。之後進行多組 Invoker 合并操作,并将合并結果指派給 methodInvokerMap。methodInvokerMap 變量在 doList 方法中會被用到,doList 會對該變量進行讀操作,在這裡是寫操作。當新的 Invoker 清單生成後,還要一個重要的工作要做,就是銷毀無用的 Invoker,避免服務消費者調用已下線的服務的服務。

接下裡,我将對上面涉及到的調用進行分析。按照順序,這裡先來分析 url 到 Invoker 的轉換過程。

toInvokers 方法一開始會對服務提供者 url 進行檢測,若服務消費端的配置不支援服務端的協定,或服務端 url 協定頭為 empty 時,toInvokers 均會忽略服務提供方 url。必要的檢測做完後,緊接着是合并 url,然後通路緩存,嘗試擷取與 url 對應的 invoker。如果緩存命中,直接将 Invoker 存入 newUrlInvokerMap 中即可。如果未命中,則需要建立 Invoker。Invoker 是通過 Protocol 的 refer 方法建立的,這個我在上一篇文章中已經分析過了,這裡就不贅述了。

toInvokers 方法傳回的是 <url, Invoker> 映射關系表,接下來還要對這個結果進行進一步處理,得到方法名到 Invoker 清單的映射關系。這個過程由 toMethodInvokers 方法完成,如下:

上面方法主要做了三件事情, 第一是對入參進行周遊,然後擷取 methods 參數,并切分成數組。随後以方法名為鍵,Invoker 清單為值,将映射關系存儲到 newMethodInvokerMap 中。第二是分别基于類和方法對 Invoker 清單進行路由操作。第三是對 Invoker 清單進行排序,并轉成不可變清單。關于 toMethodInvokers 方法就先分析到這,我們繼續向下分析,這次要分析的多組服務的合并邏輯。

上面方法首先是生成 group 到 Invoker 類比的映射關系表,若關系表中的映射關系數量大于1,表示有多組服務。此時通過叢集類合并每組 Invoker,并将合并結果存儲到 groupInvokers 中。之後将方法名與 groupInvokers 存到到 result 中,并傳回,整個邏輯結束。

接下來我們再來看一下 Invoker 清單重新整理邏輯的最後一個動作 -- 删除無用 Invoker。如下:

destroyUnusedInvokers 方法的主要邏輯是通過 newUrlInvokerMap 找出待删除 Invoker 對應的 url,并将 url 存入到 deleted 清單中。然後再周遊 deleted 清單,并從 oldUrlInvokerMap 中移除相應的 Invoker,銷毀之。整個邏輯大緻如此,不是很難了解。

到此關于 Invoker 清單的重新整理邏輯就分析了,這裡對整個過程進行簡單總結。如下:

檢測入參是否僅包含一個 url,且 url 協定頭為 empty

若第一步檢測結果為 true,表示禁用所有服務,此時銷毀所有的 Invoker

若第一步檢測結果為 false,此時将入參轉為 Invoker 清單

對将上一步邏輯删除的結果進行進一步處理,得到方法名到 Invoker 的映射關系表

合并多組 Invoker

銷毀無用 Invoker

Invoker 的重新整理邏輯還是比較複雜的,大家在看的過程中多寫點 demo 進行調試。好了,本節就到這。

本篇文章對 Dubbo 服務目錄進行了較為詳細的分析,篇幅主要集中在 RegistryDirectory 的源碼分析上。分析下來,不由得感歎,想讓本地服務目錄和注冊中心保持一緻還是需要做很多事情的,并不簡單。服務目錄是 Dubbo 叢集容錯的一部分,也是比較基礎的部分,是以大家務必搞懂。

好了,本篇文章就先到這了。感謝大家閱讀。

本文在知識共享許可協定 4.0 下釋出,轉載需在明顯位置處注明出處 作者:田小波 本文同步釋出在我的個人部落格:http://www.tianxiaobo.com
Dubbo 源碼分析 - 叢集容錯之 Directory

本作品采用知識共享署名-非商業性使用-禁止演繹 4.0 國際許可協定進行許可。