Categories
程式開發

Nacos-技術專題-配置中心實現


什麼是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 麼,配置變更是主動推送還是等定時輪詢觸發等,還有就是運維高可用方面的工作(私以為這個是配置中心的精華),例如節點跨地域部署,網絡分區時配置如何保證可寫可推送變更等。真正實現一個高質量的配置中心,還是需要長時間打磨的。

Nacos-技術專題-配置中心實現 1

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 -generated catch block
e.printStackTrace();
}

Properties 解讀

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

配置項的層級設計

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

Nacos-技術專題-配置中心實現 2

dataId 可以理解為用戶自定義的配置健,group 可以理解為配置分組名稱,這個屬於配置層級設計的概念。

簡單來說,配置中心會通過層次設計,來支持不同的分區,以此區分不同的環境、不同的分組、甚至不同的開發者,滿足在開發過程中灰度發布、測試等需求。因此怎樣設計都可以,只要有含義就好,例如下圖也不是不可以。

Nacos-技術專題-配置中心實現 3

Nacos 客戶端解析

獲取配置

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

Nacos-技術專題-配置中心實現 4

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

註冊監聽器

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

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

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

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

Nacos-技術專題-配置中心實現 5

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

配置長輪詢

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

Nacos-技術專題-配置中心實現 6

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

檢查本地配置,忽略本地快照不存在的配置項,檢查是否存在需要回調監聽器的配置項如果本地沒有配置項的,從服務端拿,返回配置內容髮生變更的鍵值列表每個鍵值再到服務端獲取最新配置,更新本地快照,補全之前缺失的配置檢查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 調用通知所有集群節點(包括自身),觸發本地文件和內存的刷新。

Nacos-技術專題-配置中心實現 7

處理長輪詢

上文提到,客戶端會有一個長輪詢任務,拉取服務端的配置變更,那麼服務端是如何處理這個長輪詢任務的呢?

源碼邏輯位於LongPollingService 類,其中有一個Runnable 任務名為ClientLongPolling,服務端會將受到的輪詢請求包裝成一個ClientLongPolling 任務,該任務持有一個AsyncContext 響應對象(Servlet 3.0 的新機制),通過定時線程池延後29.5s 執行。

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

Nacos-技術專題-配置中心實現 8

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

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

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

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

Nacos-技術專題-配置中心實現 9

全文總結

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