nacos配置中心动态刷新原理分析【客户端】

目录

      一、背景介绍

      二、nacos长轮询

      三、nacos与spring整合


一、背景介绍

配置中心是SpringCloud系统中常见的一种技术方案,配置中心区别于传统的各个系统当中分散的配置方式,它对系统中的配置文件进行集中式、统一管理。

使用配置中心好处有:

①、通过配置中心,可以使得配置标准化、格式统一化。

②、当配置信息发生变动时,修改实时生效,无需要重新重启服务器,可以实时去调整相关对应的业务。

最近在做技术调研形成落地方案的时候,突然想到了nacos的动态配置刷新,于是想去看下nacos动态配置到底怎么实现的,今天抽出点时间看了下nacos配置中心源码实现。

所以,本篇文章就介绍nacos是怎么扩展spring实现自己的配置数据动态加载的。

二、nacos长轮询

首先第一点需要知道的是nacos动态配置从nacos角度来看核心是通过长轮询(LongPolling)来实现的,我们不妨先从nacos角度来看其长轮询实现方式,然后再看和spring的整合。

所谓长轮询,就是服务器接收到请求后,hold连接一段时间暂时不返回消息,直到进行相关处理完毕后才返回响应信息并关闭连接,客户端接收到响应信息后,进行相关处理,处理完毕后再向服务器发送新的请求。

所以在一次nacos客户端连接服务端当中,服务端断开连接也就有了两种情况:

①、服务端数据发生了变化,需要推送到客户端,向客户端推送数据,断开连接。

②、在一段时间内没有数据发生改变,超时断开连接。

下面我们来看nacos是怎么实现LongPolling的。

nacos长轮询逻辑是在类ClientWorker当中。

这里我们先从类ClientWorker的构造方法看起,因为下面讲的和spring集成部分都是在类ClientWorker的构造方法的上层部分,等说到和spring继承的时候我们再衔接到类ClientWorker的构造方法这个地方。

public ClientWorker(final HttpAgent agent, final ConfigFilterChainManager configFilterChainManager, final Properties properties) {
  // ......
  this.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;
      }
  });

  this.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;
          }
      });

  this.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);
}

在类ClientWorker的构造方法中,我们看到这里初始化了executor、executorService两个定时任务和启动了executor这个定时任务,我们重点看checkConfigInfo()方法;

public void checkConfigInfo() {
    // Dispatch taskes.
    int listenerSize = cacheMap.get().size();
    // Round up the longingTaskCount.
    int longingTaskCount = (int) Math.ceil(listenerSize / ParamUtil.getPerTaskConfigSize());
    if (longingTaskCount > currentLongingTaskCount) {
        for (int i = (int) currentLongingTaskCount; i < longingTaskCount; i++) {
            // The task list is no order.So it maybe has issues when changing.
            executorService.execute(new LongPollingRunnable(i));
        }
        currentLongingTaskCount = longingTaskCount;
    }
}

在checkConfigInfo()方法当中,ParamUtil.getPerTaskConfigSize()对执行的任务分了组,其逻辑我们不细看,看核心逻辑executorService执行(new LongPollingRunnable(i)),我们直接看LongPollingRunnable的逻辑实现。

class LongPollingRunnable implements Runnable {
        private final int taskId;
    public LongPollingRunnable(int taskId) {
        this.taskId = taskId;
        }
    @Override
    public void run() {
          ......
    }
}

可以看到LongPollingRunnable就是一个实现了Runnable的任务体,其具体的执行逻辑当然就在run()方法当中了,所以,接下来重点看run()方法执行的逻辑。

public void run() {
    List<CacheData> cacheDatas = new ArrayList<CacheData>();
    List<String> inInitializingCacheList = new ArrayList<String>();
    try {
        // ...... 
        // check server config
        List<String> changedGroupKeys = checkUpdateDataIds(cacheDatas, inInitializingCacheList);
        if (!CollectionUtils.isEmpty(changedGroupKeys)) {
            LOGGER.info("get changedGroupKeys:" + changedGroupKeys);
        }

        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[] ct = getServerConfig(dataId, group, tenant, 3000L);
                CacheData cache = cacheMap.get().get(GroupKey.getKeyTenant(dataId, group, tenant));
                cache.setContent(ct[0]);
                if (null != ct[1]) {
                    cache.setType(ct[1]);
                }
                LOGGER.info("[{}] [data-received] dataId={}, group={}, tenant={}, md5={}, content={}, type={}",
                    agent.getName(), dataId, group, tenant, cache.getMd5(),
                    ContentUtils.truncateContent(ct[0]), ct[1]);
            } 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的监听器
                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);
    }
}

