Nacos 動態配置原理
可憐夜半虛前席,不問蒼生問鬼神。
簡介
動態配置管理是 Nacos 的三大功能之一,通過動態配置服務,我們可以在所有環境中以集中和動態的方式管理所有應用程序或服務的配置信息。
動態配置中心可以實現配置更新時無需重新部署應用程序和服務即可使相應的配置信息生效,這極大了增加了系統的運維能力。
從Nacos 2.1.1 源碼中簡單瞭解其動態配置原理。
動態配置
下面通過一個簡單的例子來了解下 Nacos 的動態配置的功能,看看 Nacos 是如何以簡單、優雅、高效的方式管理配置,實現配置的動態變更的。
環境準備
源碼獲取
首先我們要準備一個 Nacos 的服務端,這裏通過 Git 命令下載代碼資源包的方式獲取 Nacos 的服務端。
git clone https://github.com/alibaba/nacos.git
Git 命令下載Nacos服務端源碼
項目構建
把通過 Git 命令下載的源碼包導入 IDEA 中構建Nacos服務端項目,導入後 IDEA 後可以看到在項目目錄下有一個BUILDING文件,裏面有構建命令。
mvn -Prelease-nacos -Dmaven.test.skip=true clean install -U
項目構建指引
執行構建成功之後將會在控制檯看到BUILD SUCCESS 相關INFO 打印。
構建成功
然後在項目的 distribution 模塊的 target 目錄下我們就可以找到可執行程序和兩個壓縮包,這兩個壓縮包就是nacos 的 github 官網上發佈的 Release 包。
- nacos-server-2.1.1.tar.gz
- nacos-server-2.1.1.zip
以及nacos 的可執行程序,即Windows 和 Linux 下的開啓和關閉命令。
- startup.sh
- shutdown.sh
- startup.cmd
- shutdown.cmd
- 接下來我們把編譯好的兩個壓縮包拷貝出來,然後解壓出來直接使用,這樣就相當於我們在官網上下載了 Release 包了。
- 解壓後文件結構和 nacos-server-2.1.1 一樣,我們直接執行 startup.sh 即可啓動一個單機的 Nacos 服務端了。
- 前面這些環境準備的步驟,如果不需要修改nacos源碼,完全可以直接在網上下載Nacos 的Release 包,解壓後即可啓動運行nacos。
啓動服務端
當前安裝的nacos版本:Nacos 2.1.1。
啓動命令
解壓後CMD到bin 目錄下執行啓動命令來啓動一個 Nacos 服務端,Window系統直接雙擊 startup.cmd 即可。
可執行文件-Windows
啓動報錯
org.springframework.context.ApplicationContextException: Unable to start web server; nested exception is org.springframework.boot.web.server.WebServerException: Unable to start embedded Tomcat
startup.cmd 啓動報錯
修復啓動報錯
nacos 默認的啓動方式是集羣啓動,單機使用集羣啓動配置就會導致啓動報錯。
set MODE="cluster"
編輯 startup.cmd 可執行文件,修改啓動模式
set MODE="standalone"
編輯 startup.cmd 可執行文件
如果是非Windows 環境下運行就不會有這個問題,可以直接指定啓動方式。
sh startup.sh -m standalone
啓動完將會看到 INFO Nacos started successfully 相關打印。
startup.cmd 啓動成功
登錄
啓動成功後,我們就可以在瀏覽器訪問 Nacos 的控制檯了,訪問地址:http://localhost:8848/nacos/index.html。
Nacos首頁訪問
新版的nacos在首頁登錄界面加上了這個亮眼的標題:內部系統,不可暴露到公網,看代碼提交記錄是2021年2月份加的。
下載了Nacos源碼這些樣式我們也都可以自己的需求修改爲自己想要的效果。
通過查看登錄接口,訪問地址:http://localhost:8848/nacos/v1/auth/users/login。
nacos登錄接口
- nacos登錄接口的權限控制有一個默認的賬號和密碼都是 nacos,也可以改爲ldap。
- 就看application.properties 中的配置nacos.core.auth.system.type=nacos 當前登錄用戶了。
- 默認是的賬號和密碼都是:nacos/nacos。
登錄進去之後,可以看到空白配置列表和nacos默認賬戶信息。
啓動客戶端
創建 ConfigService連接
當服務端以及配置項都準備好之後,就可以創建客戶端了,如下圖所示新建一個 Nacos 的 ConfigService 來接收數據。
新建配置
接下來我們在控制檯上創建一個簡單的配置項,如下圖所示。
配置發佈後,可以在客戶端後臺看到打印如下信息:
修改配置信息
接下來我們在 Nacos 的控制檯上將我們的配置信息改爲如下圖所示:
修改完配置,點擊 “發佈” 按鈕後,客戶端將會收到最新的數據,如下圖所示:
到此爲止,一個簡單的動態配置管理功能已經走完一遍了。
動態配置源碼分析
從我們的 demo 中可以知道,我們首先是創建了一個 ConfigService。而 ConfigService 是通過 ConfigFactory 類創建的,如下圖所示:
上面是通過main 方法創建測試的客戶端,實際上同步配置初始化流程是由NacosConfigManager 管理。
在 NacosConfigAutoConfiguration 配置類中:
1 @Bean 2 public NacosConfigManager nacosConfigManager( NacosConfigProperties nacosConfigProperties) { 3 return new NacosConfigManager(nacosConfigProperties); 4 }
NacosConfigManager 持有:ConfigService(配置相關操作)、NacosConfigProperties(Spring Boot 對配置中心的配置)。
1 public class NacosConfigManager { 2 private static ConfigService service = null; 3 private NacosConfigProperties nacosConfigProperties; 4 5 public NacosConfigManager(NacosConfigProperties nacosConfigProperties) { 6 this.nacosConfigProperties = nacosConfigProperties; 7 createConfigService(nacosConfigProperties); 8 } 9 10 static ConfigService createConfigService( 11 NacosConfigProperties nacosConfigProperties) { 12 if (Objects.isNull(service)) { 13 // 雙重加鎖 防止創建了多個 NacosConfigManager 14 synchronized (NacosConfigManager.class) { 15 try { 16 if (Objects.isNull(service)) { 17 // 通過反射構造函數創建了 NacosService 的子類 18 // NacosConfigService(Properties properties) 19 service = NacosFactory.createConfigService( 20 nacosConfigProperties.assembleConfigServiceProperties()); 21 } 22 } 23 // ………… 24 } 25 } 26 return service; 27 } 28 // ………… 29 }
實例化 ConfigService
1 public NacosConfigService(Properties properties) throws NacosException { 2 ValidatorUtils.checkInitParam(properties); 3 // 初始化 命名空間,放到 properties 中 4 initNamespace(properties); 5 // 設置請求過濾器 6 this.configFilterChainManager = new ConfigFilterChainManager(properties); 7 // 設置服務器名稱列表的線程任務 8 ServerListManager serverListManager = new ServerListManager(properties); 9 serverListManager.start(); 10 // 實例化主要初始化對象1: ClientWorker(MVP選手) 11 this.worker = new ClientWorker(this.configFilterChainManager, serverListManager, properties); 12 // 實例化主要初始化對象2: ServerHttpAgent 13 // will be deleted in 2.0 later versions 14 agent = new ServerHttpAgent(serverListManager); 15 16 }
ClientWorker 構造函數
1 @SuppressWarnings("PMD.ThreadPoolCreationRule") 2 public ClientWorker(final ConfigFilterChainManager configFilterChainManager, ServerListManager serverListManager, 3 final Properties properties) throws NacosException { 4 // 設置請求過濾器 5 this.configFilterChainManager = configFilterChainManager; 6 // 初始化超時配置參數 7 init(properties); 8 // 創建 Grpc 請求類 9 agent = new ConfigRpcTransportClient(properties, serverListManager); 10 // 核心線程數 count == 1 11 int count = ThreadUtils.getSuitableThreadCount(THREAD_MULTIPLE); 12 /** 13 * 創建具有定時執行功能的單線程池,用於定時執行 checkConfigInfo 方法 14 * 即該線程任務用於同步配置 15 */ 16 ScheduledExecutorService executorService = Executors 17 .newScheduledThreadPool(Math.max(count, MIN_THREAD_NUM), r -> { 18 Thread t = new Thread(r); 19 // 設置線程名稱 20 t.setName("com.alibaba.nacos.client.Worker"); 21 // 設置爲守護線程,在主線程關閉後無需手動關閉守護線程,該線程會自動關閉 22 t.setDaemon(true); 23 return t; 24 }); 25 agent.setExecutor(executorService); 26 // 啓動線程 處於就緒狀態,主要處理 startInternal 方法 27 agent.start(); 28 29 }
ConfigRpcTransportClient
agent.start() 的 startInternal()
ConfigRpcTransportClient 的父類爲 ConfigTransportClient。
1 @Override 2 public void startInternal() { 3 executor.schedule(() -> { 4 /** 5 * 啓動線程任務,通過 while(true) 方式一直循環。 6 */ 7 while (!executor.isShutdown() && !executor.isTerminated()) { 8 try { 9 /** 10 * 獲取隊列頭部元素,如果獲取不到則等待5s,Nacos 通過這種方式來控制循環間隔 11 * Nacos 還可以通過調用 notifyListenConfig() 向 listenExecutebell 設置元素的方式,來立即執行 executeConfigListen() 方法 12 */ 13 listenExecutebell.poll(5L, TimeUnit.SECONDS); 14 if (executor.isShutdown() || executor.isTerminated()) { 15 continue; 16 } 17 executeConfigListen(); 18 } catch (Exception e) { 19 LOGGER.error("[ rpc listen execute ] [rpc listen] exception", e); 20 } 21 } 22 }, 0L, TimeUnit.MILLISECONDS); 23 24 }
到此處同步配置的初始化流程就完成了,我們繼續看同步配置的過程。
客戶端同步配置
同步配置的邏輯主要在 executeConfigListen()
方法中,這段方法比較長,需要耐心的分開來看。
1 @Override 2 public void executeConfigListen() { 3 // 有監聽組 4 Map<String, List<CacheData>> listenCachesMap = new HashMap<>(16); 5 // 無監聽組 6 Map<String, List<CacheData>> removeListenCachesMap = new HashMap<>(16); 7 // 系統當前時間 8 long now = System.currentTimeMillis(); 9 /** 10 * 判斷是否到全量同步時間 11 * 分鐘執行一次全量同步。 5 minutes to check all listen cache keys ,ALL_SYNC_INTERNAL == 5 * 60 * 1000L 12 * 當前時間 - 上次同步時間 是否大於等於 五分鐘 13 */ 14 boolean needAllSync = now - lastAllSyncTime >= ALL_SYNC_INTERNAL; 15 // 遍歷本地 CacheData Map, CacheData 保存了 Nacos 配置基本信息,配置的監聽器等基礎信息。 16 for (CacheData cache : cacheMap.get().values()) { 17 synchronized (cache) { 18 //check local listeners consistent. 19 /** 20 * 首先判斷,該 cacheData 是否需要檢查。也就是如果爲 isSyncWithServer == false,必定進行檢查。 isSyncWithServer 默認爲 false 21 * 1.添加listener.default爲false;需要檢查。 22 * 2.接收配置更改通知,設置爲false;需要檢查。 23 * 3.last listener被移除,設置爲false;需要檢查 24 */ 25 if (cache.isSyncWithServer()) { 26 /** 27 * 執行 CacheData.Md5 與 Listener.md5的比對與設定 28 * 即本地檢查 checkListenerMd5 如果不相同-配置有變化,則進行監聽器的回調。 29 * 跟蹤 LocalConfigInfoProcessor 方法可以查看Nacos 將配置信息保存在哪裏 30 * nacos 配置保存路徑:System.getProperty("JM.LOG.PATH", System.getProperty("user.home")) + File.separator + "nacos" + File.separator + "config"; 31 * C:\Users\01421603\nacos\config\fixed-localhost_8848_nacos\snapshot\DEFAULT_GROUP 32 */ 33 cache.checkListenerMd5(); 34 if (!needAllSync) { 35 // 是否需要全量同步,如果未達到全量同步時間即距上次全量同步小於五分鐘,則跳過這個 cacheData,即本次循環的nacos配置無需更換 36 continue; 37 } 38 } 39 // 本地nacos配置信息 監聽器不爲空 走這 40 if (!CollectionUtils.isEmpty(cache.getListeners())) { 41 //get listen config ,是否啓用本地監聽配置 isUseLocalConfig 默認 == false 42 if (!cache.isUseLocalConfigInfo()) { 43 // 有監聽器的放入 listenCachesMap 44 List<CacheData> cacheDatas = listenCachesMap.get(String.valueOf(cache.getTaskId())); 45 if (cacheDatas == null) { 46 cacheDatas = new LinkedList<>(); 47 listenCachesMap.put(String.valueOf(cache.getTaskId()), cacheDatas); 48 } 49 cacheDatas.add(cache); 50 51 } 52 // 本地nacos配置信息 監聽器爲空 走這 53 } else if (CollectionUtils.isEmpty(cache.getListeners())) { 54 if (!cache.isUseLocalConfigInfo()) { 55 // 沒有監聽器的放入 removeListenCachesMap 56 List<CacheData> cacheDatas = removeListenCachesMap.get(String.valueOf(cache.getTaskId())); 57 if (cacheDatas == null) { 58 cacheDatas = new LinkedList<>(); 59 removeListenCachesMap.put(String.valueOf(cache.getTaskId()), cacheDatas); 60 } 61 cacheDatas.add(cache); 62 63 } 64 } 65 } 66 67 } 68 // 標誌是否有更改的配置,默認爲 false 69 boolean hasChangedKeys = false; 70 // 有監聽組配置信息 非空 71 if (!listenCachesMap.isEmpty()) { 72 for (Map.Entry<String, List<CacheData>> entry : listenCachesMap.entrySet()) { 73 String taskId = entry.getKey(); 74 Map<String, Long> timestampMap = new HashMap<>(listenCachesMap.size() * 2); 75 76 List<CacheData> listenCaches = entry.getValue(); 77 for (CacheData cacheData : listenCaches) { 78 timestampMap.put(GroupKey.getKeyTenant(cacheData.dataId, cacheData.group, cacheData.tenant), 79 cacheData.getLastModifiedTs().longValue()); 80 } 81 // 構建監聽器請求 82 ConfigBatchListenRequest configChangeListenRequest = buildConfigRequest(listenCaches); 83 configChangeListenRequest.setListen(true); 84 try { 85 // 初始化 RpcClient 客戶端 86 RpcClient rpcClient = ensureRpcClient(taskId); 87 /** 88 * 發送請求向 Nacos Server 添加配置變化監聽器 89 * ConfigChangeBatchListenResponse 服務端將返回有變化的 dataId、group、tenant 90 */ 91 ConfigChangeBatchListenResponse configChangeBatchListenResponse = (ConfigChangeBatchListenResponse) requestProxy( 92 rpcClient, configChangeListenRequest); 93 if (configChangeBatchListenResponse != null && configChangeBatchListenResponse.isSuccess()) { 94 95 Set<String> changeKeys = new HashSet<>(); 96 //handle changed keys,notify listener 97 // 處理有變化的配置 98 if (!CollectionUtils.isEmpty(configChangeBatchListenResponse.getChangedConfigs())) { 99 hasChangedKeys = true; 100 for (ConfigChangeBatchListenResponse.ConfigContext changeConfig : configChangeBatchListenResponse 101 .getChangedConfigs()) { 102 String changeKey = GroupKey 103 .getKeyTenant(changeConfig.getDataId(), changeConfig.getGroup(), 104 changeConfig.getTenant()); 105 changeKeys.add(changeKey); 106 boolean isInitializing = cacheMap.get().get(changeKey).isInitializing(); 107 /** 108 * 刷新上下文 109 * 此處將請求 Nacos Server ,獲取最新配置內容,並觸發 Listener 的回調。 110 */ 111 refreshContentAndCheck(changeKey, !isInitializing); 112 } 113 114 } 115 116 //handler content configs 117 for (CacheData cacheData : listenCaches) { 118 String groupKey = GroupKey 119 .getKeyTenant(cacheData.dataId, cacheData.group, cacheData.getTenant()); 120 // 如果返回的 changeKeys 中,未包含此 groupKey。則說明此內容未發生變化。 121 if (!changeKeys.contains(groupKey)) { 122 //sync:cache data md5 = server md5 && cache data md5 = all listeners md5. 123 synchronized (cacheData) { 124 if (!cacheData.getListeners().isEmpty()) { 125 126 Long previousTimesStamp = timestampMap.get(groupKey); 127 if (previousTimesStamp != null && !cacheData.getLastModifiedTs().compareAndSet(previousTimesStamp, 128 System.currentTimeMillis())) { 129 continue; 130 } 131 // 則將同步標誌設爲 true 132 cacheData.setSyncWithServer(true); 133 } 134 } 135 } 136 // 將初始化狀態設置 false 137 cacheData.setInitializing(false); 138 } 139 140 } 141 } catch (Exception e) { 142 143 LOGGER.error("Async listen config change error ", e); 144 try { 145 Thread.sleep(50L); 146 } catch (InterruptedException interruptedException) { 147 //ignore 148 } 149 } 150 } 151 } 152 153 /** 154 * 處理無監聽器的 CacheData 155 * 無監聽器的 CacheData 就是,從 Nacos Client 與 Nacos Server 中移除掉原有的監聽器。 156 */ 157 if (!removeListenCachesMap.isEmpty()) { 158 for (Map.Entry<String, List<CacheData>> entry : removeListenCachesMap.entrySet()) { 159 String taskId = entry.getKey(); 160 List<CacheData> removeListenCaches = entry.getValue(); 161 ConfigBatchListenRequest configChangeListenRequest = buildConfigRequest(removeListenCaches); 162 configChangeListenRequest.setListen(false); 163 try { 164 RpcClient rpcClient = ensureRpcClient(taskId); 165 boolean removeSuccess = unListenConfigChange(rpcClient, configChangeListenRequest); 166 if (removeSuccess) { 167 for (CacheData cacheData : removeListenCaches) { 168 synchronized (cacheData) { 169 if (cacheData.getListeners().isEmpty()) { 170 ClientWorker.this 171 .removeCache(cacheData.dataId, cacheData.group, cacheData.tenant); 172 } 173 } 174 } 175 } 176 177 } catch (Exception e) { 178 LOGGER.error("async remove listen config change error ", e); 179 } 180 try { 181 Thread.sleep(50L); 182 } catch (InterruptedException interruptedException) { 183 //ignore 184 } 185 } 186 } 187 188 if (needAllSync) { 189 lastAllSyncTime = now; 190 } 191 //If has changed keys,notify re sync md5. 192 // 如果有改變的配置,則立即進行一次同步配置過程。 193 if (hasChangedKeys) { 194 notifyListenConfig(); 195 } 196 }
客戶端接收服務端推送
當 Nacos Config 配置發生變更時,Nacos Server 會主動通知 Nacos Client。
Nacos Client 在向 Nacos Server 發送請求前,會初始化 Nacos Rpc Client,執行的方法是
ConfigRpcTransportClient # ensureRpcClient(String taskId)
1 /** 2 * 客戶端接收服務端推送 3 * 當 Nacos Config 配置發生變更時,Nacos Server 會主動通知 Nacos Client。 4 * Nacos Client 在向 Nacos Server 發送請求前,會初始化 Nacos Rpc Client,執行 ConfigRpcTransportClient下的 ensureRpcClient(String taskId) 方法 5 */ 6 private RpcClient ensureRpcClient(String taskId) throws NacosException { 7 synchronized (ClientWorker.this) { 8 9 Map<String, String> labels = getLabels(); 10 Map<String, String> newLabels = new HashMap<>(labels); 11 newLabels.put("taskId", taskId); 12 13 RpcClient rpcClient = RpcClientFactory 14 .createClient(uuid + "_config-" + taskId, getConnectionType(), newLabels); 15 if (rpcClient.isWaitInitiated()) { 16 // 初始化處理器,在 initRpcClientHandler 方法中對 ConfigChangeNotifyRequest 的處理邏輯。 17 initRpcClientHandler(rpcClient); 18 rpcClient.setTenant(getTenant()); 19 rpcClient.clientAbilities(initAbilities()); 20 rpcClient.start(); 21 } 22 23 return rpcClient; 24 } 25 26 }
初始化 ConfigChangeNotifyRequest 處理邏輯
1 /** 2 * 初始化 ConfigChangeNotifyRequest 處理邏輯 3 */ 4 private void initRpcClientHandler(final RpcClient rpcClientInner) { 5 /* 6 * Register Config Change /Config ReSync Handler 7 */ 8 rpcClientInner.registerServerRequestHandler((request) -> { 9 if (request instanceof ConfigChangeNotifyRequest) { 10 ConfigChangeNotifyRequest configChangeNotifyRequest = (ConfigChangeNotifyRequest) request; 11 LOGGER.info("[{}] [server-push] config changed. dataId={}, group={},tenant={}", 12 rpcClientInner.getName(), configChangeNotifyRequest.getDataId(), 13 configChangeNotifyRequest.getGroup(), configChangeNotifyRequest.getTenant()); 14 String groupKey = GroupKey 15 .getKeyTenant(configChangeNotifyRequest.getDataId(), configChangeNotifyRequest.getGroup(), 16 configChangeNotifyRequest.getTenant()); 17 // 獲取 CacheData 18 CacheData cacheData = cacheMap.get().get(groupKey); 19 if (cacheData != null) { 20 synchronized (cacheData) { 21 // 設置服務器同步標誌 22 cacheData.getLastModifiedTs().set(System.currentTimeMillis()); 23 cacheData.setSyncWithServer(false); 24 // 立即觸發該 CacheData 的同步配置操作 25 notifyListenConfig(); 26 } 27 28 } 29 return new ConfigChangeNotifyResponse(); 30 } 31 return null; 32 }); 33 34 rpcClientInner.registerServerRequestHandler((request) -> { 35 if (request instanceof ClientConfigMetricRequest) { 36 ClientConfigMetricResponse response = new ClientConfigMetricResponse(); 37 response.setMetrics(getMetrics(((ClientConfigMetricRequest) request).getMetricsKeys())); 38 return response; 39 } 40 return null; 41 }); 42 43 rpcClientInner.registerConnectionListener(new ConnectionEventListener() { 44 45 @Override 46 public void onConnected() { 47 LOGGER.info("[{}] Connected,notify listen context...", rpcClientInner.getName()); 48 notifyListenConfig(); 49 } 50 51 @Override 52 public void onDisConnect() { 53 String taskId = rpcClientInner.getLabels().get("taskId"); 54 LOGGER.info("[{}] DisConnected,clear listen context...", rpcClientInner.getName()); 55 Collection<CacheData> values = cacheMap.get().values(); 56 57 for (CacheData cacheData : values) { 58 if (StringUtils.isNotBlank(taskId)) { 59 if (Integer.valueOf(taskId).equals(cacheData.getTaskId())) { 60 cacheData.setSyncWithServer(false); 61 } 62 } else { 63 cacheData.setSyncWithServer(false); 64 } 65 } 66 } 67 68 }); 69 70 rpcClientInner.serverListFactory(new ServerListFactory() { 71 @Override 72 public String genNextServer() { 73 return ConfigRpcTransportClient.super.serverListManager.getNextServerAddr(); 74 75 } 76 77 @Override 78 public String getCurrentServer() { 79 return ConfigRpcTransportClient.super.serverListManager.getCurrentServerAddr(); 80 81 } 82 83 @Override 84 public List<String> getServerList() { 85 return ConfigRpcTransportClient.super.serverListManager.getServerUrls(); 86 87 } 88 }); 89 90 NotifyCenter.registerSubscriber(new Subscriber<ServerlistChangeEvent>() { 91 @Override 92 public void onEvent(ServerlistChangeEvent event) { 93 rpcClientInner.onServerListChange(); 94 } 95 96 @Override 97 public Class<? extends Event> subscribeType() { 98 return ServerlistChangeEvent.class; 99 } 100 }); 101 } 102
服務端變更通知
入口
配置變更是在 Nacos Service 的 Web 頁面進行操作的,調用POST /v1/cs/configs
接口。
該接口主要邏輯:
- 更新配置內容
- 發送配置變更事件
1 /** 2 * 服務端變更通知 3 * 入口:配置變更,是在 Nacos Service 的 Web 頁面進行操作的,調用POST /v1/cs/configs接口,即 publishConfig。 4 * Adds or updates non-aggregated data. 5 * <p> 6 * request and response will be used in aspect, see 7 * {@link com.alibaba.nacos.config.server.aspect.CapacityManagementAspect} and 8 * {@link com.alibaba.nacos.config.server.aspect.RequestLogAspect}. 9 * </p> 10 * @throws NacosException NacosException. 11 */ 12 @PostMapping 13 @Secured(action = ActionTypes.WRITE, signType = SignType.CONFIG) 14 public Boolean publishConfig(HttpServletRequest request, HttpServletResponse response, 15 @RequestParam(value = "dataId") String dataId, 16 @RequestParam(value = "group") String group, 17 @RequestParam(value = "tenant", required = false, defaultValue = StringUtils.EMPTY) String tenant, 18 @RequestParam(value = "content") String content, @RequestParam(value = "tag", required = false) String tag, 19 @RequestParam(value = "appName", required = false) String appName, 20 @RequestParam(value = "src_user", required = false) String srcUser, 21 @RequestParam(value = "config_tags", required = false) String configTags, 22 @RequestParam(value = "desc", required = false) String desc, 23 @RequestParam(value = "use", required = false) String use, 24 @RequestParam(value = "effect", required = false) String effect, 25 @RequestParam(value = "type", required = false) String type, 26 @RequestParam(value = "schema", required = false) String schema) throws NacosException { 27 28 final String srcIp = RequestUtil.getRemoteIp(request); 29 final String requestIpApp = RequestUtil.getAppName(request); 30 if (StringUtils.isBlank(srcUser)) { 31 srcUser = RequestUtil.getSrcUserName(request); 32 } 33 //check type 34 if (!ConfigType.isValidType(type)) { 35 type = ConfigType.getDefaultType().getType(); 36 } 37 38 // encrypted 39 Pair<String, String> pair = EncryptionHandler.encryptHandler(dataId, content); 40 content = pair.getSecond(); 41 42 // check tenant 43 ParamUtils.checkTenant(tenant); 44 ParamUtils.checkParam(dataId, group, "datumId", content); 45 ParamUtils.checkParam(tag); 46 Map<String, Object> configAdvanceInfo = new HashMap<>(10); 47 MapUtil.putIfValNoNull(configAdvanceInfo, "config_tags", configTags); 48 MapUtil.putIfValNoNull(configAdvanceInfo, "desc", desc); 49 MapUtil.putIfValNoNull(configAdvanceInfo, "use", use); 50 MapUtil.putIfValNoNull(configAdvanceInfo, "effect", effect); 51 MapUtil.putIfValNoNull(configAdvanceInfo, "type", type); 52 MapUtil.putIfValNoNull(configAdvanceInfo, "schema", schema); 53 ParamUtils.checkParam(configAdvanceInfo); 54 55 if (AggrWhitelist.isAggrDataId(dataId)) { 56 LOGGER.warn("[aggr-conflict] {} attempt to publish single data, {}, {}", RequestUtil.getRemoteIp(request), 57 dataId, group); 58 throw new NacosException(NacosException.NO_RIGHT, "dataId:" + dataId + " is aggr"); 59 } 60 61 final Timestamp time = TimeUtils.getCurrentTime(); 62 String betaIps = request.getHeader("betaIps"); 63 ConfigInfo configInfo = new ConfigInfo(dataId, group, tenant, appName, content); 64 configInfo.setType(type); 65 String encryptedDataKey = pair.getFirst(); 66 configInfo.setEncryptedDataKey(encryptedDataKey); 67 if (StringUtils.isBlank(betaIps)) { 68 if (StringUtils.isBlank(tag)) { 69 persistService.insertOrUpdate(srcIp, srcUser, configInfo, time, configAdvanceInfo, false); 70 ConfigChangePublisher.notifyConfigChange( 71 new ConfigDataChangeEvent(false, dataId, group, tenant, time.getTime())); 72 } else { 73 // 更新配置內容 74 persistService.insertOrUpdateTag(configInfo, tag, srcIp, srcUser, time, false); 75 // 發送配置變更事件 76 ConfigChangePublisher.notifyConfigChange( 77 new ConfigDataChangeEvent(false, dataId, group, tenant, tag, time.getTime())); 78 } 79 } else { 80 // beta publish 81 configInfo.setEncryptedDataKey(encryptedDataKey); 82 persistService.insertOrUpdateBeta(configInfo, betaIps, srcIp, srcUser, time, false); 83 ConfigChangePublisher.notifyConfigChange( 84 new ConfigDataChangeEvent(true, dataId, group, tenant, time.getTime())); 85 } 86 ConfigTraceService.logPersistenceEvent(dataId, group, tenant, requestIpApp, time.getTime(), 87 InetUtils.getSelfIP(), ConfigTraceService.PERSISTENCE_EVENT_PUB, content); 88 return true; 89 }
ConfigDataChangeEvent 監聽器
AsyncNotifyService 在初始化時,向事件通知中心添加了監聽器。
1 /** 2 * AsyncNotifyService 在初始化時,向事件通知中心添加了監聽器 3 */ 4 @Autowired 5 public AsyncNotifyService(ServerMemberManager memberManager) { 6 this.memberManager = memberManager; 7 8 // Register ConfigDataChangeEvent to NotifyCenter. 9 NotifyCenter.registerToPublisher(ConfigDataChangeEvent.class, NotifyCenter.ringBufferSize); 10 11 // Register A Subscriber to subscribe ConfigDataChangeEvent. 12 NotifyCenter.registerSubscriber(new Subscriber() { 13 14 @Override 15 public void onEvent(Event event) { 16 // Generate ConfigDataChangeEvent concurrently 17 if (event instanceof ConfigDataChangeEvent) { 18 // ConfigDataChangeEvent 監聽器 19 ConfigDataChangeEvent evt = (ConfigDataChangeEvent) event; 20 long dumpTs = evt.lastModifiedTs; 21 String dataId = evt.dataId; 22 String group = evt.group; 23 String tenant = evt.tenant; 24 String tag = evt.tag; 25 Collection<Member> ipList = memberManager.allMembers(); 26 27 // In fact, any type of queue here can be 28 Queue<NotifySingleTask> httpQueue = new LinkedList<>(); 29 Queue<NotifySingleRpcTask> rpcQueue = new LinkedList<>(); 30 // 把參數包裝爲 NotifySingleRpcTask 添加到 rpcQueue 31 for (Member member : ipList) { 32 if (!MemberUtil.isSupportedLongCon(member)) { 33 httpQueue.add(new NotifySingleTask(dataId, group, tenant, tag, dumpTs, member.getAddress(), 34 evt.isBeta)); 35 } else { 36 rpcQueue.add( 37 new NotifySingleRpcTask(dataId, group, tenant, tag, dumpTs, evt.isBeta, member)); 38 } 39 } 40 if (!httpQueue.isEmpty()) { 41 ConfigExecutor.executeAsyncNotify(new AsyncTask(nacosAsyncRestTemplate, httpQueue)); 42 } 43 // 若rpcQueue 不爲空,則把 rpcQueue 包裝爲 AsyncRpcTask 44 if (!rpcQueue.isEmpty()) { 45 ConfigExecutor.executeAsyncNotify(new AsyncRpcTask(rpcQueue)); 46 } 47 48 } 49 } 50 51 @Override 52 public Class<? extends Event> subscribeType() { 53 return ConfigDataChangeEvent.class; 54 } 55 }); 56 }
AsyncRpcTask異步任務
AsyncRpcTask #run()
1 // AsyncRpcTask 異步任務 2 class AsyncRpcTask implements Runnable { 3 4 private Queue<NotifySingleRpcTask> queue; 5 6 public AsyncRpcTask(Queue<NotifySingleRpcTask> queue) { 7 this.queue = queue; 8 } 9 10 @Override 11 public void run() { 12 while (!queue.isEmpty()) { 13 NotifySingleRpcTask task = queue.poll(); 14 15 ConfigChangeClusterSyncRequest syncRequest = new ConfigChangeClusterSyncRequest(); 16 // 組裝 syncRequest 參數 17 syncRequest.setDataId(task.getDataId()); 18 syncRequest.setGroup(task.getGroup()); 19 syncRequest.setBeta(task.isBeta); 20 syncRequest.setLastModified(task.getLastModified()); 21 syncRequest.setTag(task.tag); 22 syncRequest.setTenant(task.getTenant()); 23 Member member = task.member; 24 if (memberManager.getSelf().equals(member)) { 25 if (syncRequest.isBeta()) { 26 // 提交異步任務 dump 27 dumpService.dump(syncRequest.getDataId(), syncRequest.getGroup(), syncRequest.getTenant(), 28 syncRequest.getLastModified(), NetUtils.localIP(), true); 29 } else { 30 dumpService.dump(syncRequest.getDataId(), syncRequest.getGroup(), syncRequest.getTenant(), 31 syncRequest.getTag(), syncRequest.getLastModified(), NetUtils.localIP()); 32 } 33 continue; 34 } 35 // nacos 集羣通知 36 if (memberManager.hasMember(member.getAddress())) { 37 // start the health check and there are ips that are not monitored, put them directly in the notification queue, otherwise notify 38 boolean unHealthNeedDelay = memberManager.isUnHealth(member.getAddress()); 39 if (unHealthNeedDelay) { 40 // target ip is unhealthy, then put it in the notification list 41 ConfigTraceService.logNotifyEvent(task.getDataId(), task.getGroup(), task.getTenant(), null, 42 task.getLastModified(), InetUtils.getSelfIP(), ConfigTraceService.NOTIFY_EVENT_UNHEALTH, 43 0, member.getAddress()); 44 // get delay time and set fail count to the task 45 asyncTaskExecute(task); 46 } else { 47 48 if (!MemberUtil.isSupportedLongCon(member)) { 49 asyncTaskExecute( 50 new NotifySingleTask(task.getDataId(), task.getGroup(), task.getTenant(), task.tag, 51 task.getLastModified(), member.getAddress(), task.isBeta)); 52 } else { 53 try { 54 configClusterRpcClientProxy 55 .syncConfigChange(member, syncRequest, new AsyncRpcNotifyCallBack(task)); 56 } catch (Exception e) { 57 MetricsMonitor.getConfigNotifyException().increment(); 58 asyncTaskExecute(task); 59 } 60 } 61 62 } 63 } else { 64 //No nothig if member has offline. 65 } 66 67 } 68 } 69 }
接下來繼續看 dumpService.dump()
1 /** 2 * Add DumpTask to TaskManager, it will execute asynchronously. 3 * DumpTask 異步任務 4 * 該異步任務由 TaskManager 執行,其在EmbeddedDumpService初始化時,被創建。 5 * 實際由TaskManager 的父類 NacosDelayTaskExecuteEngine 執行 processTasks() 方法 6 */ 7 public void dump(String dataId, String group, String tenant, long lastModified, String handleIp, boolean isBeta) { 8 String groupKey = GroupKey2.getKey(dataId, group, tenant); 9 String taskKey = String.join("+", dataId, group, tenant, String.valueOf(isBeta)); 10 dumpTaskMgr.addTask(taskKey, new DumpTask(groupKey, lastModified, handleIp, isBeta)); 11 DUMP_LOG.info("[dump-task] add task. groupKey={}, taskKey={}", groupKey, taskKey); 12 }
DumpTask 異步任務
該異步任務由 TaskManager
執行,其在EmbeddedDumpService
初始化時,被創建。
實際由 TaskManager
的父類 NacosDelayTaskExecuteEngine
執行 processTasks()
方法。
1 /** 2 * process tasks in execute engine. 3 */ 4 protected void processTasks() { 5 Collection<Object> keys = getAllTaskKeys(); 6 for (Object taskKey : keys) { 7 AbstractDelayTask task = removeTask(taskKey); 8 if (null == task) { 9 continue; 10 } 11 // 根據 taskKey 取到對應的 NacosTaskProcessor 執行 process() 方法 12 NacosTaskProcessor processor = getProcessor(taskKey); 13 if (null == processor) { 14 getEngineLog().error("processor not found for task, so discarded. " + task); 15 continue; 16 } 17 try { 18 // ReAdd task if process failed 19 if (!processor.process(task)) { 20 retryFailedTask(taskKey, task); 21 } 22 } catch (Throwable e) { 23 getEngineLog().error("Nacos task execute error ", e); 24 retryFailedTask(taskKey, task); 25 } 26 } 27 }
實際上就是根據 taskKey
取到對應的NacosTaskProcessor
執行process()
方法。
此處 DumpTask
對應的是 DumpProcessor。
1 @Override 2 public boolean process(NacosTask task) { 3 final PersistService persistService = dumpService.getPersistService(); 4 DumpTask dumpTask = (DumpTask) task; 5 // dumpTask 參數賦值 6 String[] pair = GroupKey2.parseKey(dumpTask.getGroupKey()); 7 String dataId = pair[0]; 8 String group = pair[1]; 9 String tenant = pair[2]; 10 long lastModified = dumpTask.getLastModified(); 11 String handleIp = dumpTask.getHandleIp(); 12 boolean isBeta = dumpTask.isBeta(); 13 String tag = dumpTask.getTag(); 14 // 構建 ConfigDumpEvent 事件 15 ConfigDumpEvent.ConfigDumpEventBuilder build = ConfigDumpEvent.builder().namespaceId(tenant).dataId(dataId) 16 .group(group).isBeta(isBeta).tag(tag).lastModifiedTs(lastModified).handleIp(handleIp); 17 18 if (isBeta) { 19 // if publish beta, then dump config, update beta cache 20 ConfigInfo4Beta cf = persistService.findConfigInfo4Beta(dataId, group, tenant); 21 // build 參數賦值 22 build.remove(Objects.isNull(cf)); 23 build.betaIps(Objects.isNull(cf) ? null : cf.getBetaIps()); 24 build.content(Objects.isNull(cf) ? null : cf.getContent()); 25 build.encryptedDataKey(Objects.isNull(cf) ? null : cf.getEncryptedDataKey()); 26 27 return DumpConfigHandler.configDump(build.build()); 28 } 29 if (StringUtils.isBlank(tag)) { 30 ConfigInfo cf = persistService.findConfigInfo(dataId, group, tenant); 31 32 build.remove(Objects.isNull(cf)); 33 build.content(Objects.isNull(cf) ? null : cf.getContent()); 34 build.type(Objects.isNull(cf) ? null : cf.getType()); 35 build.encryptedDataKey(Objects.isNull(cf) ? null : cf.getEncryptedDataKey()); 36 } else { 37 ConfigInfo4Tag cf = persistService.findConfigInfo4Tag(dataId, group, tenant, tag); 38 39 build.remove(Objects.isNull(cf)); 40 build.content(Objects.isNull(cf) ? null : cf.getContent()); 41 42 } 43 return DumpConfigHandler.configDump(build.build()); 44 }
繼續進入DumpConfigHandler.configDump(build.build())。
1 /** 2 * trigger config dump event. 3 * 4 * @param event {@link ConfigDumpEvent} 5 * @return {@code true} if the config dump task success , else {@code false} 6 */ 7 public static boolean configDump(ConfigDumpEvent event) { 8 final String dataId = event.getDataId(); 9 final String group = event.getGroup(); 10 final String namespaceId = event.getNamespaceId(); 11 final String content = event.getContent(); 12 final String type = event.getType(); 13 final long lastModified = event.getLastModifiedTs(); 14 final String encryptedDataKey = event.getEncryptedDataKey(); 15 if (event.isBeta()) { 16 boolean result; 17 if (event.isRemove()) { 18 result = ConfigCacheService.removeBeta(dataId, group, namespaceId); 19 if (result) { 20 ConfigTraceService.logDumpEvent(dataId, group, namespaceId, null, lastModified, event.getHandleIp(), 21 ConfigTraceService.DUMP_EVENT_REMOVE_OK, System.currentTimeMillis() - lastModified, 0); 22 } 23 return result; 24 } else { 25 result = ConfigCacheService 26 .dumpBeta(dataId, group, namespaceId, content, lastModified, event.getBetaIps(), 27 encryptedDataKey); 28 if (result) { 29 ConfigTraceService.logDumpEvent(dataId, group, namespaceId, null, lastModified, event.getHandleIp(), 30 ConfigTraceService.DUMP_EVENT_OK, System.currentTimeMillis() - lastModified, 31 content.length()); 32 } 33 } 34 35 return result; 36 } 37 if (StringUtils.isBlank(event.getTag())) { 38 if (dataId.equals(AggrWhitelist.AGGRIDS_METADATA)) { 39 AggrWhitelist.load(content); 40 } 41 42 if (dataId.equals(ClientIpWhiteList.CLIENT_IP_WHITELIST_METADATA)) { 43 ClientIpWhiteList.load(content); 44 } 45 46 if (dataId.equals(SwitchService.SWITCH_META_DATAID)) { 47 SwitchService.load(content); 48 } 49 50 boolean result; 51 if (!event.isRemove()) { 52 result = ConfigCacheService 53 .dump(dataId, group, namespaceId, content, lastModified, type, encryptedDataKey); 54 55 if (result) { 56 ConfigTraceService.logDumpEvent(dataId, group, namespaceId, null, lastModified, event.getHandleIp(), 57 ConfigTraceService.DUMP_EVENT_OK, System.currentTimeMillis() - lastModified, 58 content.length()); 59 } 60 } else { 61 result = ConfigCacheService.remove(dataId, group, namespaceId); 62 63 if (result) { 64 ConfigTraceService.logDumpEvent(dataId, group, namespaceId, null, lastModified, event.getHandleIp(), 65 ConfigTraceService.DUMP_EVENT_REMOVE_OK, System.currentTimeMillis() - lastModified, 0); 66 } 67 } 68 return result; 69 } else { 70 // 71 boolean result; 72 if (!event.isRemove()) { 73 // 保存配置文件並更新緩存中的 md5 值 74 result = ConfigCacheService 75 .dumpTag(dataId, group, namespaceId, event.getTag(), content, lastModified, encryptedDataKey); 76 if (result) { 77 ConfigTraceService.logDumpEvent(dataId, group, namespaceId, null, lastModified, event.getHandleIp(), 78 ConfigTraceService.DUMP_EVENT_OK, System.currentTimeMillis() - lastModified, 79 content.length()); 80 } 81 } else { 82 result = ConfigCacheService.removeTag(dataId, group, namespaceId, event.getTag()); 83 if (result) { 84 ConfigTraceService.logDumpEvent(dataId, group, namespaceId, null, lastModified, event.getHandleIp(), 85 ConfigTraceService.DUMP_EVENT_REMOVE_OK, System.currentTimeMillis() - lastModified, 0); 86 } 87 } 88 return result; 89 } 90 91 }
繼續進入ConfigCacheService.dump()。
1 /** 2 * Update md5 value. 3 * 4 * @param groupKey groupKey string value. 5 * @param md5 md5 string value. 6 * @param lastModifiedTs lastModifiedTs long value. 7 */ 8 public static void updateMd5(String groupKey, String md5, long lastModifiedTs, String encryptedDataKey) { 9 CacheItem cache = makeSure(groupKey, encryptedDataKey, false); 10 if (cache.md5 == null || !cache.md5.equals(md5)) { 11 cache.md5 = md5; 12 cache.lastModifiedTs = lastModifiedTs; 13 // 發佈 LocalDataChangeEvent 事件 14 NotifyCenter.publishEvent(new LocalDataChangeEvent(groupKey)); 15 } 16 }
LocalDataChangeEvent 監聽器
RpcConfigChangeNotifier 是 LocalDataChangeEvent 的監聽器。
1 /** 2 * adaptor to config module ,when server side config change ,invoke this method. 3 * 4 * @param groupKey groupKey 5 */ 6 public void configDataChanged(String groupKey, String dataId, String group, String tenant, boolean isBeta, 7 List<String> betaIps, String tag) { 8 // 獲取變更配置對應的客戶端 9 Set<String> listeners = configChangeListenContext.getListeners(groupKey); 10 if (CollectionUtils.isEmpty(listeners)) { 11 return; 12 } 13 int notifyClientCount = 0; 14 for (final String client : listeners) { 15 // 根據客戶端獲取連接 16 Connection connection = connectionManager.getConnection(client); 17 if (connection == null) { 18 continue; 19 } 20 21 ConnectionMeta metaInfo = connection.getMetaInfo(); 22 //beta ips check. 23 String clientIp = metaInfo.getClientIp(); 24 String clientTag = metaInfo.getTag(); 25 if (isBeta && betaIps != null && !betaIps.contains(clientIp)) { 26 continue; 27 } 28 //tag check 29 if (StringUtils.isNotBlank(tag) && !tag.equals(clientTag)) { 30 continue; 31 } 32 // 構造請求 33 ConfigChangeNotifyRequest notifyRequest = ConfigChangeNotifyRequest.build(dataId, group, tenant); 34 // 構造任務 35 RpcPushTask rpcPushRetryTask = new RpcPushTask(notifyRequest, 50, client, clientIp, metaInfo.getAppName()); 36 // 發送請求 37 push(rpcPushRetryTask); 38 notifyClientCount++; 39 } 40 Loggers.REMOTE_PUSH.info("push [{}] clients ,groupKey=[{}]", notifyClientCount, groupKey); 41 } 42 43 @Override 44 public void onEvent(LocalDataChangeEvent event) { 45 String groupKey = event.groupKey; 46 boolean isBeta = event.isBeta; 47 List<String> betaIps = event.betaIps; 48 String[] strings = GroupKey.parseKey(groupKey); 49 String dataId = strings[0]; 50 String group = strings[1]; 51 String tenant = strings.length > 2 ? strings[2] : ""; 52 String tag = event.tag; 53 54 configDataChanged(groupKey, dataId, group, tenant, isBeta, betaIps, tag); 55 56 }
發送請求
發送請求的邏輯在RpcPushTask # run()
中。
1 /** 2 * 發送請求的邏輯在RpcPushTask # run() 中 3 */ 4 @Override 5 public void run() { 6 tryTimes++; 7 if (!tpsMonitorManager.applyTpsForClientIp(POINT_CONFIG_PUSH, connectionId, clientIp)) { 8 // 如果 tps 受限,自旋等待 tps 控制放開。 9 push(this); 10 } else { 11 // 發送請求 12 rpcPushService.pushWithCallback(connectionId, notifyRequest, new AbstractPushCallBack(3000L) { 13 @Override 14 public void onSuccess() { 15 tpsMonitorManager.applyTpsForClientIp(POINT_CONFIG_PUSH_SUCCESS, connectionId, clientIp); 16 } 17 18 @Override 19 public void onFail(Throwable e) { 20 tpsMonitorManager.applyTpsForClientIp(POINT_CONFIG_PUSH_FAIL, connectionId, clientIp); 21 Loggers.REMOTE_PUSH.warn("Push fail", e); 22 push(RpcPushTask.this); 23 } 24 25 }, ConfigExecutor.getClientConfigNotifierServiceExecutor()); 26 27 } 28 29 }
總結
Nacos 2.x 中拋棄了之前版本的 長輪詢 模式,採用 長連接 模式。
- 在長輪詢的任務中,當服務端配置信息發生變更時,客戶端將最新的數據獲取下來之後,保存在了 CacheData 中,同時更新了該 CacheData 的 md5 值,所以當下次執行 checkListenerMd5 方法時,就會發現當前 listener 所持有的 md5 值已經和 CacheData 的 md5 值不一樣了,也就意味着服務端的配置信息發生改變了,這時就需要將最新的數據通知給 Listener 的持有者。
- Nacos 並不是通過推的方式將服務端最新的配置信息發送給客戶端的,而是客戶端維護了一個長輪詢的任務,定時去拉取發生變更的配置信息,然後將最新的數據推送給 Listener 的持有者。
- Nacos Config Client 每 5 分鐘進行一次全量比對。
- Nacos Config Server 有配置發生變化時,發佈
LocalDataChangeEvent
,監聽器監聽到該事件,即開始向 Nacos Config Client 發送ConfigChangeNotifyRequest
。Nacos Config Client 感到到有配置發生變化,向 Nacos Config Server 發送ConfigQueryRequest
請求最新配置內容。 - 客戶端拉取服務端的數據與服務端推送數據給客戶端相比,優勢在於其 Nacos 不設計成主動推送數據,而是要客戶端去拉取。如果用推的方式,服務端需要維持與客戶端的長連接,這樣的話需要耗費大量的資源,並且還需要考慮連接的有效性,例如需要通過心跳來維持兩者之間的連接。而用拉的方式,客戶端只需要通過一個無狀態的 http 請求即可獲取到服務端的數據。
可憐夜半虛前席
不問蒼生問鬼神