nacos配置中心的核心原理


前言

前序文章介绍了nacos配置中心的基本应用和进阶应用,为了更流畅的应用,本文将介绍nacos配置中心的基本原理。


一、nacos变更监听

1、客户端

每个版本的nacos代码都有一定的差异点,但整体架构是不变的,本文就1.4.2版本进行解读

		<dependency>
    		<groupId>com.alibaba.nacos</groupId>
    		<artifactId>nacos-client</artifactId>
    		<version>1.4.2</version>
		</dependency>
        <dependency>
            <groupId>com.alibaba.boot</groupId>
            <artifactId>nacos-config-spring-boot-starter</artifactId>
            <version>0.2.12</version>
        </dependency>

1.1、自动装配

从springboot的自动装配出发,nacos-starter下面的spring.factories

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.alibaba.boot.nacos.config.autoconfigure.NacosConfigAutoConfiguration
#org.springframework.context.ApplicationContextInitializer=\
#  com.alibaba.boot.nacos.config.autoconfigure.NacosConfigApplicationContextInitializer
# 环境变量后置处理器
org.springframework.boot.env.EnvironmentPostProcessor=\
  com.alibaba.boot.nacos.config.autoconfigure.NacosConfigEnvironmentProcessor,\
  com.alibaba.boot.nacos.config.support.MultiProfilesYamlConfigParseSupport
org.springframework.context.ApplicationListener=\
  com.alibaba.boot.nacos.config.logging.NacosLoggingListener

通过后置处理器NacosConfigEnvironmentProcessor,往spring里添加nacos初始化器

	@Override
	public void postProcessEnvironment(ConfigurableEnvironment environment,
			SpringApplication application) {
		// spring里添加nacos初始化器
		application.addInitializers(new NacosConfigApplicationContextInitializer(this));
		// 加载nacos元数据
		nacosConfigProperties = NacosConfigPropertiesUtils
				.buildNacosConfigProperties(environment);
		if (enable()) {
			System.out.println(
					"[Nacos Config Boot] : The preload log configuration is enabled");
			loadConfig(environment);
			NacosConfigLoader nacosConfigLoader = NacosConfigLoaderFactory.getSingleton(nacosConfigProperties, environment, builder);
			LogAutoFreshProcess.build(environment, nacosConfigProperties, nacosConfigLoader, builder).process();
		}
	}

接下来初始化器NacosConfigApplicationContextInitializer开始初始化

	// NacosConfigApplicationContextInitializer
	public void initialize(ConfigurableApplicationContext context) {
		singleton.setApplicationContext(context);
		environment = context.getEnvironment();
		nacosConfigProperties = NacosConfigPropertiesUtils
				.buildNacosConfigProperties(environment);
		final NacosConfigLoader configLoader = NacosConfigLoaderFactory.getSingleton(
				nacosConfigProperties, environment, builder);
		if (!enable()) {
			logger.info("[Nacos Config Boot] : The preload configuration is not enabled");
		}
		else {

			// If it opens the log level loading directly will cache
			// DeferNacosPropertySource release

			if (processor.enable()) {
				processor.publishDeferService(context);
				configLoader
						.addListenerIfAutoRefreshed(processor.getDeferPropertySources());
			}
			else {
				// 远程访问nacos配置中心读取配置数据
				configLoader.loadConfig();
				// 设置监听器来监听nacos配置中心数据的变更并更新到本地
				configLoader.addListenerIfAutoRefreshed();
			}
		}

		final ConfigurableListableBeanFactory factory = context.getBeanFactory();
		if (!factory
				.containsSingleton(NacosBeanUtils.GLOBAL_NACOS_PROPERTIES_BEAN_NAME)) {
			factory.registerSingleton(NacosBeanUtils.GLOBAL_NACOS_PROPERTIES_BEAN_NAME,
					configLoader.getGlobalProperties());
		}
	}
	

loadConfig

	//	NacosConfigLoader
	public void loadConfig() {
        MutablePropertySources mutablePropertySources = environment.getPropertySources();
        List<NacosPropertySource> sources = reqGlobalNacosConfig(globalProperties,
                nacosConfigProperties.getType());
        for (NacosConfigProperties.Config config : nacosConfigProperties.getExtConfig()) {
            List<NacosPropertySource> elements = reqSubNacosConfig(config,
                    globalProperties, config.getType());
            sources.addAll(elements);
        }
       // 如果远程nacos的配置数据比本地配置数据的优先级高,则执行以下方法
        if (nacosConfigProperties.isRemoteFirst()) {
            for (ListIterator<NacosPropertySource> itr = sources.listIterator(sources.size()); itr.hasPrevious();) {
                // 这里是个关键点,可以确保远程的配置数据会被优先使用
                mutablePropertySources.addAfter(
                        StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, itr.previous());
            }
        } else {
            for (NacosPropertySource propertySource : sources) {
                mutablePropertySources.addLast(propertySource);
            }
        }
    }