首先看checkUpdateDataIds(cacheDatas, inInitializingCacheList)这个方法,这个方法就是和nacos服务端进行通信,目的是看是否有数据发生改变,如果发生改变,返回的changedGroupKeys就是那些key发生了改变,我们先看下和服务端通信的逻辑。

List<String> checkUpdateDataIds(List<CacheData> cacheDatas, List<String> inInitializingCacheList) throws Exception {
    //......  
    return checkUpdateConfigStr(sb.toString(), isInitializingCacheList);
}

List<String> checkUpdateConfigStr(String probeUpdateString, boolean isInitializingCacheList) throws Exception {
    //......
    HttpRestResult<String> result = agent
        .httpPost(Constants.CONFIG_CONTROLLER_PATH + "/listener", headers, params, agent.getEncode(),
                readTimeoutMs);
    if (result.ok()) {
        setHealthServer(true);
        return parseUpdateDataIdResponse(result.getData());
    } else {
        setHealthServer(false);
        LOGGER.error("[{}] [check-update] get changed dataId error, code: {}", agent.getName(),
            result.getCode());
        }
    return Collections.emptyList();
}

public HttpRestResult<String> httpPost(String path, Map<String, String> headers, Map<String, String> paramValues,
        String encode, long readTimeoutMs) throws Exception {
    //......
    HttpRestResult<String> result = NACOS_RESTTEMPLATE
            .postForm(getUrl(currentServerAddr, path), httpConfig, newHeaders,
                            new HashMap<String, String>(0), paramValues, String.class);
    if (isFail(result)) {
        LOGGER.error("[NACOS ConnectException] currentServerAddr: {}, httpCode: {}", currentServerAddr,
                result.getCode());
    } else {
        // Update the currently available server addr
        serverListMgr.updateCurrentServerAddr(currentServerAddr);
        return result;
    }
    //......
    throw new ConnectException("no available server, currentServerAddr : " + currentServerAddr);
}

其实就是拿到项目中配置的nacos服务端地址拼接上uri后直接和nacos服务端进行通信,获取已经发生改变的key。

然后下面整个一个for循环就是对changedGroupKeys进行遍历,从遍历出来的每一项groupKey解析出来dataId、group、tenant,然后通过这三个参数执行getServerConfig()方法获取到真正改变的内容,更新到本地缓存cacheMap中的CacheData。

然后接下来的for循环核心执行的就是通知监听器刷新nacos数据。

for (CacheData cacheData : cacheDatas) {
    if (!cacheData.isInitializing() || inInitializingCacheList
        .contains(GroupKey.getKeyTenant(cacheData.dataId, cacheData.group, cacheData.tenant))) {
        //通知CacheData的监听器
        cacheData.checkListenerMd5();
        cacheData.setInitializing(false);
    }
}

