浅析Nacos的动态配置原理
Nacos(Dynamic Naming and Configuration Service)是一个动态服务发现,配置管理和服务管理的平台,本文主要讲述它的动态配置功能,属于配置管理的模块。 本文基于Nacos2.0.3版本(github.com/alibaba/nac…)
相关概念
命名空间(Namespace)
不同的命名空间下, 可以存在相同的 Group 或 Data ID 的配置。Namespace 的常用场景之⼀是不同环境的配置的区分隔离, 例如开发测试环境和生产环境的资源(如数据库配置、 限流阈值、 降级开关) 隔离等。未指定 Namespace 的情况下, 将默认使用 public 命名空间。
配置组(Group)
Nacos 中的一组配置集,是配置的纬度之一。通过一个有意义的字符串(如 ABTest 中的实验组、对照组) 对配置集进行分组, 从而区分 Data ID 相同的配置集。未指定 Group的情况下, 将默认使用DEFAULT_GROUP 。 配置分组的常见场景: 不同的应用或组件使用了相同的配置项, 如数据库url配置,消息的topic配置等。
配置ID(Data ID)
Nacos 中的某个配置集的 ID。 配置集 ID 是划分配置的维度之⼀。 Data ID 通常用于划分系统的配置集。 ⼀个系统或者应用可以包含多个配置集, 每个配置集都可以被⼀个有意义的名称标识。 DataID 尽量保障全局唯⼀, 可以参考 Nacos Spring Cloud 中的命名规则{spring.profiles.active}-${file-extension} ,其中prefix使用spring-application-name,spring.profile.active指的是环境,file-extention指的是配置文件的类型,yaml或者properties都可以
在实际开发中不一定非要按照上述的规则来进行划分,我见过使用Namespace做应用的配置,使用Group做不同区域服务器的配置,DataID做不同环境的配置 在我们进行使用的时候,只要配置好了这三个,以及对应的nacos的地址,就可以进行动态更新配置了,这三个会组成单个配置的唯一标识
Nacos的使用就不再进行赘述了,配置好了之后就跟平时有yml或者properties文件一样,进行配置的读取即可
相关代码
Client相关代码
客户端的ClientWorker的初始化,会通过阻塞队列listenExcutebell,进行监听同步任务的进行
@Override
public void startInternal() throws NacosException {
executor.schedule(new Runnable() {
@Override
public void run() {
while (!executor.isShutdown() && !executor.isTerminated()) {
try {
//阻塞队列,无元素可取的时候会阻塞
listenExecutebell.poll(5L, TimeUnit.SECONDS);
if (executor.isShutdown() || executor.isTerminated()) {
continue;
}
executeConfigListen();
} catch (Exception e) {
LOGGER.error("[ rpc listen execute ] [rpc listen] exception", e);
}
}
}
}, 0L, TimeUnit.MILLISECONDS);
}
excuteConfigListen方法就是我们的监听同步任务
@Override
public void executeConfigListen() {
Map<String, List<CacheData>> listenCachesMap = new HashMap<String, List<CacheData>>(16);
Map<String, List<CacheData>> removeListenCachesMap = new HashMap<String, List<CacheData>>(16);
long now = System.currentTimeMillis();
//查看现在距离key的上次所有Key同步时间不小于5分钟的时候,需要做一次全部key同步
boolean needAllSync = now - lastAllSyncTime >= ALL_SYNC_INTERNAL;
//本地保存的所有的cache
for (CacheData cache : cacheMap.get().values()) {
synchronized (cache) {
//检查本地与server端的配置是否同步
//一致的话,检测md5,通知监听者,修改监听者的lastMd5与cache的md5一致
if (cache.isSyncWithServer()) {
cache.checkListenerMd5();
//如果不需要进行全部的同步的话,就开始下一个cache的检查,如果需要全部同步的话,这个key就需要走后续流程
if (!needAllSync) { continue; }
}
//当cache至少有一个listener时
if (!CollectionUtils.isEmpty(cache.getListeners())) {
//不使用本地的配置,操作,3000个为一组,放LinkedList中
if (!cache.isUseLocalConfigInfo()) {
List<CacheData> cacheDatas = listenCachesMap.get(String.valueOf(cache.getTaskId()));
if (cacheDatas == null) {
cacheDatas = new LinkedList<CacheData>();
listenCachesMap.put(String.valueOf(cache.getTaskId()), cacheDatas);
}
cacheDatas.add(cache);
}
//当cache没有listener的时候,将其放入要移除监听的cacheMap列表中
} else if (CollectionUtils.isEmpty(cache.getListeners())) {
if (!cache.isUseLocalConfigInfo()) {
List<CacheData> cacheDatas = removeListenCachesMap.get(String.valueOf(cache.getTaskId()));
if (cacheDatas == null) {
cacheDatas = new LinkedList<CacheData>();
removeListenCachesMap.put(String.valueOf(cache.getTaskId()), cacheDatas);
}
cacheDatas.add(cache);
}
}
}
}
boolean hasChangedKeys = false;
if (!listenCachesMap.isEmpty()) {
for (Map.Entry<String, List<CacheData>> entry : listenCachesMap.entrySet()) {
String taskId = entry.getKey();
Map<String, Long> timestampMap = new HashMap<>(listenCachesMap.size() * 2);
List<CacheData> listenCaches = entry.getValue();
for (CacheData cacheData : listenCaches) {
//每一个有Listener的cache都将最后一次修改时间放入timestampMap中去
timestampMap.put(GroupKey.getKeyTenant(cacheData.dataId, cacheData.group, cacheData.tenant),
cacheData.getLastModifiedTs().longValue());
}
ConfigBatchListenRequest configChangeListenRequest = buildConfigRequest(listenCaches);
configChangeListenRequest.setListen(true);
try {
RpcClient rpcClient = ensureRpcClient(taskId);
//去请求有变化的cache列表
ConfigChangeBatchListenResponse configChangeBatchListenResponse = (ConfigChangeBatchListenResponse) requestProxy(
rpcClient, configChangeListenRequest);
if (configChangeBatchListenResponse != null && configChangeBatchListenResponse.isSuccess()) {
Set<String> changeKeys = new HashSet<String>();
//处理有变化的配置,通知监听者
if (!CollectionUtils.isEmpty(configChangeBatchListenResponse.getChangedConfigs())) {
hasChangedKeys = true;
for (ConfigChangeBatchListenResponse.ConfigContext changeConfig : configChangeBatchListenResponse
.getChangedConfigs()) {
String changeKey = GroupKey
.getKeyTenant(changeConfig.getDataId(), changeConfig.getGroup(),
changeConfig.getTenant());
changeKeys.add(changeKey);
//查看这个有变化的配置的Key是否正在初始化
boolean isInitializing = cacheMap.get().get(changeKey).isInitializing();
refreshContentAndCheck(changeKey, !isInitializing);
}
}
for (CacheData cacheData : listenCaches) {
String groupKey = GroupKey
.getKeyTenant(cacheData.dataId, cacheData.group, cacheData.getTenant());
if (!changeKeys.contains(groupKey)) {
//同步本地cache与server的md5,再将该cache的所有listener的md5也更新为Cache的md5
//sync:cache data md5 = server md5 && cache data md5 = all listeners md5.
synchronized (cacheData) {
if (!cacheData.getListeners().isEmpty()) {
Long previousTimesStamp = timestampMap.get(groupKey);
//将cache当前的最后一次修改的时间改为当下
if (previousTimesStamp != null) {
if (!cacheData.getLastModifiedTs().compareAndSet(previousTimesStamp,
System.currentTimeMillis())) {
continue;
}
}
//设置:已经与server同步了
cacheData.setSyncWithServer(true);
}
}
}
//设置initializing属性
cacheData.setInitializing(false);
}
}
} catch (Exception e) {
}
}
}
//这个遍历的是没有监听器的cache,通知server端移除监听,本地Map清除缓存
if (!removeListenCachesMap.isEmpty()) {
for (Map.Entry<String, List<CacheData>> entry : removeListenCachesMap.entrySet()) {
String taskId = entry.getKey();
List<CacheData> removeListenCaches = entry.getValue();
ConfigBatchListenRequest configChangeListenRequest = buildConfigRequest(removeListenCaches);
configChangeListenRequest.setListen(false);
try {
RpcClient rpcClient = ensureRpcClient(taskId);
//请求server端,这个config不进行监听了(Server端的数据发生变化也不会进行通知)
boolean removeSuccess = unListenConfigChange(rpcClient, configChangeListenRequest);
if (removeSuccess) {
for (CacheData cacheData : removeListenCaches) {
synchronized (cacheData) {
if (cacheData.getListeners().isEmpty()) {
ClientWorker.this
.removeCache(cacheData.dataId, cacheData.group, cacheData.tenant);
}
}
}
}
} catch (Exception e) { }
try {
Thread.sleep(50L);
} catch (InterruptedException interruptedException) {
}
}
}
if (needAllSync) {
lastAllSyncTime = now;
}
//如果有变化的key,往之前说过的阻塞队列中压入元素,取出后再次执行本方法
if (hasChangedKeys) {
notifyListenConfig();
}
}
Server相关代码
查询变化的配置列表(同时也是移除不再监听的Key的方法)注:该方法不是configController下的Listener方法了,那个是1.X版本的(只是兼容)。2.x版本的在
这其中的request方法中,通过Request的类型使用不同的处理,我们的配置变更就是使用的ConfigChangeBatchListenRequestHandler
@Override
public void request(Payload grpcRequest, StreamObserver<Payload> responseObserver) {
//这个方法就是远程调用的时候会调用的方法
//根据请求转化的类型不同,进行不同的处理
RequestHandler requestHandler = requestHandlerRegistry.getByRequestType(type);
Request request = (Request) parseObj;
try {
Connection connection = connectionManager.getConnection(CONTEXT_KEY_CONN_ID.get());
RequestMeta requestMeta = new RequestMeta();
requestMeta.setClientIp(connection.getMetaInfo().getClientIp());
requestMeta.setConnectionId(CONTEXT_KEY_CONN_ID.get());
requestMeta.setClientVersion(connection.getMetaInfo().getVersion());
requestMeta.setLabels(connection.getMetaInfo().getLabels());
connectionManager.refreshActiveTime(requestMeta.getConnectionId());
//使用对应的处理器开始处理请求
Response response = requestHandler.handleRequest(request, requestMeta);
Payload payloadResponse = GrpcUtils.convert(response);
traceIfNecessary(payloadResponse, false);
responseObserver.onNext(payloadResponse);
responseObserver.onCompleted();
} catch (Throwable e) {
}
}
这地方的处理器包括发布配置的,配置变更的,配置查询的等等
public ConfigChangeBatchListenResponse handle(ConfigBatchListenRequest configChangeListenRequest, RequestMeta meta)
throws NacosException {
String connectionId = StringPool.get(meta.getConnectionId());
String tag = configChangeListenRequest.getHeader(Constants.VIPSERVER_TAG);
ConfigChangeBatchListenResponse configChangeBatchListenResponse = new ConfigChangeBatchListenResponse();
for (ConfigBatchListenRequest.ConfigListenContext listenContext : configChangeListenRequest
.getConfigListenContexts()) {
String groupKey = GroupKey2
.getKey(listenContext.getDataId(), listenContext.getGroup(), listenContext.getTenant());
groupKey = StringPool.get(groupKey);
String md5 = StringPool.get(listenContext.getMd5());
if (configChangeListenRequest.isListen()) {
//添加监听
configChangeListenContext.addListen(groupKey, md5, connectionId);
//查看它是否有变化(使用Md5比较),相同为ture,不同为false
boolean isUptoDate = ConfigCacheService.isUptodate(groupKey, md5, meta.getClientIp(), tag);
//假如这个Key不相同
if (!isUptoDate) {
configChangeBatchListenResponse.addChangeConfig(listenContext.getDataId(), listenContext.getGroup(),
listenContext.getTenant());
}
//移除监听的时候也是调用的这个方法,但是会走这条路径
//就是将客户端会将发来的请求中的isListen设置为false
} else {
configChangeListenContext.removeListen(groupKey, connectionId);
}
}
return configChangeBatchListenResponse;
}
查询配置的方法,直接在数据库进行查询
private ConfigQueryResponse getContext(ConfigQueryRequest configQueryRequest, RequestMeta meta, boolean notify)
throws UnsupportedEncodingException {
String dataId = configQueryRequest.getDataId();
String group = configQueryRequest.getGroup();
String tenant = configQueryRequest.getTenant();
String clientIp = meta.getClientIp();
String tag = configQueryRequest.getTag();
ConfigQueryResponse response = new ConfigQueryResponse();
final String groupKey = GroupKey2
.getKey(configQueryRequest.getDataId(), configQueryRequest.getGroup(), configQueryRequest.getTenant());
int lockResult = tryConfigReadLock(groupKey);
if (lockResult > 0) {
try {
ConfigInfoBase configInfoBase = null;
PrintWriter out = null;
if (StringUtils.isBlank(tag)) {
if (isUseTag(cacheItem, autoTag)) {
if (cacheItem != null) {
if (cacheItem.tagMd5 != null) {
md5 = cacheItem.tagMd5.get(autoTag);
}
if (cacheItem.tagLastModifiedTs != null) {
lastModified = cacheItem.tagLastModifiedTs.get(autoTag);
}
}
if (PropertyUtil.isDirectRead()) {
configInfoBase = persistService.findConfigInfo4Tag(dataId, group, tenant, autoTag);
} else {
file = DiskUtil.targetTagFile(dataId, group, tenant, autoTag);
}
response.setTag(URLEncoder.encode(autoTag, Constants.ENCODE));
} else {
md5 = cacheItem.getMd5();
lastModified = cacheItem.getLastModifiedTs();
if (PropertyUtil.isDirectRead()) {
//直接在数据库进行查询
configInfoBase = persistService.findConfigInfo(dataId, group, tenant);
} else {
file = DiskUtil.targetFile(dataId, group, tenant);
}
if (configInfoBase == null && fileNotExist(file)) {
ConfigTraceService.logPullEvent(dataId, group, tenant, requestIpApp, -1,
ConfigTraceService.PULL_EVENT_NOTFOUND, -1, clientIp, false);
response.setErrorInfo(ConfigQueryResponse.CONFIG_NOT_FOUND, "config data not exist");
return response;
}
return response;
}
配置发生变更时的推送
public Boolean publishConfig(HttpServletRequest request, HttpServletResponse respons) throws NacosException {
final String srcIp = RequestUtil.getRemoteIp(request);
final String requestIpApp = RequestUtil.getAppName(request);
srcUser = RequestUtil.getSrcUserName(request);
final Timestamp time = TimeUtils.getCurrentTime();
String betaIps = request.getHeader("betaIps");
ConfigInfo configInfo = new ConfigInfo(dataId, group, tenant, appName, content);
configInfo.setType(type);
if (StringUtils.isBlank(betaIps)) {
if (StringUtils.isBlank(tag)) {
persistService.insertOrUpdate(srcIp, srcUser, configInfo, time, configAdvanceInfo, false);
ConfigChangePublisher
.notifyConfigChange(new ConfigDataChangeEvent(false, dataId, group, tenant, time.getTime()));
} else {
//持久化数据
persistService.insertOrUpdateTag(configInfo, tag, srcIp, srcUser, time, false);
//发布配置变更事件,后续会在RpcPushService中调用pushWithCallback
ConfigChangePublisher.notifyConfigChange(
new ConfigDataChangeEvent(false, dataId, group, tenant, tag, time.getTime()));
}
} else {
persistService.insertOrUpdateBeta(configInfo, betaIps, srcIp, srcUser, time, false);
ConfigChangePublisher
.notifyConfigChange(new ConfigDataChangeEvent(true, dataId, group, tenant, time.getTime()));
}
ConfigTraceService
.logPersistenceEvent(dataId, group, tenant, requestIpApp, time.getTime(), InetUtils.getSelfIP(),
ConfigTraceService.PERSISTENCE_EVENT_PUB, content);
return true;
}
失败的话,会再次尝试push
@Override
public void run() {
tryTimes++;
if (!tpsMonitorManager.applyTpsForClientIp(POINT_CONFIG_PUSH, connectionId, clientIp)) {
push(this);
} else {
rpcPushService.pushWithCallback(connectionId, notifyRequest, new AbstractPushCallBack(3000L) {
@Override
public void onSuccess() {
tpsMonitorManager.applyTpsForClientIp(POINT_CONFIG_PUSH_SUCCESS, connectionId, clientIp);
}
@Override
public void onFail(Throwable e) {
tpsMonitorManager.applyTpsForClientIp(POINT_CONFIG_PUSH_FAIL, connectionId, clientIp);
Loggers.REMOTE_PUSH.warn("Push fail", e);
//失败了的话,会进行重试
push(RpcPushTask.this);
}
}, ConfigExecutor.getClientConfigNotifierServiceExecutor());
}
}
总结
nacos的客户端与server端保持一致的方式如下图所示(来自官方文档)
Nacos关于配置的交互,大致如图
Nacos的控制台跟NacosServer之间使用的是http调用,Nacos Client跟Nacos Server使用的是grpc调用。监听同步任务是通过一个阻塞队列实现的,ClientWorker会使用各种方式往阻塞队列中放元素,能取出的时候就执行一次。 在Server端的配置变更后,通过发布事件,进行grpc的调用,而client端则又会执行一次监听同步任务,进行配置数据的更新。本次分享只是简单地进行了下分析,下次分享会将更加具体的细节展现给大家。谢谢!