Nacos刷新配置原理

在从服务器拉取配置时,Nacos客户端的NacosPropertySourceLocator类实现的locate()方法中会生成ConfigService实例。调用NacosConfigProperties类中的configServiceInstance()使用当前的属性返回ConfigService实例。

    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(-400, e.getMessage());
        }
    }

可以看到先加载NacosConfigService类,再以反射创建实例对象。此时会创建ServerHttpAgent实例对象用于发送http请求,与服务器交互。再创建一个ClientWorker客户端,ClientWorker会创建10ms执行一次的调度任务checkConfigInfo()。checkConfigInfo()做的事件就是检查配置源的数量有没有增加,如果增加到一定数量就开辟线程进行处理。

    public void checkConfigInfo() {
        //获取所有的配置缓存信息
        int listenerSize = cacheMap.get().size();
        // 向上取整为批数,一个线程可以处理3000=ParamUtil.getPerTaskConfigSize()
        int longingTaskCount = (int)Math.ceil(listenerSize / ParamUtil.getPerTaskConfigSize());
        if (longingTaskCount > currentLongingTaskCount) {
            //currentLongingTaskCount的默认值为0,每3000个就创建一个线程去轮询处理
            for (int i = (int)currentLongingTaskCount; i < longingTaskCount; i++) {
                // 要判断任务是否在执行 这块需要好好想想。 任务列表现在是无序的。变化过程可能有问题
                //LongPullingRunnable的run()
                executorService.execute(new LongPullingRunnable(i));
            }
            //当前配置源改变,cacheMap新增时,就在原有基础上开线程
            currentLongingTaskCount = longingTaskCount;
        }
    }

在checkConfigInfo()中,先获取cacheMap的大小再创建任务,所以接下来就得说说cacheMap这个是怎么来的了。 回到NacosConfigAutoConfiguration自动加载配置类,这个类中创建了NacosContextRefresher的Bean。NacosContextRefresher实现了ApplicationListener接口,会监听ApplicationReadyEvent事件,同时实现了ApplicationContextAware接口,可以保存应用上下文。

public class NacosContextRefresher
		implements ApplicationListener<ApplicationReadyEvent>, ApplicationContextAware

首先在prepareContext()方法加载spring上下文完成时,会为每个监听器设置应用上下文,此时NacosContextRefresher便可以保存应用上下文。

	public void contextLoaded(ConfigurableApplicationContext context) {
		for (ApplicationListener<?> listener : this.application.getListeners()) {
			if (listener instanceof ApplicationContextAware) {
				((ApplicationContextAware) listener).setApplicationContext(context);
			}
			context.addApplicationListener(listener);
		}
		this.initialMulticaster.multicastEvent(
				new ApplicationPreparedEvent(this.application, this.args, context));
	}

当应用完成启动时,就会发布ApplicationReadyEvent事件,NacosContextRefresher监听器就会处理这个事件,离cacheMap很近了。在处理事件时,先获取Nacos的所有属性源,再从属性源中获取group和dataId,然后向listenerMap注册监听器,key为dataId,value为监听器(lambda表达式),之后添加这个CacheData,到这里就已经到cacheMap了。

  1. 到cacheMap中根据key获取CacheData,key为dataId, group, tenant用+连接的字符串;同时设置taskId
        int taskId = cacheMap.get().size() / (int)ParamUtil.getPerTaskConfigSize();
        cache.setTaskId(taskId);
  1. 没有获取就生成key和CacheData准备保存到CacheDatanew CacheData(configFilterChainManager, agent.getName(), dataId, group, tenant)
  2. 为cacheMap加锁再次获取,防止有线程正在操作导致我们没有获取成功
  3. cacheMap的类型为AtomicReference,当保存的值改变时会通知其他线程,没有用ConcurrentHashMap是因为大部分时间都在读。这里先cacheMap.get()获取保存的HashMap,再把key和对应的CacheData保存到这个HashMap。
    创建了CacheData后,把刚刚创建的lambda表达式定义的监听器添加到CacheData中。

回到LongPullingRunnable的run()方法,可以看到先获取所有缓存的CacheData,每个CacheData中的taskId都是除3000得来的,而线程的taskId也是除3000得来的,所有每个线程可以处理3000个CacheData。

                for (CacheData cacheData : cacheMap.get().values()) {
                    if (cacheData.getTaskId() == taskId) {
                        cacheDatas.add(cacheData);
                        try {
                            checkLocalConfig(cacheData);
                            if (cacheData.isUseLocalConfigInfo()) {
                                cacheData.checkListenerMd5();
                            }
                        } catch (Exception e) {
                            log.error("NACOS-CLIENT", "get local config info error", e);
                        }
                    }
                }