我们直接看cacheData.checkListenerMd5()方法的执行逻辑。

 void checkListenerMd5() {
        for (ManagerListenerWrap wrap : listeners) {
            if (!md5.equals(wrap.lastCallMd5)) {
                safeNotifyListener(dataId, group, content, type, md5, wrap);
            }
        }
    }
    
    private void safeNotifyListener(final String dataId, final String group, final String content, final String type,
                                final String md5, final ManagerListenerWrap listenerWrap) {
    final Listener listener = listenerWrap.listener;
    Runnable job = new Runnable() {
        @Override
        public void run() {
            ClassLoader myClassLoader = Thread.currentThread().getContextClassLoader();
            ClassLoader appClassLoader = listener.getClass().getClassLoader();
            try {
                if (listener instanceof AbstractSharedListener) {
                    AbstractSharedListener adapter = (AbstractSharedListener) listener;
                    adapter.fillContext(dataId, group);
                    LOGGER.info("[{}] [notify-context] dataId={}, group={}, md5={}", name, dataId, group, md5);
                }
                // 执行回调之前先将线程classloader设置为具体webapp的classloader,以免回调方法中调用spi接口出现异常或错用(多应用部署才会有该问题)。
                Thread.currentThread().setContextClassLoader(appClassLoader);
                ConfigResponse cr = new ConfigResponse();
                cr.setDataId(dataId);
                cr.setGroup(group);
                cr.setContent(content);
                configFilterChainManager.doFilter(null, cr);
                String contentTmp = cr.getContent();
                listener.receiveConfigInfo(contentTmp);
                // compare lastContent and content
                if (listener instanceof AbstractConfigChangeListener) {
                    Map data = ConfigChangeHandler.getInstance()
                        .parseChangeData(listenerWrap.lastContent, content, type);
                    ConfigChangeEvent event = new ConfigChangeEvent(data);
                    ((AbstractConfigChangeListener) listener).receiveConfigChange(event);
                    listenerWrap.lastContent = content;
                }
                listenerWrap.lastCallMd5 = md5;
                LOGGER.info("[{}] [notify-ok] dataId={}, group={}, md5={}, listener={} ", name, dataId, group, md5,
                    listener);
            } catch (NacosException ex) {
                LOGGER.error("[{}] [notify-error] dataId={}, group={}, md5={}, listener={} errCode={} errMsg={}",
                    name, dataId, group, md5, listener, ex.getErrCode(), ex.getErrMsg());
            } catch (Throwable t) {
                LOGGER.error("[{}] [notify-error] dataId={}, group={}, md5={}, listener={} tx={}", name, dataId,
                    group, md5, listener, t.getCause());
            } finally {
                Thread.currentThread().setContextClassLoader(myClassLoader);
            }
        }
    };
    final long startNotify = System.currentTimeMillis();
    try {
        if (null != listener.getExecutor()) {
            listener.getExecutor().execute(job);
        } else {
            job.run();
        }
    } catch (Throwable t) {
        LOGGER.error("[{}] [notify-error] dataId={}, group={}, md5={}, listener={} throwable={}", name, dataId,
            group, md5, listener, t.getCause());
    }
    final long finishNotify = System.currentTimeMillis();
    LOGGER.info("[{}] [notify-listener] time cost={}ms in ClientWorker, dataId={}, group={}, md5={}, listener={} ",
        name, (finishNotify - startNotify), dataId, group, md5, listener);
}

遍历所有的监听器,最终执行safeNotifyListener()这个方法

很显然这个方法中创建了一个Runnable任务,最终通过监听器Listener里面的线程池或者直接执行这个Runnable任务。

所以,我们接下来重点看Runnable任务执行的逻辑。

这个Runnable任务里面核心逻辑是这行代码。

 listener.receiveConfigInfo(contentTmp)

表示接收到动态修改的内容。

所以我们直接看这行代码的逻辑。

要看这行代码的逻辑,首先需要搞懂这个listener是哪个实现类,在这里先告诉大家,这个实现类是在NacosContextRefresher里面注册nacos监听器的时候创建的内部类,如果自己debug一下也能找到。

NacosContextRefresher这个类其实已经是和spring整合的桥梁了,接下来我们直接看NacosContextRefresher里面注册nacos监听器的时候创建的内部类,我们先看是怎么实现动态刷新数据的,等会再看和spring整合。

这个内部类innerReceive()方法正是实现了AbstractSharedListener这个抽象类,而AbstractSharedListener这个抽象类的顶层接口就是Listener,我们看这个内部类中的核心逻辑有两处:

①、nacosRefreshHistory通过链表的方式记录nacos数据变更历史

②、通过applicationContext.publishEvent()方法刷新容器

很显然,我们想看的逻辑正是刷新容器这行代码。

applicationContext.publishEvent()方法刷新容器的这行代码我跟着源码跟踪的很深、很久,但是我陷入了疑惑,随着我不断的跟踪spring的源码,发现这行代码的底层是spring的时间发布机制在不重启spring容器的前提下,动态的刷新容器,但是好像和nacos动态刷新加载最新的刚才传过来的数据String configInfo没有关系,也就是说,我想着从nacos服务端拿过来的数据并没有加载到spring中啊。