addListenerIfAutoRefreshed

    // 添加nacos配置中心监听器
    public void addListenerIfAutoRefreshed() {
        addListenerIfAutoRefreshed(nacosPropertySources);
    }

    public void addListenerIfAutoRefreshed(
            final List<DeferNacosPropertySource> deferNacosPropertySources) {
        for (DeferNacosPropertySource deferNacosPropertySource : deferNacosPropertySources) {
            NacosPropertySourcePostProcessor.addListenerIfAutoRefreshed(
                    deferNacosPropertySource.getNacosPropertySource(),
                    deferNacosPropertySource.getProperties(),
                    deferNacosPropertySource.getEnvironment());
        }
    }
    
    public static void addListenerIfAutoRefreshed(final NacosPropertySource nacosPropertySource, Properties properties, final ConfigurableEnvironment environment) {
        if (nacosPropertySource.isAutoRefreshed()) {
            final String dataId = nacosPropertySource.getDataId();
            final String groupId = nacosPropertySource.getGroupId();
            final String type = nacosPropertySource.getType();
            NacosServiceFactory nacosServiceFactory = NacosBeanUtils.getNacosServiceFactoryBean(beanFactory);

            try {
            	// 创建configService 
                ConfigService configService = nacosServiceFactory.createConfigService(properties);
                Listener listener = new AbstractListener() {
                    public void receiveConfigInfo(String config) {
                        String name = nacosPropertySource.getName();
                        NacosPropertySource newNacosPropertySource = new NacosPropertySource(dataId, groupId, name, config, type);
                        newNacosPropertySource.copy(nacosPropertySource);
                        MutablePropertySources propertySources = environment.getPropertySources();
                        propertySources.replace(name, newNacosPropertySource);
                    }
                };
                if (configService instanceof EventPublishingConfigService) {
                    ((EventPublishingConfigService)configService).addListener(dataId, groupId, type, listener);
                } else {
                    configService.addListener(dataId, groupId, listener);
                }

            } catch (NacosException var9) {
                throw new RuntimeException("ConfigService can't add Listener with properties : " + properties, var9);
            }
        }
    }

nacosServiceFactory.createConfigService(properties)

    public ConfigService createConfigService(Properties properties) throws NacosException {
        Properties copy = new Properties();
        copy.putAll(properties);
        // 走策略模式返回相应的ConfigService,这里是ConfigCreateWorker
        return (ConfigService)
((AbstractCreateWorker)this.createWorkerManager.get(CacheableEventPublishingNacosServiceFactory.ServiceType.CONFIG)).run(copy, (Object)null);
    }
        class ConfigCreateWorker extends AbstractCreateWorker<ConfigService> {
        ConfigCreateWorker() {
        }
		// ConfigCreateWorker
        public ConfigService run(Properties properties, ConfigService service) throws NacosException {
            String cacheKey = NacosUtils.identify(properties);
			// 从缓存获取,因为是第一次进来,所有不存在
            ConfigService configService = (ConfigService)CacheableEventPublishingNacosServiceFactory.this.configServicesCache.get(cacheKey);
            if (configService == null) {
                if (service == null) {
                	// 创建ConfigService 
                    service = NacosFactory.createConfigService(properties);
                }

                configService = new EventPublishingConfigService(service, properties, CacheableEventPublishingNacosServiceFactory.getSingleton().context, CacheableEventPublishingNacosServiceFactory.getSingleton().nacosConfigListenerExecutor);
                CacheableEventPublishingNacosServiceFactory.this.configServicesCache.put(cacheKey, configService);
            }

            return (ConfigService)configService;
        }
    }

NacosFactory.createConfigService(properties),通过配置信息创建ConfigService

public class ConfigFactory {

    /**
     * Create Config
     *
     * @param properties init param
     * @return ConfigService
     * @throws NacosException Exception
     */
    public static ConfigService createConfigService(Properties properties) throws NacosException {
        try {
            Class<?> driverImplClass = Class.forName("com.alibaba.nacos.client.config.NacosConfigService");
            // 获取带Properties参数的构造函数
            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);
        }
    }
}

创建的configService为NacosConfigService

public class NacosConfigService implements ConfigService {
    
    private static final long POST_TIMEOUT = 3000L;
    private static final String EMPTY = "";

    // Http请求代理
    private HttpAgent agent;
    
    // 长轮询
    private ClientWorker worker;
    private String namespace;
    private String encode;
    
	// 创建一个 配置过滤器链管理器
    private ConfigFilterChainManager configFilterChainManager = new ConfigFilterChainManager();
    
    public NacosConfigService(Properties properties) throws NacosException {
        String encodeTmp = properties.getProperty(PropertyKeyConst.ENCODE);
        if (StringUtils.isBlank(encodeTmp)) {
            encode = Constants.ENCODE;
        } else {
            encode = encodeTmp.trim();
        }
        
        // 初始化 namespace
        initNamespace(properties);
        
        // 初始化一个HttpAgent,这里又用到了装饰起模式,实际工作的类是ServerHttpAgent,
		// MetricsHttpAgent内部也是调用了ServerHttpAgent的方法,增加了监控统计的信息
        agent = new MetricsHttpAgent(new ServerHttpAgent(properties));
        agent.start();
        
        // 创建客户端工作对象,结合agent实现长轮询机制
        worker = new ClientWorker(agent, configFilterChainManager, properties);
    }
}

