nacos源码分析(二)--------客户端获取配置中心的实现

1,993 阅读5分钟

nacos的版本1.1.3. 客户端在启动的时候,需要去配紫中心的获取远程的配置,首先给出一个nacos项目的demo

 public class ConfigExample {

    public static void main(String[] args) throws NacosException, InterruptedException {
        String serverAddr = "localhost";
        String dataId = "test";
        String group = "DEFAULT_GROUP";
        Properties properties = new Properties();
        properties.put("serverAddr", serverAddr);
        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("receive:" + configInfo);
            }

            @Override
            public Executor getExecutor() {
                return null;
            }
        });

        boolean isPublishOk = configService.publishConfig(dataId, group, "content");
        System.out.println(isPublishOk);

        Thread.sleep(3000);
        content = configService.getConfig(dataId, group, 5000);
        System.out.println(content);

        boolean isRemoveOk = configService.removeConfig(dataId, group);
        System.out.println(isRemoveOk);
        Thread.sleep(3000);

        content = configService.getConfig(dataId, group, 5000);
        System.out.println(content);
        Thread.sleep(300000);

    }
}

首先,需要指定配置中心serverAddr的地址、dataId以及group, 然后通过NacosFactory工厂 创建NacosConfigService, 从下图看到,这里就是通过反射创建对象,并通过构造函数传入 Property的配置对象.

 public static ConfigService createConfigService(Properties properties) throws NacosException {
        try {
            Class<?> driverImplClass = Class.forName("com.alibaba.nacos.client.config.NacosConfigService");
            Constructor constructor = driverImplClass.getConstructor(Properties.class);
            ConfigService vendorImpl = (ConfigService) constructor.newInstance(properties);
            return vendorImpl;
        } catch (Throwable e) {
            throw new NacosException(NacosException.CLIENT_INVALID_PARAM, e);
        }
    }

那么接下里就看NacosConfigService是怎么拉取配置的,首先看下NacosConfigServic的构造函数的初始化, 第一步: 获取properties配置文件的配置的编码,默认是UTF-8 第二步: 初始化Namespace,如果没有配置namespace,默认是空的字符串. 第三步: 创建MetricsHttpAgent,构造函数传入了ServerHttpAgent,这里其实是对ServerHttpAgent的装饰器模式, 第四步: 执行MetricsHttpAgent的start方法.去拉取服务列表. 第五步: 创建ClientWork对象, 客户端工作线程池取获取配置中心的配置.

   public NacosConfigService(Properties properties) throws NacosException {
        String encodeTmp = properties.getProperty(PropertyKeyConst.ENCODE);
        if (StringUtils.isBlank(encodeTmp)) {
            encode = Constants.ENCODE;
        } else {
            encode = encodeTmp.trim();
        }
        initNamespace(properties);
        agent = new MetricsHttpAgent(new ServerHttpAgent(properties));
        agent.start();
        worker = new ClientWorker(agent, configFilterChainManager, properties);
    }

接着看下ServerHttpAgent的初始化中,新建了一个ServerListManager对象,然后初始化编码、AccessKey、MaxRetry(默认值是3次)三个参数

   public ServerHttpAgent(Properties properties) throws NacosException {
        serverListMgr = new ServerListManager(properties);
        init(properties);
    }
     private void init(Properties properties) {
        initEncode(properties);
        initAkSk(properties);
        initMaxRetry(properties);
    }

初始化ServerListManager,初始endpoint、化contextPath、ClusterName等参数,然后对 serverAddrsStr配置参数进行处理,处理成标准的url地址赋值给serverUrls.

    public ServerListManager(Properties properties) throws NacosException {
       isStarted = false;
       serverAddrsStr = properties.getProperty(PropertyKeyConst.SERVER_ADDR);
       String namespace = properties.getProperty(PropertyKeyConst.NAMESPACE);
       initParam(properties);
       //判断serverAddrsStr是否为空
       if (StringUtils.isNotEmpty(serverAddrsStr)) {
           isFixed = true;
           List<String> serverAddrs = new ArrayList<String>();
           String[] serverAddrsArr = serverAddrsStr.split(",");
           for (String serverAddr: serverAddrsArr) {
               if (serverAddr.startsWith(HTTPS) || serverAddr.startsWith(HTTP)) {
                   serverAddrs.add(serverAddr);
               } else {
                   String[] serverAddrArr = serverAddr.split(":");
                   if (serverAddrArr.length == 1) {
                       serverAddrs.add(HTTP + serverAddrArr[0] + ":" + ParamUtil.getDefaultServerPort());
                   } else {
                       serverAddrs.add(HTTP + serverAddr);
                   }
               }
           }
           serverUrls = serverAddrs;
           if (StringUtils.isBlank(namespace)) {
               name = FIXED_NAME + "-" + getFixedNameSuffix(serverUrls.toArray(new String[serverUrls.size()]));
           } else {
               this.namespace = namespace;
               this.tenant = namespace;
               name = FIXED_NAME + "-" + getFixedNameSuffix(serverUrls.toArray(new String[serverUrls.size()])) + "-"
                   + namespace;
           }
       } else {
          //异常判断
           if (StringUtils.isBlank(endpoint)) {
               throw new NacosException(NacosException.CLIENT_INVALID_PARAM, "endpoint is blank");
           }
           isFixed = false;
           if (StringUtils.isBlank(namespace)) {
               name = endpoint;
               addressServerUrl = String.format("http://%s:%d/%s/%s", endpoint, endpointPort, contentPath,
                   serverListName);
           } else {
               this.namespace = namespace;
               this.tenant = namespace;
               name = endpoint + "-" + namespace;
               addressServerUrl = String.format("http://%s:%d/%s/%s?namespace=%s", endpoint, endpointPort,
                   contentPath, serverListName, namespace);
           }
       }
   }