优先从本地配置文件中获取配置,检查本地配置文件是否变动。注意这里的本地配置文件不是从服务器上下载的配置文件。

private void checkLocalConfig(CacheData cacheData) {
    final String dataId = cacheData.dataId;
    final String group = cacheData.group;
    final String tenant = cacheData.tenant;
    File path = LocalConfigInfoProcessor.getFailoverFile(agent.getName(), dataId, group, tenant);

    // 没有 -> 有
    //创建cacheData时没有使用本地配置,当获取到服务器的配置源之后,就会生成path
    if (!cacheData.isUseLocalConfigInfo() && path.exists()) {
        String content = LocalConfigInfoProcessor.getFailover(agent.getName(), dataId, group, tenant);
        String md5 = MD5.getInstance().getMD5String(content);
        cacheData.setUseLocalConfigInfo(true);
        cacheData.setLocalConfigInfoVersion(path.lastModified());
        cacheData.setContent(content);

        log.warn(agent.getName(),
            "[failover-change] failover file created. dataId={}, group={}, tenant={}, md5={}, content={}",
            dataId, group, tenant, md5, ContentUtils.truncateContent(content));
        return;
    }

    // 有 -> 没有。不通知业务监听器,从server拿到配置后通知。
    //如果path不存在了,说明不能使用本地的配置文件了,改成false
    if (cacheData.isUseLocalConfigInfo() && !path.exists()) {
        cacheData.setUseLocalConfigInfo(false);
        log.warn(agent.getName(), "[failover-change] failover file deleted. dataId={}, group={}, tenant={}", dataId,
            group, tenant);
        return;
    }

    // 有变更
    //如果本地的配置文件发生变动,就从本地加载配置文件
    if (cacheData.isUseLocalConfigInfo() && path.exists()
        && cacheData.getLocalConfigInfoVersion() != path.lastModified()) {
        String content = LocalConfigInfoProcessor.getFailover(agent.getName(), dataId, group, tenant);
        String md5 = MD5.getInstance().getMD5String(content);
        cacheData.setUseLocalConfigInfo(true);
        cacheData.setLocalConfigInfoVersion(path.lastModified());
        cacheData.setContent(content);
        log.warn(agent.getName(),
            "[failover-change] failover file changed. dataId={}, group={}, tenant={}, md5={}, content={}",
            dataId, group, tenant, md5, ContentUtils.truncateContent(content));
        return;
    }
}

接下来到服务器中获取配置源。首先如果是第一次初始化需要到服务器获取一次配置源,将这些cacheData的参数缓存下来批量获取。向服务器发post请求,服务器返回了也能说明当前的服务器是健康正在运行的,将服务器返回的变化的dataId和group保存下来。然后再根据dataId、group、tenant到服务器获取配置源内容,将配置内容和内容的md5写到CacheData中。

for (String dataIdAndGroup : response.split(LINE_SEPARATOR)) {
    if (!StringUtils.isBlank(dataIdAndGroup)) {
        String[] keyArr = dataIdAndGroup.split(WORD_SEPARATOR);
        String dataId = keyArr[0];
        String group = keyArr[1];
        if (keyArr.length == 2) {
            updateList.add(GroupKey.getKey(dataId, group));
            log.info(agent.getName(), "[polling-resp] config changed. dataId={}, group={}", dataId, group);
        } else if (keyArr.length == 3) {
            String tenant = keyArr[2];
            updateList.add(GroupKey.getKeyTenant(dataId, group, tenant));
            log.info(agent.getName(), "[polling-resp] config changed. dataId={}, group={}, tenant={}", dataId,
                group, tenant);
        } else {
            log.error(agent.getName(), "NACOS-XXXX", "[polling-resp] invalid dataIdAndGroup error",
                dataIdAndGroup);
        }
    }
}

如果检测到配置变化了cacheData.checkListenerMd5()则通知所有的监听器,调用每个监听器的receiveConfigInfo(),即lambda表达式。

void checkListenerMd5() {
    for (ManagerListenerWrap wrap : listeners) {
        if (!md5.equals(wrap.lastCallMd5)) {
            safeNotifyListener(dataId, group, content, md5, wrap);
        }
    }
}

这时会调用lambda表达式处理配置文件的变化。