1.2 、ClientWorker

    public ClientWorker(final HttpAgent agent, ConfigFilterChainManager configFilterChainManager, Properties properties) {
        this.agent = agent;
        this.configFilterChainManager = configFilterChainManager;
        // Initialize the timeout parameter
        this.init(properties);
        // 初始化一个定时调度的线程池,Worker执行器
        this.executor = Executors.newScheduledThreadPool(1, new ThreadFactory() {
            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() {
            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;
            }
        });
        // 设置定时任务的执行频率,执行延迟时间为1毫秒、延迟时间为10毫秒,调用checkConfigInfo这个方法,定时去检测配置是否发生了变化
        this.executor.scheduleWithFixedDelay(new Runnable() {
            public void run() {
                try {
                    ClientWorker.this.checkConfigInfo();
                } catch (Throwable var2) {
                    ClientWorker.LOGGER.error("[" + agent.getName() + "] [sub-check] rotate check error", var2);
                }

            }
        }, 1L, 10L, TimeUnit.MILLISECONDS);
    }
    private static double perTaskConfigSize = 3000.0;
	// 配置信息检查
	public void checkConfigInfo() {
        // 分任:分批处理
        int listenerSize = cacheMap.get().size();
        // 向上取整为批数,监听的配置数量除以3000,得到一个整数,代表长轮训任务的数量
        int longingTaskCount = (int) Math.ceil(listenerSize / ParamUtil.getPerTaskConfigSize());
        if (longingTaskCount > currentLongingTaskCount) {
            for (int i = (int) currentLongingTaskCount; i < longingTaskCount; i++) {
                // 长轮询, i 为当前批次,用于筛选过滤出属于当前批次的cacheData
                executorService.execute(new LongPollingRunnable(i));
            }
            currentLongingTaskCount = longingTaskCount;
        }
    }

1.3、 长轮询实现机制

LongPollingRunnable

  • 对任务按照批次分类
  • 检查当前批次的缓存和本地文件的数据是否一致,如果发生了变化,则触发监听。
class LongPollingRunnable implements Runnable {
		// 当前批次id,用于筛选过滤出属于当前批次的cacheData
        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 local config
               	// 获取属于当前批次的cacheData
                for (CacheData cacheData : cacheMap.get().values()) {
                    if (cacheData.getTaskId() == taskId) {
                        cacheDatas.add(cacheData);
                        try {
                        	// 检查本地配置
                            checkLocalConfig(cacheData);
                            if (cacheData.isUseLocalConfigInfo()) { // 使用本地配置信息
                            	// 检查cacheData和内存缓存文件是否不一致,如果不一致,通知所有Listener
                                cacheData.checkListenerMd5();
                            }
                        } catch (Exception e) {
                            LOGGER.error("get local config info error", e);
                        }
                    }
                }

                // check server config
                // 长轮询:将当前批次的所有cacheData通过Http请求发送给服务端,并附带30s超时时间
                // 1.服务端数据无变化,请求超时,changedGroupKeys = Collections.emptyList()
                // 2.服务端数据存在变更,循环遍历,通过getServerConfig获取并更新本地缓存,触发事件监听
                List<String> changedGroupKeys = checkUpdateDataIds(cacheDatas, inInitializingCacheList);

				// 遍历发送变更的groupKey 
                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);
                    }
                }
                
                // 遍历cacheDatas,找到发生变化的数据进行通知
                for (CacheData cacheData : cacheDatas) {
                    if (!cacheData.isInitializing() || inInitializingCacheList
                        .contains(GroupKey.getKeyTenant(cacheData.dataId, cacheData.group, cacheData.tenant))) {
                        // 检查cacheData和内存缓存文件是否不一致,如果不一致,通知所有Listener
                        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);
                // 如果发生异常,延迟taskPenaltyTime后执行当前任务
                executorService.schedule(this, taskPenaltyTime, TimeUnit.MILLISECONDS);
            }
        }
    }
1.3.1、check local config – 检查本地配置

1、是否使用本地配置checkLocalConfig
2、检查变更并通知checkListenerMd5

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);
    // 没有 -> 有
    if (!cacheData.isUseLocalConfigInfo() && path.exists()) {
        String content = LocalConfigInfoProcessor.getFailover(agent.getName(), dataId, group, tenant);
        final String md5 = MD5Utils.md5Hex(content, Constants.ENCODE);
        cacheData.setUseLocalConfigInfo(true);
        cacheData.setLocalConfigInfoVersion(path.lastModified());
        cacheData.setContent(content);
        String encryptedDataKey = LocalEncryptedDataKeyProcessor
                .getEncryptDataKeyFailover(agent.getName(), dataId, group, tenant);
        cacheData.setEncryptedDataKey(encryptedDataKey);
        
        LOGGER.warn(
                "[{}] [failover-change] failover file created. dataId={}, group={}, tenant={}, md5={}, content={}",
                agent.getName(), dataId, group, tenant, md5, ContentUtils.truncateContent(content));
        return;
    }
     // 有 -> 没有。不通知业务监听器,从server拿到配置后通知。
    // If use local config info, then it doesn't notify business listener and notify after getting from server.
    if (cacheData.isUseLocalConfigInfo() && !path.exists()) {
        cacheData.setUseLocalConfigInfo(false);
        LOGGER.warn("[{}] [failover-change] failover file deleted. dataId={}, group={}, tenant={}", agent.getName(),
                dataId, group, tenant);
        return;
    }
    
     // 有变更
    if (cacheData.isUseLocalConfigInfo() && path.exists() && cacheData.getLocalConfigInfoVersion() != path
            .lastModified()) {
        String content = LocalConfigInfoProcessor.getFailover(agent.getName(), dataId, group, tenant);
        final String md5 = MD5Utils.md5Hex(content, Constants.ENCODE);
        cacheData.setUseLocalConfigInfo(true);
        cacheData.setLocalConfigInfoVersion(path.lastModified());
        cacheData.setContent(content);
        String encryptedDataKey = LocalEncryptedDataKeyProcessor
                .getEncryptDataKeyFailover(agent.getName(), dataId, group, tenant);
        cacheData.setEncryptedDataKey(encryptedDataKey);
        LOGGER.warn(
                "[{}] [failover-change] failover file changed. dataId={}, group={}, tenant={}, md5={}, content={}",
                agent.getName(), dataId, group, tenant, md5, ContentUtils.truncateContent(content));
    }
}
// 遍历用户自己添加的监听器,如果发现数据的md5值不同,则发送通知
	void checkListenerMd5() {
        for (ManagerListenerWrap wrap : listeners) {
            if (!md5.equals(wrap.lastCallMd5)) {
                safeNotifyListener(dataId, group, content, md5, wrap);
            }
        }
    }
