圖文解析 Nacos 配置中心的實現

圖文解析 Nacos 配置中心的實現

本文不會貼太多源碼,基本靠圖片和文字敘述
全文共 2582 字,預計閱讀時間 12 分鐘

什麼是 Nacos

Nacos 是阿里發起的開源項目,地址:https://github.com/alibaba/nacos。Nacos 主要提供兩種服務,一是配置中心,支持配置註冊、變更下發、層級管理等,意義是不停機就可以動態刷新服務內部的配置項;二是作爲命名服務,提供服務的註冊和發現功能,通常用於在 RPC 框架的 Client 和 Server 中間充當媒介,還附帶有健康監測、負載均衡等功能。

本文聚焦於 Nacos 的第一塊功能,即配置中心的實現。先敘述一個配置中心通常需要哪些組成部分,再結合 Nacos 1.1.4 的源碼,探究一下這些設計是如何反映在源碼上的。

配置中心的架構

配置中心本身並不複雜,前提是你先將 CAP 的取捨問題晾在一邊的話。配置中心最基礎的功能就是存儲一個鍵值對,用戶發佈一個配置(configKey),然後客戶端獲取這個配置項(configValue);進階的功能就是當某個配置項發生變更時,將變更告知客戶端刷新舊值。

下方的架構圖,簡要描述了一個配置中心的大致架構,用戶可以通過管理平臺發佈配置,通過 HTTP 調用將配置註冊到服務端,服務端將之保存在 MySQL 等持久化存儲引擎中;用戶通過客戶端 SDK 訪問服務端的配置,同時建立 HTTP 的長輪詢監聽配置項變更,同時爲了減輕服務端壓力和保證容災特性,配置項拉取到客戶端之後會保存一份快照在本地文件中,SDK 優先讀取文件裏的內容。

這裏省略了許多細節問題,例如配置分層設計,權限校驗,客戶端長輪詢的間隔設置,服務端每次查詢都需要訪問 MySQL 麼,配置變更是主動推送還是等定時輪詢觸發等,還有就是運維高可用方面的工作(私以爲這個是配置中心的精華),例如節點跨地域部署,網絡分區時配置如何保證可寫可推送變更等。真正實現一個高質量的配置中心,還是需要長時間打磨的。

image.png

Nacos 使用示例

下文涉及的源碼均基於 Nacos 1.1.4 版本

官方代碼示例

先看一下官方文檔中對於 Nacos 的 API 使用的示例代碼,第一步是傳遞配置,新建 ConfigService 實例,第二步可以通過相應的接口獲取配置和註冊配置監聽器。使用方式非常簡單易懂,不再贅述。

try {
    // 傳遞配置
	String serverAddr = "{serverAddr}";
	String dataId = "{dataId}";
	String group = "{group}";
	Properties properties = new Properties();
	properties.put("serverAddr", serverAddr);
    
    // 新建 configService
	ConfigService configService = NacosFactory.createConfigService(properties);
	String content = configService.getConfig(dataId, group, 5000);
	System.out.println(content);
    
    // 註冊監聽器
    configService.addListener(dataId, group, new Listener() {
	@Override
	public void receiveConfigInfo(String configInfo) {
		System.out.println("recieve1:" + configInfo);
	}
	@Override
	public Executor getExecutor() {
		return null;
	}
});
} catch (NacosException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
}

Properties 解讀

serverAddr 傳遞的是配置中心服務端的地址列表,被內部名爲 ServerListManager 的類解析成地址列表進行管理,進行 HTTP 調用時會從中選擇存活的機器拼接成 URL 完成調用,一旦在調用時該地址拋異常,則客戶端會有一些處理措施,例如轉換下次選擇的節點等。值得注意的是,通常在實踐中不會採取這種硬編碼的方式,可以將其配置在 Zookeeper 或者註冊發現中心上,在啓動時動態拉取。

配置項的層級設計

Nacos 官方給出了這樣的設計圖:

image.png


dataId 可以理解爲用戶自定義的配置健,group 可以理解爲配置分組名稱,這個屬於配置層級設計的概念。簡單來說,配置中心會通過層次設計,來支持不同的分區,以此區分不同的環境、不同的分組、甚至不同的開發者,滿足在開發過程中灰度發佈、測試等需求。因此怎樣設計都可以,只要有含義就好,例如下圖也不是不可以。

image.png

Nacos 客戶端解析

獲取配置

獲取配置的主要方法是 NacosConfigService 類的 getConfigInner 方法,通常情況下該方法直接從本地文件中取得配置的值,如果本地文件不存在或者內容爲空,則再通過 HTTP GET 方法從遠端拉取配置,並保存到本地快照中。

image.png

當通過 HTTP 獲取遠端配置時,Nacos 提供了兩種熔斷策略,一是超時時間,二是最大重試次數,默認重試三次。

註冊監聽器

配置中心客戶端對某個配置項註冊監聽器是很常見的需求,達到在配置項變更的時候執行回調的功能。

iconfig.addListener(dataId, group, ml);
iconfig.getConfigAndSignListener(dataId, group, 1000, ml);

Nacos 可以通過以上方式註冊監聽器,它們內部的實現均是調用 ClientWorker 類的 addCacheDataIfAbsent。其中 CacheData 是一個維護配置項和其下注冊的所有監聽器的實例,私以爲這個名字取得並不好,不容易理解。