接下来就是执行MetricsHttpAgent的start,启动Http代理的逻辑,MetricsHttpAgen里面ServerHttpAgent对象,实际ServerHttpAgent对象的start方法.

public class MetricsHttpAgent implements HttpAgent {
  //代理对象是ServerHttpAgent对象
  private HttpAgent httpAgent;

   @Override
  public void start() throws NacosException {
      httpAgent.start();
  }

这里ServerListManager的start方法是通过GetServerListTask后服务中心去拉取配置中心的地址列表,由于demo中配置是固定的配置中心的地址,所以这里isFixed是true,直接返回

  public synchronized void start() throws NacosException {
      if (isStarted || isFixed) {
          return;
      }
      GetServerListTask getServersTask = new GetServerListTask(addressServerUrl);
      for (int i = 0; i < initServerlistRetryTimes && serverUrls.isEmpty(); ++i) {
          getServersTask.run();
          try {
              this.wait((i + 1) * 100L);
          } catch (Exception e) {
              LOGGER.warn("get serverlist fail,url: {}", addressServerUrl);
          }
      }

      if (serverUrls.isEmpty()) {
          LOGGER.error("[init-serverlist] fail to get NACOS-server serverlist! env: {}, url: {}", name,
              addressServerUrl);
          throw new NacosException(NacosException.SERVER_ERROR,
              "fail to get NACOS-server serverlist! env:" + name + ", not connnect url:" + addressServerUrl);
      }
      //定时任务每隔30s进行获取配置中心地址列表
      TimerService.scheduleWithFixedDelay(getServersTask, 0L, 30L, TimeUnit.SECONDS);
      isStarted = true;
  }

到了这里,接下来出场的就是今天的主角----ClientWorker类,初始化NacosConfigService第五步中,可以看到创建ClientWorker时候,传入四个参数HttpAagent就是MetricsHttpAgent对象,第二个过滤器是ConfigFilterChainManager对象,第三个是properties配置,

public ClientWorker(final HttpAgent agent, final ConfigFilterChainManager configFilterChainManager, final Properties properties) {
      this.agent = agent;
      this.configFilterChainManager = configFilterChainManager;
      //初始化超时时间的配置
      init(properties);
      //客户端,周期执行的线程池,这里重写ThreadFactory,这样线程运行会com.alibaba.nacos.client.Worke的前前缀,主要是执行检查配置信息的周期的线程池
      executor = Executors.newScheduledThreadPool(1, new ThreadFactory() {
          @Override
          public Thread newThread(Runnable r) {
              Thread t = new Thread(r);
              t.setName("com.alibaba.nacos.client.Worker." + agent.getName());
              t.setDaemon(true);
              return t;
          }
      });
      //客户端长轮询的线程池,配置中心拉去配置
      executorService = Executors.newScheduledThreadPool(Runtime.getRuntime().availableProcessors(), new ThreadFactory() {
          @Override
          public Thread newThread(Runnable r) {
              Thread t = new Thread(r);
              t.setName("com.alibaba.nacos.client.Worker.longPolling." + agent.getName());
              t.setDaemon(true);
              return t;
          }
      });
      //每隔10毫秒执行checkConfigInfo方法
      executor.scheduleWithFixedDelay(new Runnable() {
          @Override
          public void run() {
              try {
                  checkConfigInfo();
              } catch (Throwable e) {
                  LOGGER.error("[" + agent.getName() + "] [sub-check] rotate check error", e);
              }
          }
      }, 1L, 10L, TimeUnit.MILLISECONDS);
  }

执行checkConfigInfo方法, 这里是每3000个任务为一个批次任务处理,使用另外一个 线程池的处理长轮询的处理,一个批次处理就是执行LongPollingRunnable这个线程.

    public void checkConfigInfo() {
       // 分任务
       int listenerSize = cacheMap.get().size();
       // 向上取整为批数
       int longingTaskCount = (int) Math.ceil(listenerSize / ParamUtil.getPerTaskConfigSize());
       if (longingTaskCount > currentLongingTaskCount) {
           for (int i = (int) currentLongingTaskCount; i < longingTaskCount; i++) {
               // 要判断任务是否在执行 这块需要好好想想。 任务列表现在是无序的。变化过程可能有问题
               executorService.execute(new LongPollingRunnable(i));
           }
           currentLongingTaskCount = longingTaskCount;
       }
   }

下面看下LongPollingRunnable这个类,它是实现了Runnable接口的,所以丢给线程池执行,就会运行run方法, 首先循环cacheMap中缓存的数据,checkLocalConfig,主要是检查本地文件的 信息是否变更,如果是使用本地文件缓存的配置则需要校验文件内容的md5值, 获取有变化的 dataId、group,然后http从远程获取存储到cacheMap中