在这里疑惑了很久,因为跟踪源码并没有发现数据并没有加载到spring中,只是看到了在不重启spring容器的前提下,动态的刷新spring容器,所以这个时候我就猜想,动态的刷新spring容器怎么才能拿到nacos最新的数据呢,突然一个灵感,spring容器重启的时候不就是会初始化第一次从nacos拿最新的数据嘛,后续才通过nacos长轮询获取数据。

然后我在NacosConfigManager这个类的构造方法打了个debug断点,因为这个类就是spring容器启动的时候第一次初始化创建nacos需要的一些组件、开启定时任务等核心逻辑。

public NacosConfigManager(NacosConfigProperties nacosConfigProperties) {
    this.nacosConfigProperties = nacosConfigProperties;
    // Compatible with older code in NacosConfigProperties,It will be deleted in the
    // future.
    createConfigService(nacosConfigProperties);
}

果然,是进到了这个debug,也就印证了我的猜想。

三、nacos与spring整合

上面我们主要是从nacos长轮询LongPolling的核心逻辑倒推监听器怎么执行最后到和spring整合的地方,这里我们详细看下和spring整合的部分。

和spring整合首先看类NacosConfigAutoConfiguration、NacosConfigBootstrapConfiguration这两个配置类,这两个配置类主要创建了nacos的一些核心组件。

重点看类NacosConfigManager和NacosContextRefresher。

NacosConfigManager是创建nacos长轮询的定时任务,定时执行长轮询的定时任务,是nacos配置更新的启动点。

  public NacosConfigManager(NacosConfigProperties nacosConfigProperties) {
    this.nacosConfigProperties = nacosConfigProperties;
    // Compatible with older code in NacosConfigProperties,It will be deleted in the
    // future.
    createConfigService(nacosConfigProperties);
  }

NacosContextRefresher是和spring整合的桥梁,实现了ApplicationContextAware用来获取ApplicationContext,实现了ApplicationListener用来通知spring发布时间通知。

在发布事件通知的逻辑中就注册了nacos的监听器。

  @Override
  public void onApplicationEvent(ApplicationReadyEvent event) {
    // many Spring context
    if (this.ready.compareAndSet(false, true)) {
      this.registerNacosListenersForApplications();
    }
  }
  
    private void registerNacosListenersForApplications() {
    if (isRefreshEnabled()) {
      for (NacosPropertySource propertySource : NacosPropertySourceRepository
          .getAll()) {
        if (!propertySource.isRefreshable()) {
          continue;
        }
        String dataId = propertySource.getDataId();
        registerNacosListener(propertySource.getGroup(), dataId);
      }
    }
  }
  
  private void registerNacosListener(final String groupKey, final String dataKey) {
    String key = NacosPropertySourceRepository.getMapKey(dataKey, groupKey);
    Listener listener = listenerMap.computeIfAbsent(key,
        lst -> new AbstractSharedListener() {
          @Override
          public void innerReceive(String dataId, String group,
              String configInfo) {
            refreshCountIncrement();
            nacosRefreshHistory.addRefreshRecord(dataId, group, configInfo);
            // todo feature: support single refresh for listening
            applicationContext.publishEvent(
                new RefreshEvent(this, null, "Refresh Nacos config"));
            if (log.isDebugEnabled()) {
              log.debug(String.format(
                  "Refresh Nacos config group=%s,dataId=%s,configInfo=%s",
                  group, dataId, configInfo));
            }
          }
        });
    try {
      configService.addListener(dataKey, groupKey, listener);
    }
    catch (NacosException e) {
      log.warn(String.format(
          "register fail for nacos listener ,dataId=[%s],group=[%s]", dataKey,
          groupKey), e);
    }
  }

nacos注册的监听器的这个内部类正是我们刚才看的动态刷新数据通知spring发布事件通知的那个内部类。

通过这两个类就实现了spring容器启动的时候启动nacos长轮询监听服务端数据变更和监听到数据变更后动态获取最新的nacos配置数据。

个人之见,难免考虑不全,如果大家有更好的建议欢迎大家私信留言。

如果觉得对你有一点点帮助,希望能够动动小手,你的点赞是对我最大的鼓励支持。

更多分享请移步至个人公众号,谢谢支持😜😜......

公众号:wenyixicodedog  

  • 7
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值