1.3.2、check server config 检查服务端配置

checkUpdateDataIds
向服务器端发起检查请求,判断自己本地的配置和服务端的配置是否一致

	/**
     * 从Server获取值变化了的DataID列表。返回的对象里只有dataId和group是有效的。 保证不返回NULL。
     */
    List<String> checkUpdateDataIds(List<CacheData> cacheDatas, List<String> inInitializingCacheList) throws IOException {
        StringBuilder sb = new StringBuilder();
        // 把需要检查的配置项,拼接成一个字符串
        for (CacheData cacheData : cacheDatas) {
            if (!cacheData.isUseLocalConfigInfo()) {
                sb.append(cacheData.dataId).append(WORD_SEPARATOR);
                sb.append(cacheData.group).append(WORD_SEPARATOR);
                if (StringUtils.isBlank(cacheData.tenant)) {
                    sb.append(cacheData.getMd5()).append(LINE_SEPARATOR);
                } else {
                    sb.append(cacheData.getMd5()).append(WORD_SEPARATOR);
                    sb.append(cacheData.getTenant()).append(LINE_SEPARATOR);
                }
                if (cacheData.isInitializing()) {
                    // cacheData 首次出现在cacheMap中&首次check更新
                    inInitializingCacheList
                        .add(GroupKey.getKeyTenant(cacheData.dataId, cacheData.group, cacheData.tenant));
                }
            }
        }
        boolean isInitializingCacheList = !inInitializingCacheList.isEmpty();
        // 检查更新配置字符串
        return checkUpdateConfigStr(sb.toString(), isInitializingCacheList);
    }

checkUpdateConfigStr
从Server获取值变化了的DataID列表。返回的对象里只有dataId和group是有效的。 保证不返回NULL

	/**
     * 从Server获取值变化了的DataID列表。返回的对象里只有dataId和group是有效的。 保证不返回NULL。
     */
    List<String> checkUpdateConfigStr(String probeUpdateString, boolean isInitializingCacheList) throws IOException {

        List<String> params = Arrays.asList(Constants.PROBE_MODIFY_REQUEST, probeUpdateString);

        List<String> headers = new ArrayList<String>(2);
        headers.add("Long-Pulling-Timeout");
        // 设置超时时间,默认30s
        headers.add("" + timeout);

        // told server do not hang me up if new initializing cacheData added in
        // 是否初始化缓存列表
        if (isInitializingCacheList) {
            headers.add("Long-Pulling-Timeout-No-Hangup");
            headers.add("true");
        }

		// 判断可能发生变更的字符串是否为空,如果是,则直接返回
        if (StringUtils.isBlank(probeUpdateString)) {
            return Collections.emptyList();
        }

        try {
        	// 发送带超时时间的Http请求,请求路径:/v1/cs/configs/listener
            HttpResult result = agent.httpPost(Constants.CONFIG_CONTROLLER_PATH + "/listener", headers, params,
                agent.getEncode(), timeout);

            if (HttpURLConnection.HTTP_OK == result.code) {
                setHealthServer(true);
                // 解析更新数据 ID 响应
                return parseUpdateDataIdResponse(result.content);
            } else {
                setHealthServer(false);
                LOGGER.error("[{}] [check-update] get changed dataId error, code: {}", agent.getName(), result.code);
            }
        } catch (IOException e) {
            setHealthServer(false);
            LOGGER.error("[" + agent.getName() + "] [check-update] get changed dataId exception", e);
            throw e;
        }
        // 超时返回 Collections.emptyList()
        return Collections.emptyList();
    }

2、服务端

2.1、处理客户端长轮询的请求

通过前面的分析可以得到,客户端会发送一个 /v1/cs/configs/listener 的请求

@Controller
// Constants.CONFIG_CONTROLLER_PATH = /v1/cs/configs
@RequestMapping(Constants.CONFIG_CONTROLLER_PATH)
public class ConfigController {

    /**
     * 比较MD5
     */
	@RequestMapping(value = "/listener", method = RequestMethod.POST)
    public void listener(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
        request.setAttribute("org.apache.catalina.ASYNC_SUPPORTED", true);
        // 获取需要比较的字符串
        String probeModify = request.getParameter("Listening-Configs");
        if (StringUtils.isBlank(probeModify)) {
            throw new IllegalArgumentException("invalid probeModify");
        }

		// 解码
        probeModify = URLDecoder.decode(probeModify, Constants.ENCODE);

        // key -> groupKey  value -> md5
        Map<String, String> clientMd5Map;
        try {
        	// 获取客户端传输过来的md5值
            clientMd5Map = MD5Util.getClientMd5Map(probeModify);
        } catch (Throwable e) {
            throw new IllegalArgumentException("invalid probeModify");
        }

        // do long-polling
        // 长轮询
        inner.doPollingConfig(request, response, clientMd5Map, probeModify.length());
    }
}