 class LongPollingRunnable implements Runnable {
        private int taskId;
        public LongPollingRunnable(int taskId) {
            this.taskId = taskId;
        }
        @Override
        public void run() {
            List<CacheData> cacheDatas = new ArrayList<CacheData>();
            List<String> inInitializingCacheList = new ArrayList<String>();
            try {
                // check failover config
                for (CacheData cacheData : cacheMap.get().values()) {
                    if (cacheData.getTaskId() == taskId) {
                        cacheDatas.add(cacheData);
                        try {
                            checkLocalConfig(cacheData);
                            if (cacheData.isUseLocalConfigInfo()) {
                                cacheData.checkListenerMd5();
                            }
                        } catch (Exception e) {
                            LOGGER.error("get local config info error", e);
                        }
                    }
                }
                // check server config
                List<String> changedGroupKeys = checkUpdateDataIds(cacheDatas, inInitializingCacheList);
                for (String groupKey : changedGroupKeys) {
                    String[] key = GroupKey.parseKey(groupKey);
                    String dataId = key[0];
                    String group = key[1];
                    String tenant = null;
                    if (key.length == 3) {
                        tenant = key[2];
                    }
                    try {
                        String content = getServerConfig(dataId, group, tenant, 3000L);
                        CacheData cache = cacheMap.get().get(GroupKey.getKeyTenant(dataId, group, tenant));
                        cache.setContent(content);
                        LOGGER.info("[{}] [data-received] dataId={}, group={}, tenant={}, md5={}, content={}",
                            agent.getName(), dataId, group, tenant, cache.getMd5(),
                            ContentUtils.truncateContent(content));
                    } catch (NacosException ioe) {
                        String message = String.format(
                            "[%s] [get-update] get changed config exception. dataId=%s, group=%s, tenant=%s",
                            agent.getName(), dataId, group, tenant);
                        LOGGER.error(message, ioe);
                    }
                }
                for (CacheData cacheData : cacheDatas) {
                    if (!cacheData.isInitializing() || inInitializingCacheList
                        .contains(GroupKey.getKeyTenant(cacheData.dataId, cacheData.group, cacheData.tenant))) {
                        cacheData.checkListenerMd5();
                        cacheData.setInitializing(false);
                    }
                }
                inInitializingCacheList.clear();
                executorService.execute(this);
            } catch (Throwable e) {
                // If the rotation training task is abnormal, the next execution time of the task will be punished
                LOGGER.error("longPolling error : ", e);
                executorService.schedule(this, taskPenaltyTime, TimeUnit.MILLISECONDS);
            }
        }
    }

拉取配置就是利用NacosConfigService中getConfig方法,优先使用本地的文件的配置,没有则从远程拉取配置.


    private String getConfigInner(String tenant, String dataId, String group, long timeoutMs) throws NacosException {
        group = null2defaultGroup(group);
        ParamUtils.checkKeyParam(dataId, group);
        ConfigResponse cr = new ConfigResponse();
        cr.setDataId(dataId);
        cr.setTenant(tenant);
        cr.setGroup(group);
        // 优先使用本地配置
        String content = LocalConfigInfoProcessor.getFailover(agent.getName(), dataId, group, tenant);
        if (content != null) {
            LOGGER.warn("[{}] [get-config] get failover ok, dataId={}, group={}, tenant={}, config={}", agent.getName(),
                dataId, group, tenant, ContentUtils.truncateContent(content));
            cr.setContent(content);
            configFilterChainManager.doFilter(null, cr);
            content = cr.getContent();
            return content;
        }
        try {
            content = worker.getServerConfig(dataId, group, tenant, timeoutMs);
            cr.setContent(content);
            configFilterChainManager.doFilter(null, cr);
            content = cr.getContent();
            return content;
        } catch (NacosException ioe) {
            if (NacosException.NO_RIGHT == ioe.getErrCode()) {
                throw ioe;
            }
            LOGGER.warn("[{}] [get-config] get from server error, dataId={}, group={}, tenant={}, msg={}",
                agent.getName(), dataId, group, tenant, ioe.toString());
        }

        LOGGER.warn("[{}] [get-config] get snapshot ok, dataId={}, group={}, tenant={}, config={}", agent.getName(),
            dataId, group, tenant, ContentUtils.truncateContent(content));
        content = LocalConfigInfoProcessor.getSnapshot(agent.getName(), dataId, group, tenant);
        cr.setContent(content);
        configFilterChainManager.doFilter(null, cr);
        content = cr.getContent();
        return content;
    }

总结
今天主要分析nacos的初始化,并获取配置中心的过程分析.那么监听的呢,下一篇继续分析.