浅析Nacos自动配置的原理

711 阅读7分钟

浅析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 中的命名规则prefix{prefix}-{spring.profiles.active}-${file-extension} ,其中prefix使用spring-application-name,spring.profile.active指的是环境,file-extention指的是配置文件的类型,yaml或者properties都可以

在实际开发中不一定非要按照上述的规则来进行划分,我见过使用Namespace做应用的配置,使用Group做不同区域服务器的配置,DataID做不同环境的配置 在我们进行使用的时候,只要配置好了这三个,以及对应的nacos的地址,就可以进行动态更新配置了,这三个会组成单个配置的唯一标识

Nacos的使用就不再进行赘述了,配置好了之后就跟平时有yml或者properties文件一样,进行配置的读取即可

相关代码

Client相关代码

image-20220426152625041.png

客户端的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版本的在

image-20220426144843982.png

这其中的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) {
        }
        
    }

这地方的处理器包括发布配置的,配置变更的,配置查询的等等

image-20220426145133152.png

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端保持一致的方式如下图所示(来自官方文档)

image-20220426145953569.png

Nacos关于配置的交互,大致如图

image-20220426150621737.png

Nacos的控制台跟NacosServer之间使用的是http调用,Nacos Client跟Nacos Server使用的是grpc调用。监听同步任务是通过一个阻塞队列实现的,ClientWorker会使用各种方式往阻塞队列中放元素,能取出的时候就执行一次。 在Server端的配置变更后,通过发布事件,进行grpc的调用,而client端则又会执行一次监听同步任务,进行配置数据的更新。本次分享只是简单地进行了下分析,下次分享会将更加具体的细节展现给大家。谢谢!