public void receiveConfigInfo(String configInfo) {
    //设置加载次数,每次变化都会新增
    loadCount.incrementAndGet();
    String md5 = "";
    if (!StringUtils.isEmpty(configInfo)) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            md5 = new BigInteger(1, md.digest(configInfo.getBytes("UTF-8")))
                    .toString(16);
        }
        catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
            LOGGER.warn("[Nacos] unable to get md5 for dataId: " + dataId, e);
        }
    }
    //记录当前dataId对应的配置源的改变
    refreshHistory.add(dataId, md5);
    //发布RefreshEvent事件
    applicationContext.publishEvent(
            new RefreshEvent(this, null, "Refresh Nacos config"));
    if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("Refresh Nacos config group{},dataId{}", group, dataId);
    }
}

spring启动时会创建RefreshEventListener监听器,内部的handle方法标注了@EventListener,因此能够处理RefreshEvent刷新事件。

	@EventListener
	public void handle(RefreshEvent event) {
		if (this.ready.get()) { // don't handle events before app is ready
			log.debug("Event received " + event.getEventDesc());
			Set<String> keys = this.refresh.refresh();
			log.info("Refresh keys changed: " + keys);
		}
	}

在refresh()方法中先获取老的属性,使用当前的环境environment和空的Empty.class运行spring应用上下文,再获取新的配置addConfigFilesToEnvironment()。然后将新的属性和老的属性进行比较,如果发现不一样就用新的属性替换老的属性。

ConfigurableApplicationContext addConfigFilesToEnvironment() {
    ConfigurableApplicationContext capture = null;
    try {
        //复制当前环境
        StandardEnvironment environment = copyEnvironment(
                this.context.getEnvironment());
        //使用当前环境和空类运行一个新的应用上下文        
        SpringApplicationBuilder builder = new SpringApplicationBuilder(Empty.class)
                .bannerMode(Mode.OFF).web(WebApplicationType.NONE)
                .environment(environment);
        // Just the listeners that affect the environment (e.g. excluding logging
        // listener because it has side effects)
        builder.application()
                .setListeners(Arrays.asList(new BootstrapApplicationListener(),
                        new ConfigFileApplicationListener()));
        //运行spring应用上下文                
        capture = builder.run();
        if (environment.getPropertySources().contains(REFRESH_ARGS_PROPERTY_SOURCE)) {
            environment.getPropertySources().remove(REFRESH_ARGS_PROPERTY_SOURCE);
        }
        //获取当前应用上下文属性源
        MutablePropertySources target = this.context.getEnvironment()
                .getPropertySources();
        String targetName = null;
        //遍历新的应用上下文的属性源
        for (PropertySource<?> source : environment.getPropertySources()) {
            String name = source.getName();
            if (target.contains(name)) {
                targetName = name;
            }
            if (!this.standardSources.contains(name)) {
                //如果原来就有这个属性源,那么就用新的属性源进行替换
                if (target.contains(name)) {
                    target.replace(name, source);
                }
                else {
                    if (targetName != null) {
                        //如果原来没有这个属性源,将新得到的属性源加到原来的应用上下文
                        target.addAfter(targetName, source);
                    }
                    else {
                        // targetName was null so we are at the start of the list
                        target.addFirst(source);
                        targetName = name;
                    }
                }
            }
        }
    }
    finally {
        ConfigurableApplicationContext closeable = capture;
        while (closeable != null) {
            try {
                closeable.close();
            }
            catch (Exception e) {
                // Ignore;
            }
            if (closeable.getParent() instanceof ConfigurableApplicationContext) {
                closeable = (ConfigurableApplicationContext) closeable.getParent();
            }
            else {
                break;
            }
        }
    }
    return capture;
}

获取变化了的属性源的key集合,发布一个EnvironmentChangeEvent事件,之后再this.scope.refreshAll()将配置刷到使用了@RefreshScope注解的Bean中。

private Map<String, Object> changes(Map<String, Object> before,
        Map<String, Object> after) {
    Map<String, Object> result = new HashMap<String, Object>();
    for (String key : before.keySet()) {
        if (!after.containsKey(key)) {
            result.put(key, null);
        }
        else if (!equal(before.get(key), after.get(key))) {
            result.put(key, after.get(key));
        }
    }
    for (String key : after.keySet()) {
        if (!before.containsKey(key)) {
            result.put(key, after.get(key));
        }
    }
    return result;
}

ConfigurationPropertiesRebinder监听器负责处理EnvironmentChangeEvent事件,在处理事件时会刷新所有的配置源Bean(@ConfigurationProperties注解Bean),刷新的方法是先销毁这些属性源Bean,再重新创建。之后再看@RefreshScope注解标注的Bean的属性怎么获取的新的配置。

你可能感兴趣的:(分布式框架,java,spring,开发语言)