ConfigServletInner.doPollingConfig
这个方法主要是用来做长轮训和短轮询的判断

  • 如果是长轮训,直接走addLongPollingClient方法
  • 如果是短轮询,直接比较服务端的数据,如果存在md5不一致,直接把数据返回。
@Service
public class ConfigServletInner {

	/**
     * 轮询接口
     */
    public String doPollingConfig(HttpServletRequest request, HttpServletResponse response,
                                  Map<String, String> clientMd5Map, int probeRequestSize)
        throws IOException, ServletException {

        // 长轮询
        if (LongPollingService.isSupportLongPolling(request)) {
        	// 添加长轮询客户端
            longPollingService.addLongPollingClient(request, response, clientMd5Map, probeRequestSize);
            return HttpServletResponse.SC_OK + "";
        }

        // else 兼容短轮询逻辑
        List<String> changedGroups = MD5Util.compareMd5(request, response, clientMd5Map);

        // 兼容短轮询result
        String oldResult = MD5Util.compareMd5OldResult(changedGroups);
        String newResult = MD5Util.compareMd5ResultString(changedGroups);

        String version = request.getHeader(Constants.CLIENT_VERSION_HEADER);
        if (version == null) {
            version = "2.0.0";
        }
        int versionNum = Protocol.getVersionNumber(version);

        /**
         * 2.0.4版本以前, 返回值放入header中
         */
        if (versionNum < START_LONGPOLLING_VERSION_NUM) {
            response.addHeader(Constants.PROBE_MODIFY_RESPONSE, oldResult);
            response.addHeader(Constants.PROBE_MODIFY_RESPONSE_NEW, newResult);
        } else {
            request.setAttribute("content", newResult);
        }

        // 禁用缓存
        response.setHeader("Pragma", "no-cache");
        response.setDateHeader("Expires", 0);
        response.setHeader("Cache-Control", "no-cache,no-store");
        response.setStatus(HttpServletResponse.SC_OK);
        return HttpServletResponse.SC_OK + "";
    }
}

LongPollingService.addLongPollingClient

  • 获得客户端传递过来的超时时间,并且进行本地计算,提前500ms返回响应,这就能解释为什么客户端响应超时时间是29.5+了。如果 isFixedPolling=true ,不会提前返回响应
  • md5比较,如果不一致,通过 generateResponse 将结果返回
  • 如果配置文件没有发生变化,则通过 scheduler.execute 启动了一个定时任务,将客户端的长轮询请求封装成一个叫 ClientLongPolling 的任务,交给 scheduler 去执行
public void addLongPollingClient(HttpServletRequest req, HttpServletResponse rsp, Map<String, String> clientMd5Map,
        int probeRequestSize) {
    // 获取客户端长轮训的超时时间
    String str = req.getHeader(LongPollingService.LONG_POLLING_HEADER); 
    // 不允许断开的标记
    String noHangUpFlag = req.getHeader(LongPollingService.LONG_POLLING_NO_HANG_UP_HEADER);
    // 应用名称
    String appName = req.getHeader(RequestUtil.CLIENT_APPNAME_HEADER);
    String tag = req.getHeader("Vipserver-Tag");
    // 延期时间,默认为500ms
    int delayTime = SwitchService.getSwitchInteger(SwitchService.FIXED_DELAY_TIME, 500);

    // Add delay time for LoadBalance, and one response is returned 500 ms in advance to avoid client timeout.
    // 提前500ms返回一个响应,避免客户端出现超时
    long timeout = Math.max(10000, Long.parseLong(str) - delayTime);
    if (isFixedPolling()) {
    	// Do nothing but set fix polling timeout.
        timeout = Math.max(10000, getFixedPollingInterval());
    } else {
        long start = System.currentTimeMillis();
        // 通过md5判断客户端请求过来的key是否有和服务器端有不一致的,如果有,则保存到changedGroups中。
        List<String> changedGroups = MD5Util.compareMd5(req, rsp, clientMd5Map);
        if (changedGroups.size() > 0) { 
            // 如果发现有变更,则直接把请求返回给客户端
            generateResponse(req, rsp, changedGroups);
            LogUtil.CLIENT_LOG.info("{}|{}|{}|{}|{}|{}|{}", System.currentTimeMillis() - start, "instant",
                    RequestUtil.getRemoteIp(req), "polling", clientMd5Map.size(), probeRequestSize,
                    changedGroups.size());
            return;
        } else if (noHangUpFlag != null && noHangUpFlag.equalsIgnoreCase(TRUE_STR)) { 
            // 如果noHangUpFlag为true,说明不需要挂起客户端,所以直接返回。
            LogUtil.CLIENT_LOG.info("{}|{}|{}|{}|{}|{}|{}", System.currentTimeMillis() - start, "nohangup",
                    RequestUtil.getRemoteIp(req), "polling", clientMd5Map.size(), probeRequestSize,
                    changedGroups.size());
            return;
        }
    }
    // 获取请求端的ip
    String ip = RequestUtil.getRemoteIp(req);

    // Must be called by http thread, or send response.
    // 把当前请求转化为一个异步请求(意味着此时tomcat线程被释放,也就是客户端的请求,需要通过asyncContext来手动触发返回,否则一直挂起)
    // AsyncContext是Servlet3.0中提供的对象,调用startAsync获得AsyncContext对象之后,这个请求的响应会被延后,并释放容器分配的线程。
    final AsyncContext asyncContext = req.startAsync();
    // AsyncContext.setTimeout() is incorrect, Control by oneself
    // 设置异步请求超时时间,0表示自己控制
    asyncContext.setTimeout(0L); 
    // 执行长轮训请求
    ConfigExecutor.executeLongPolling(new ClientLongPolling(asyncContext, clientMd5Map, ip, probeRequestSize, timeout, appName, tag));
}

