注冊中心作用
開篇首先想思考一個問題,沒有注冊中心 Dubbo 還能玩下去嗎?
當然可以,只要知道服務提供者地址相關信息,消費者配置之后就可以調用。如果只有幾個服務,這么玩當然沒問題。但是生產服務動輒成千上百,如果每個服務都需要手寫配置信息,想象一下是多么麻煩。
好吧,如果上面的問題都不是事的話,試想一下如果一個服務提供者在運行過程中宕機,消費者怎么辦?消費者不知情,所以它還會不斷把請求發往服務提供者,然后不斷失敗。這個時候唯一的辦法就是修改服務地址信息,然后重啟服務。
可以看到如果沒有注冊中心,分布式環境中服務查找發現將會非常麻煩,一切需要手工配置,無法完成自動化。所以這里就需要一個第三者,協調服務提供者與消費者之間關系,這就是注冊中心。
注冊中心主要作用如下:
- 動態加入,服務提供者通過注冊中心動態的把自己暴露給消費者,無需消費者逐個更新配置文件。
- 動態發現服務,消費者可以動態發現新的服務,無需重啟生效。
- 統一配置,避免本地配置導致每個服務配置不一致。
- 動態調整,注冊中心支持參數動態調整,新參數自動更新到所有相關的服務節點。
- 統一管理,依靠注冊中心數據,可以統一管理配置服務節點。
注冊中心工作流程
注冊中心工作流程總體比較簡單,流程圖大致如下:
主要工作流程可以分為如下幾步:
- 服務提供者啟動之后,會將服務注冊到注冊中心。
- 消費者啟動之后主動訂閱注冊中心上提供者服務,從而獲取到當前所有可用服務,同時留下一個回調函數。
- 若服務提供者新增或下線,注冊中心將通過第二步的注冊的回調函數通知消費者。
- dubbo-admin(服務治理中心)將會會訂閱服務提供者以及消費者,從而可以在控制台管理所有服務提供者以及消費者。
Dubbo 之前版本主要可以使用 ZooKeeper,Redis 作為注冊中心 ,而隨着 Dubbo 版本不斷更新,目前還支持 nacos,consul,etcd 等做為注冊中心。
Dubbo 注冊中心核心源碼
ps: 以下源碼基於 dubbo 2.7.3 版本
注冊中心實現使用模板模式,源碼位於 dubbo-registry 模塊,類關系如下圖:
最上層的 RegistryService
接口定義了核心方法,分別為注冊,取消注冊,訂閱,取消訂閱以及查詢。
中間層抽象類主要實現通用邏輯,如:AbstractRegistry
實現緩存機制,FailbackRegistry
實現失敗重試功能。
底層 ZookeeperRegistry
等為具體實現類,實現與 ZooKeeper 等注冊中心交互的邏輯。
接下去我們具體分析 AbstractRegistry
與 FailbackRegistry
邏輯。
AbstractRegistry
緩存實現的原理
如果每次服務調用都需要調用注冊中心實時查詢可用服務列表,不但會讓注冊中心承受巨大的流量壓力,還會產生額外的網絡請求,導致系統性能下降。
其次注冊中心需要非強依賴,其宕機不能影響正常的服務調用。
基於以上幾點,注冊中心模塊在 AbstractRegistry
類中實現通用的緩存機制。這里的緩存可以分為兩類,內存服務緩存以及磁盤文件緩存。
內存服務緩存
內存服務緩存很好理解也最容易實現,AbstractRegistry
使用一個 ConcurrentMap
保存相關信息。
private final ConcurrentMap<URL, Map<String, List<URL>>> notified = new ConcurrentHashMap<>();
這個集合中 key 為消費者的 URL,而 value 為一個 Map 集合。這個內層 Map 集合使用服務目錄作為 key,分別為 providers,routers,configurators,consumers 四類,value 則是對應服務列表集合。
磁盤文件緩存
由於服務重啟就會導致內存緩存消失,所以額外增加磁盤文件緩存。
文件緩存默認位置位於 ${user.home}/.dubbo/
文件夾,文件名為dubbo-registry-${application.name}-${register_address}.cache
。可以設置 dubbo.registry.file
配置信息從而修改默認配置,實現源碼如下:
String filename = url.getParameter(Constants.FILE_KEY, System.getProperty("user.home") + "/.dubbo/dubbo-registry-" + url.getParameter(Constants.APPLICATION_KEY) + "-" + url.getAddress() + ".cache");
ps: ${application.name} 取自
dubbo.application.name
信息,${register_address} 取值注冊中心地址信息。緩存文件完整名稱為:C:\Users\xxx/.dubbo/dubbo-registry-dubbo-auto-configure-consumer-sample-127.0.0.1:2181.cache
緩存文件內容使用 properties
配置文件格式,即 key=value
格式。key為服務接口名稱,value 為服務列表,由於服務可能存在多個,將會使用空格分隔。
緩存文件的加載
dubbo 程序初始化的時候,AbstractRegistry
構造函數將會從本地磁盤文件中將數據讀取到 Properties
對象實例中,后續都將會先寫入 Properties
,最后再將里面信息再寫入文件。
緩存初始化的源碼為下圖。
緩存文件的保存與更新
緩存文件將會通過 AbstractRegistry#notify
方法保存或更新。客戶端第一次訂閱服務獲取的全量數據,或者后續回調中獲取到新數據,都將會調用 AbstractRegistry#notify
方法,用來更新內存緩存以及文件緩存。
notify
方法源碼如下圖:
在保存文件緩存方法中,首先把根據 URL 取出的數據,拼接成字符串,然后寫入上面提到過的 properties
對象中,最后輸出到文件中。
這里可以選擇兩種保存方式,同步或異步。由於 notify
可能被多次調用,為了提高系統能,系統默認使用異步方式保存。
saveProperties
方法源碼如下:
doSaveProperties
方法最終將會將信息寫入緩存。考慮到保存方法可能會被多個線程同時調用,這里使用 CAS 方法,首先比較版本大小,若小於,代表有新線程正在寫入信息,本次更新直接丟棄。
其次考慮到多個 dubbo 應用可能共用一份緩存文件,所以這里使用文件排他鎖當做分布式鎖,防止多個應用並發操作同一份文件。
一旦文件寫入異常或者獲取鎖失敗,保存操作將會不斷重試,直到超過最大次數。
ps: dubbo 2.7.2 之前重試沒有設置最大次數,如果文件沒有權限保存,保存將會一直失敗,異步線程將會陷入死循環。
doSaveProperties
方法源碼如下:
FailbackRegistry
重試機制
FailbackRegistry
繼承 AbstractRegistry
,實現了 register
,subscribe
等通用法,並增加 doRegister
,doSubscribe
等模板方法,交由子類實現。
如果 doRegister
等模板方法發生異常,會將失敗任務放入集合,然后定時再次調用模板方法。
FailbackRegistry
失敗重試集合分別為:
以 subscribe
方法為例,這里將會調用這些 doSubscribe
的模板方法。如果發生異常將會讀取緩存文件中內容,然后加載服務。最后新建異步定時任務加入重試集合中,然后由定時器去重試這些任務。
FailbackRegistry#subscribe
方法源碼:
在 addFailedSubscribed
中將會新建定時任務,然后交由定時器執行。定時任務默認最大重試次數為 3 次,調用時間間隔默認為 5 s。
addFailedSubscribed
源碼如下:
其他失敗重試任務都比較類似,全都繼承自 AbstractRetryTask
父類,類關系如下圖。
總結
本文主要講述注冊中心作用,工作流程,通用緩存機制以及失敗重試機制。從中可以學到模板模式,以及多線程並發技巧。
這里沒有涉及到具體注冊中心實現,由於目前最主要使用 ZooKeeper 作為注冊中心,所以下篇將會聊聊 ZooKeeper 注冊中心原理,敬請期待。
幫助書籍
『深入理解 Apache Dubbo 與實戰』