如果有人問你 Dubbo 中註冊中心工作原理,就把這篇文章給他
註冊中心作用
開篇首先想思考一個問題,沒有註冊中心 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: {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 與實戰』