ClientLongPolling

  • LongPollingService 的内部类
  • 通过scheduler.schedule实现了一个定时任务,它的delay时间正好是前面计算的29.5s
  • 如果在29.5s之内,数据发生变化,需要提前通知。allSubs 和发布订阅有关系,订阅了数据变化的事件
class ClientLongPolling implements Runnable {

    @Override
    public void run() {
        // 构建一个异步任务,延后29.5s执行
        asyncTimeoutFuture = ConfigExecutor.scheduleLongPolling(new Runnable() {       
            @Override
            public void run() { 
                try {
                	// 获取并设置客户端IP
                    getRetainIps().put(ClientLongPolling.this.ip, System.currentTimeMillis());

                    // 移除订阅关系
                    allSubs.remove(ClientLongPolling.this); 
					// 如果是固定间隔的长轮询
                    if (isFixedPolling()) { 
                        LogUtil.CLIENT_LOG
                                .info("{}|{}|{}|{}|{}|{}", (System.currentTimeMillis() - createTime), "fix",
                                        RequestUtil.getRemoteIp((HttpServletRequest) asyncContext.getRequest()),
                                        "polling", clientMd5Map.size(), probeRequestSize);
                        // 通过md5值,获取当前所有变更的groups
                        List<String> changedGroups = MD5Util
                                .compareMd5((HttpServletRequest) asyncContext.getRequest(),
                                        (HttpServletResponse) asyncContext.getResponse(), clientMd5Map);
                        if (changedGroups.size() > 0) {
                       		// 如果大于0,表示有变更,直接响应
                            sendResponse(changedGroups);
                        } else {
                       	    // 否则返回null
                            sendResponse(null); 
                        }
                    } else {
                        LogUtil.CLIENT_LOG
                                .info("{}|{}|{}|{}|{}|{}", (System.currentTimeMillis() - createTime), "timeout",
                                        RequestUtil.getRemoteIp((HttpServletRequest) asyncContext.getRequest()),
                                        "polling", clientMd5Map.size(), probeRequestSize);
                        sendResponse(null);
                    }
                } catch (Throwable t) {
                    LogUtil.DEFAULT_LOG.error("long polling error:" + t.getMessage(), t.getCause());
                }

            }

        }, timeoutTime, TimeUnit.MILLISECONDS);
		// 把当前线程添加到订阅事件队列中
        allSubs.add(this);  
    }
		
		void sendResponse(List<String> changedGroups) {
            // 取消超时任务
            if (null != asyncTimeoutFuture) {
                asyncTimeoutFuture.cancel(false);
            }
            generateResponse(changedGroups);
        }
		
		void generateResponse(List<String> changedGroups) {
            if (null == changedGroups) {
                asyncContext.complete();
                return;
            }
            HttpServletResponse response = (HttpServletResponse)asyncContext.getResponse();
            try {
            	// 获取resp 
                String respString = MD5Util.compareMd5ResultString(changedGroups);

                // 禁用缓存
                response.setHeader("Pragma", "no-cache");
                response.setDateHeader("Expires", 0);
                response.setHeader("Cache-Control", "no-cache,no-store");
                response.setStatus(HttpServletResponse.SC_OK);
                // 回写数据
                response.getWriter().println(respString);
                asyncContext.complete();
            } catch (Exception se) {
                pullLog.error(se.toString(), se);
                asyncContext.complete();
            }
        }
}

2.2、控制台的变更与监听

当在控制台点击保存时,会调用nacos服务端的 /v1/cs/configs/ 请求,保存配置并且发送一个 LocalDataChangeEvent 事件,由LongPollingService 进行相应

@Service
public class LongPollingService extends AbstractEventListener {
    /**
     * 长轮询订阅关系
     */
    final Queue<ClientLongPolling> allSubs;
    
	@Override
    public void onEvent(Event event) {
    	// 固定轮询时长的不处理
        if (isFixedPolling()) {
            // ignore
        } else {
            if (event instanceof LocalDataChangeEvent) {
            	// 接收 LocalDataChangeEvent  
                LocalDataChangeEvent evt = (LocalDataChangeEvent)event;
                // 执行 DataChangeTask
                scheduler.execute(new DataChangeTask(evt.groupKey, evt.isBeta, evt.betaIps));
            }
        }
    }