所有的 CacheData 都保存在 ClientWorker 類中的原子 cacheMap 中,其內部的核心成員有:

image.png

其中,content 是配置內容,MD5 值是用來檢測配置是否發生變更的關鍵,內部還維護着一個若干監聽器組成的數組,一旦發生變更則依次回調這些監聽器。

配置長輪詢

ClientWorker 通過其下的兩個線程池完成配置長輪詢的工作,一個是單線程的 executor,每隔 10ms 按照每 3000 個配置項爲一批次撈取待輪詢的 cacheData 實例,將其包裝成爲一個 LongPollingTask 提交進入第二個線程池 executorService 處理。

image.png

該長輪詢任務內部主要分爲四步:

  1. 檢查本地配置,忽略本地快照不存在的配置項,檢查是否存在需要回調監聽器的配置項
  2. 如果本地沒有配置項的,從服務端拿,返回配置內容發生變更的鍵值列表
  3. 每個鍵值再到服務端獲取最新配置,更新本地快照,補全之前缺失的配置
  4. 檢查 MD5 標籤是否一致,不一致需要回調監聽器

如果該輪詢任務拋出異常,等待一段時間再開始下一次調用,減輕服務端壓力。另外,Nacos 在 HTTP 工具類中也有限流器的代碼,通過多種手段降低輪詢或者大流量情況下的風險。下文還會講到,如果在服務端沒有發現變更的鍵值,那麼服務端會夯住這個 HTTP 請求一段時間(客戶端側默認傳遞的超時是 30s),以此進一步減輕客戶端的輪詢頻率和服務端的壓力。

Nacos 服務端解析

配置 Dump

服務端啓動時就會依賴 DumpService 的 init 方法,從數據庫中 load 配置存儲在本地磁盤上,並將一些重要的元信息例如 MD5 值緩存在內存中。服務端會根據心跳文件中保存的最後一次心跳時間,來判斷到底是從數據庫 dump 全量配置數據還是部分增量配置數據(如果機器上次心跳間隔是 6h 以內的話)。

全量 dump 當然先清空磁盤緩存,然後根據主鍵 ID 每次撈取一千條配置刷進磁盤和內存。增量 dump 就是撈取最近六小時的新增配置(包括更新的和刪除的),先按照這批數據刷新一遍內存和文件,再根據內存裏所有的數據全量去比對一遍數據庫,如果有改變的再同步一次,相比於全量 dump 的話會減少一定的數據庫 IO 和磁盤 IO 次數。

配置註冊

Nacos 服務端是一個 SpringBoot 實現的服務,註冊配置主要代碼位於 ConfigController 和 ConfigServletInner 中。服務端一般是多節點部署的集羣,因此請求一開始只會打到一臺機器,這臺機器將配置插入 MySQL 中進行持久化,這部分代碼很簡單不再贅述。

因爲服務端並不是針對每次配置查詢都去訪問 MySQL 的,而是會依賴 dump 功能在本地文件中將配置緩存起來。因此當單臺機器保存完畢配置之後,需要通知其他機器刷新內存和本地磁盤中的文件內容,因此它會發佈一個名爲 ConfigDataChangeEvent 的事件,這個事件會通過 HTTP 調用通知所有集羣節點(包括自身),觸發本地文件和內存的刷新。

image.png

處理長輪詢

上文提到,客戶端會有一個長輪詢任務,拉取服務端的配置變更,那麼服務端是如何處理這個長輪詢任務的呢?源碼邏輯位於 LongPollingService 類,其中有一個 Runnable 任務名爲 ClientLongPolling,服務端會將受到的輪詢請求包裝成一個 ClientLongPolling 任務,該任務持有一個 AsyncContext 響應對象(Servlet 3.0 的新機制),通過定時線程池延後 29.5s 執行。

爲什麼比客戶端 30s 的超時時間提前 500ms 返回是爲了最大程度上保證客戶端不會因爲網絡延時造成超時

image.png

這裏需要注意的是,在 ClientLongPolling 任務被提交進入線程池待執行的同時,服務端也通過一個隊列 allSubs 保存了所有正在被夯住的輪詢請求,這是因爲在配置項被夯住的期間內,如果用戶通過管理平臺操作了配置項變更、或者服務端該節點收到了來自其他節點的 dump 刷新通知,那麼都應立即取消夯住的任務,及時通知客戶端數據發生了變更。

爲了達到這個目的,LongPollingService 類繼承自 Event 接口,實際上本身是個事件觸發器,需要實現 onEvent 方法,其事件類型是 LocalDataChangeEvent。

當服務端在請求被夯住的期間接收到某項配置變更時,就會發佈一個 LocalDataChangeEvent 類型的事件通知(注意同上文中的 ConfigDataChangeEvent 區別),之後會將這個變更包裝成一個 DataChangeTask 異步執行,內容就是從 allSubs 中找出夯住的 ClientLongPolling 請求,寫入變更強制其立即返回。

因此完整的流程如下,如果非接收請求的節點,那麼忽略第一步持久化配置後開始:

image.png

全文總結

本文聚焦於 Nacos 作爲配置中心的源碼實現,包含了客戶端和服務端兩部分,內容基本覆蓋了配置中心功能的關鍵點,既作爲學習總結,也希望對閱讀的朋友有所幫助。

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