	class DataChangeTask implements Runnable {
        @Override
        public void run() {
            try {
                ConfigService.getContentBetaMd5(groupKey);
                // 循环遍历 allSubs Queue<ClientLongPolling> allSubs;
                for (Iterator<ClientLongPolling> iter = allSubs.iterator(); iter.hasNext(); ) {
                    ClientLongPolling clientSub = iter.next();
					
					// 如果当前 ClientLongPolling  中的 clientMd5Map key中存在当前 groupKey,则进行通知
                    if (clientSub.clientMd5Map.containsKey(groupKey)) {
                        // 如果beta发布且不在beta列表直接跳过
                        if (isBeta && !betaIps.contains(clientSub.ip)) {
                            continue;
                        }

                        // 如果tag发布且不在tag列表直接跳过
                        if (StringUtils.isNotBlank(tag) && !tag.equals(clientSub.tag)) {
                            continue;
                        }

                        getRetainIps().put(clientSub.ip, System.currentTimeMillis());
                        
                        // 删除订阅关系
                        iter.remove();
                        
                        LogUtil.clientLog.info("{}|{}|{}|{}|{}|{}|{}",
                            (System.currentTimeMillis() - changeTime),
                            "in-advance",
                            RequestUtil.getRemoteIp((HttpServletRequest)clientSub.asyncContext.getRequest()),
                            "polling",
                            clientSub.clientMd5Map.size(), clientSub.probeRequestSize, groupKey);
                            
                        // 发送服务配置变更groupKey,完成实时通知
                        clientSub.sendResponse(Arrays.asList(groupKey));
                    }
                }
            } catch (Throwable t) {
                LogUtil.defaultLog.error("data change error:" + t.getMessage(), t.getCause());
            }
        }

		void sendResponse(List<String> changedGroups) {
            // 取消超时任务
            if (null != asyncTimeoutFuture) {
                asyncTimeoutFuture.cancel(false);
            }
            // 直接返回
            generateResponse(changedGroups);
        }
}

服务端一方面通过提前500MS返回当前的nacos对比结果,一方面通过监听实际的nacos变更时机来提前返回,实现的效果如消费队列线程在无消息的情况下阻塞挂起,然后监听队列是否有推送,如有就唤醒消费线程去继续消费队列。且通过异步机制,不消耗Tomcat的线程,是一种值得学习的技术方案。

二、nacos动态更新

1、基于springboot

在Springboot中,如果客户端监听到服务端的nacos的配置更新,则会发起NacosConfigReceivedEvent事件

  • checkListenerMd5
	// 对比MD5 检查配置是否发生变化
	void checkListenerMd5() {
        for (ManagerListenerWrap wrap : listeners) {
            if (!md5.equals(wrap.lastCallMd5)) {
            	// 发生变化
                safeNotifyListener(dataId, group, content, md5, wrap);
            }
        }
    }

    private void safeNotifyListener(final String dataId, final String group, final String content,
                                    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);
                    listenerWrap.lastCallMd5 = md5;
                    LOGGER.info("[{}] [notify-ok] dataId={}, group={}, md5={}, listener={} ", name, dataId, group, md5,
                        listener);
                } catch (NacosException de) {
                    LOGGER.error("[{}] [notify-error] dataId={}, group={}, md5={}, listener={} errCode={} errMsg={}", name,
                        dataId, group, md5, listener, de.getErrCode(), de.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);
    }

DelegatingEventPublishingListener

	@Override
	public void receiveConfigInfo(String content) {
		onReceived(content);
		publishEvent(content);
	}

	private void publishEvent(String content) {
		// 构造NacosConfigReceivedEvent nacos配置更新事件并发送
		NacosConfigReceivedEvent event = new NacosConfigReceivedEvent(configService,
				dataId, groupId, content, configType);
		applicationEventPublisher.publishEvent(event);
	}

NacosValueAnnotationBeanPostProcessor

  • 消费NacosConfigReceivedEvent事件
	@Override
	public void onApplicationEvent(NacosConfigReceivedEvent event) {
		// In to this event receiver, the environment has been updated the
		// latest configuration information, pull directly from the environment
		// fix issue #142
		for (Map.Entry<String, List<NacosValueTarget>> entry : placeholderNacosValueTargetMap
				.entrySet()) {
			String key = environment.resolvePlaceholders(entry.getKey());
			String newValue = environment.getProperty(key);

			if (newValue == null) {
				continue;
			}
			List<NacosValueTarget> beanPropertyList = entry.getValue();
			for (NacosValueTarget target : beanPropertyList) {
				String md5String = MD5Utils.md5Hex(newValue, "UTF-8");
				boolean isUpdate = !target.lastMD5.equals(md5String);
				if (isUpdate) {
					// 更新MD5
					target.updateLastMD5(md5String);
					// 处理EL表达式
					Object evaluatedValue = resolveNotifyValue(target.nacosValueExpr, key, newValue);
					// 更新对应的自动更新的值
					if (target.method == null) {
						setField(target, evaluatedValue);
					}
					else {
						setMethod(target, evaluatedValue);
					}
				}
			}
		}
	}

2、基于Springcloud

在Springcloud中,如果客户端监听到服务端的nacos的配置更新,则会发起NacosConfigReceivedEvent事件
checkListenerMd5中接收变化的配置信息listener.receiveConfigInfo(contentTmp),此时调用的是NacosContextRefresher

	 public void onApplicationEvent(ApplicationReadyEvent event) {
        if (this.ready.compareAndSet(false, true)) {
            this.registerNacosListenersForApplications();
        }

    }

    private void registerNacosListenersForApplications() {
    	// 如果配置@RefreshScope
		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
						// 触发RefreshEvent事件,刷新nacos配置
						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);
		}
	}

RefreshEventListener

public class RefreshEventListener implements SmartApplicationListener {

	private static Log log = LogFactory.getLog(RefreshEventListener.class);

	private ContextRefresher refresh;

	private AtomicBoolean ready = new AtomicBoolean(false);

	public RefreshEventListener(ContextRefresher refresh) {
		this.refresh = refresh;
	}

	@Override
	public boolean supportsEventType(Class<? extends ApplicationEvent> eventType) {
		return ApplicationReadyEvent.class.isAssignableFrom(eventType)
				|| RefreshEvent.class.isAssignableFrom(eventType);
	}

	@Override
	public void onApplicationEvent(ApplicationEvent event) {
		if (event instanceof ApplicationReadyEvent) {
			handle((ApplicationReadyEvent) event);
		}
		else if (event instanceof RefreshEvent) {
			// 执行此handle
			handle((RefreshEvent) event);
		}
	}

	public void handle(ApplicationReadyEvent event) {
		this.ready.compareAndSet(false, true);
	}

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

}

ContextRefresher

	public synchronized Set<String> refresh() {
		// 刷新环境变量
		Set<String> keys = refreshEnvironment();
		// 刷新配置
		this.scope.refreshAll();
		return keys;
	}
	// 动态刷新配置的事件发送方法
	public synchronized Set<String> refreshEnvironment() {
		Map<String, Object> before = extract(
				this.context.getEnvironment().getPropertySources());
		addConfigFilesToEnvironment();
		Set<String> keys = changes(before,
				extract(this.context.getEnvironment().getPropertySources())).keySet();
		this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys));
		return keys;
	}
	
	@ManagedOperation(description = "Dispose of the current instance of all beans "
			+ "in this scope and force a refresh on next method execution.")
	public void refreshAll() {
		super.destroy();
		// RefreshScopeRefreshedEvent刷新事件
		this.context.publishEvent(new RefreshScopeRefreshedEvent());
	}

EnvironmentChangeEvent事件监听器ConfigurationPropertiesRebinder

@Component
@ManagedResource
public class ConfigurationPropertiesRebinder
		implements ApplicationContextAware, ApplicationListener<EnvironmentChangeEvent> {

	private ConfigurationPropertiesBeans beans;

	private ApplicationContext applicationContext;

	private Map<String, Exception> errors = new ConcurrentHashMap<>();

	public ConfigurationPropertiesRebinder(ConfigurationPropertiesBeans beans) {
		this.beans = beans;
	}

	@Override
	public void setApplicationContext(ApplicationContext applicationContext)
			throws BeansException {
		this.applicationContext = applicationContext;
	}

	@Override
	public void onApplicationEvent(EnvironmentChangeEvent event) {
		if (this.applicationContext.equals(event.getSource())
				// Backwards compatible
				|| event.getKeys().equals(event.getSource())) {
			rebind();
		}
	}
	
	/**
	 * A map of bean name to errors when instantiating the bean.
	 * @return The errors accumulated since the latest destroy.
	 */
	public Map<String, Exception> getErrors() {
		return this.errors;
	}

	@ManagedOperation
	public void rebind() {
		this.errors.clear();
		for (String name : this.beans.getBeanNames()) {
			rebind(name);
		}
	}

	@ManagedOperation
	public boolean rebind(String name) {
		if (!this.beans.getBeanNames().contains(name)) {
			return false;
		}
		if (this.applicationContext != null) {
			try {
				Object bean = this.applicationContext.getBean(name);
				if (AopUtils.isAopProxy(bean)) {
					bean = ProxyUtils.getTargetObject(bean);
				}
				if (bean != null) {
					// TODO: determine a more general approach to fix this.
					// see https://github.com/spring-cloud/spring-cloud-commons/issues/571
					if (getNeverRefreshable().contains(bean.getClass().getName())) {
						return false; // ignore
					}
					// 销毁bean后重新初始化,因为环境变量已经更新成最新的nacos,所以初始化后配置也会跟着更新
					this.applicationContext.getAutowireCapableBeanFactory()
							.destroyBean(bean);
					this.applicationContext.getAutowireCapableBeanFactory()
							.initializeBean(bean, name);
					return true;
				}
			}
			catch (RuntimeException e) {
				this.errors.put(name, e);
				throw e;
			}
			catch (Exception e) {
				this.errors.put(name, e);
				throw new IllegalStateException("Cannot rebind to " + name, e);
			}
		}
		return false;
	}

	@ManagedAttribute
	public Set<String> getNeverRefreshable() {
		String neverRefresh = this.applicationContext.getEnvironment().getProperty(
				"spring.cloud.refresh.never-refreshable",
				"com.zaxxer.hikari.HikariDataSource");
		return StringUtils.commaDelimitedListToSet(neverRefresh);
	}

	@ManagedAttribute
	public Set<String> getBeanNames() {
		return new HashSet<>(this.beans.getBeanNames());
	}
}

总结

自此分析完nacos配置中心的变更监听和动态更新,有很多设计思维值得学习,长轮询、异步线程挂起、动态刷新、MD5比较器。这些都是可以带到工作中,让自己的代码变得更加优雅。
备注:后续附上执行流程图

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值