Nacos配置中心源码解析

Nacos配置中心源码解析

入口

当启动springboot项目时,调用SpringApplication.run(…)方法

@SpringBootApplication
@MapperScan("com.charlotte.gupao.study.logserviceprovider.dubbo.mapper")
public class LogServiceProviderApplication {

    public static void main(String[] args) {
        SpringApplication.run(LogServiceProviderApplication.class, args);
    }

}

在调用过程中,会先初始化SpringApplication,这时候会把实现了

@SuppressWarnings({ "unchecked", "rawtypes" })
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
	this.resourceLoader = resourceLoader;
	Assert.notNull(primarySources, "PrimarySources must not be null");
	this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
	this.webApplicationType = WebApplicationType.deduceFromClasspath();
	// 把实现了ApplicationContextInitializer接口的类全部加载进来
	setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
	// 把实现了ApplicationListener接口的类全部加载进来
	setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
	this.mainApplicationClass = deduceMainApplicationClass();
}

当调用run方法的时候,会对这些加载的类进行处理

/**
 * Run the Spring application, creating and refreshing a new
 * {@link ApplicationContext}.
 * @param args the application arguments (usually passed from a Java main method)
 * @return a running {@link ApplicationContext}
 */
public ConfigurableApplicationContext run(String... args) {
	StopWatch stopWatch = new StopWatch();
	stopWatch.start();
	ConfigurableApplicationContext context = null;
	Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
	configureHeadlessProperty();
	SpringApplicationRunListeners listeners = getRunListeners(args);
	listeners.starting();
	try {
		ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
		ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
		configureIgnoreBeanInfo(environment);
		Banner printedBanner = printBanner(environment);
		context = createApplicationContext();
		exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
				new Class[] { ConfigurableApplicationContext.class }, context);
		// 准备上下文环境
		prepareContext(context, environment, listeners, applicationArguments, printedBanner);
		refreshContext(context);
		afterRefresh(context, applicationArguments);
		stopWatch.stop();
		if (this.logStartupInfo) {
			new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
		}
		listeners.started(context);
		callRunners(context, applicationArguments);
	}
	catch (Throwable ex) {
		handleRunFailure(context, ex, exceptionReporters, listeners);
		throw new IllegalStateException(ex);
	}

	try {
		listeners.running(context);
	}
	catch (Throwable ex) {
		handleRunFailure(context, ex, exceptionReporters, null);
		throw new IllegalStateException(ex);
	}
	return context;
}

然后我们关注prepareContext(…)这个方法,里面会调用applyInitializers(…)方法,会去遍历上面初始化SpringApplication的时候加载进来的所有实现了ApplicationContextInitializer接口的实现类,并调用initialize(…)方法

private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment,
			SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) {
	context.setEnvironment(environment);
	postProcessApplicationContext(context);
	// 注意该方法
	applyInitializers(context);
	listeners.contextPrepared(context);
	if (this.logStartupInfo) {
		logStartupInfo(context.getParent() == null);
		logStartupProfileInfo(context);
	}
	// Add boot specific singleton beans
	ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
	beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
	if (printedBanner != null) {
		beanFactory.registerSingleton("springBootBanner", printedBanner);
	}
	if (beanFactory instanceof DefaultListableBeanFactory) {
		((DefaultListableBeanFactory) beanFactory)
				.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
	}
	if (this.lazyInitialization) {
		context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
	}
	// Load the sources
	Set<Object> sources = getAllSources();
	Assert.notEmpty(sources, "Sources must not be empty");
	load(context, sources.toArray(new Object[0]));
	listeners.contextLoaded(context);
}


@SuppressWarnings({ "rawtypes", "unchecked" })
protected void applyInitializers(ConfigurableApplicationContext context) {
	for (ApplicationContextInitializer initializer : getInitializers()) {
		Class<?> requiredType = GenericTypeResolver.resolveTypeArgument(initializer.getClass(),
				ApplicationContextInitializer.class);
		Assert.isInstanceOf(requiredType, context, "Unable to call initializer.");
		// 遍历调用ApplicationContextInitializer的实现类的initialize(..)方法
		initializer.initialize(context);
	}
}

而SpringCloud生态中,提供了一个PropertySourceBootstrapConfiguration类,该类实现了ApplicationContextInitializer接口。

分析PropertySourceBootstrapConfiguration

根据上面的逻辑,会调用PropertySourceBootstrapConfiguration的initialize(…)方法

@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
	List<PropertySource<?>> composite = new ArrayList<>();
	AnnotationAwareOrderComparator.sort(this.propertySourceLocators);
	boolean empty = true;
	ConfigurableEnvironment environment = applicationContext.getEnvironment();
	for (PropertySourceLocator locator : this.propertySourceLocators) {
	    // 遍历执行locator.locateCollection(environment)方法
		Collection<PropertySource<?>> source = locator.locateCollection(environment);
		if (source == null || source.size() == 0) {
			continue;
		}
		List<PropertySource<?>> sourceList = new ArrayList<>();
		for (PropertySource<?> p : source) {
			sourceList.add(new BootstrapPropertySource<>(p));
		}
		logger.info("Located property source: " + sourceList);
		// 将配置信息加入到composite中
		composite.addAll(sourceList);
		// 标识source配置信息不为空,下面需要进行处理
		empty = false;
	}
	if (!empty) {
		MutablePropertySources propertySources = environment.getPropertySources();
		String logConfig = environment.resolvePlaceholders("${logging.config:}");
		LogFile logFile = LogFile.get(environment);
		// 遍历移除bootstrapProperty的相关属性
		for (PropertySource<?> p : environment.getPropertySources()) {
			if (p.getName().startsWith(BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
				propertySources.remove(p.getName());
			}
		}
		insertPropertySources(propertySources, composite);
		reinitializeLoggingSystem(environment, logConfig, logFile);
		setLogLevels(applicationContext, environment);
		handleIncludedProfiles(environment);
	}
}

这里会循环遍历this.propertySourceLocators这个属性,而这个属性是一个

@Autowired(required = false)
private List propertySourceLocators = new ArrayList<>();

是自动注入的一个PropertySourceLocator接口集合,而在使用nacos作为配置中心的依赖下,PropertySourceLocator只有一个实现类,即NacosPropertySourceLocator类。
所以可以发现,经过层层调用,会进入NacosPropertySourceLocator类的locate方法。

@Override
public PropertySource<?> locate(Environment env) {
	nacosConfigProperties.setEnvironment(env);
	ConfigService configService = nacosConfigManager.getConfigService();

	if (null == configService) {
		log.warn("no instance of config service found, can't load config from nacos");
		return null;
	}
	long timeout = nacosConfigProperties.getTimeout();
	nacosPropertySourceBuilder = new NacosPropertySourceBuilder(configService,
			timeout);
	String name = nacosConfigProperties.getName();

	String dataIdPrefix = nacosConfigProperties.getPrefix();
	if (StringUtils.isEmpty(dataIdPrefix)) {
		dataIdPrefix = name;
	}

	if (StringUtils.isEmpty(dataIdPrefix)) {
		dataIdPrefix = env.getProperty("spring.application.name");
	}

	CompositePropertySource composite = new CompositePropertySource(
			NACOS_PROPERTY_SOURCE_NAME);

    // 加载共享配置
	loadSharedConfiguration(composite);
	// 加载扩展配置
	loadExtConfiguration(composite);
	// 加载当前应用的配置
	loadApplicationConfiguration(composite, dataIdPrefix, nacosConfigProperties, env);

	return composite;
}

很明显,这里正式开始加载配置,而加载顺序也可以看出,加载共享配置 -> 加载扩展配置 -> 加载当前应用配置,从这里也可以看出配置的优先级顺序,共享配置 < 扩展配置 < 应用自身配置

加载共享配置loadSharedConfiguration(composite)
/**
 * load shared configuration.
 */
private void loadSharedConfiguration(
		CompositePropertySource compositePropertySource) {
	
	List<NacosConfigProperties.Config> sharedConfigs = nacosConfigProperties
			.getSharedConfigs();
	// 如果当前项目配置了共享配置信息,才会进行处理
	if (!CollectionUtils.isEmpty(sharedConfigs)) {
		checkConfiguration(sharedConfigs, "shared-configs");
		loadNacosConfiguration(compositePropertySource, sharedConfigs);
	}
}

加载扩展配置
private void loadExtConfiguration(CompositePropertySource compositePropertySource) {
	List<NacosConfigProperties.Config> extConfigs = nacosConfigProperties
			.getExtensionConfigs();
	// 与共享配置一样,只有配置了才会加载扩展配置
	if (!CollectionUtils.isEmpty(extConfigs)) {
		checkConfiguration(extConfigs, "extension-configs");
		loadNacosConfiguration(compositePropertySource, extConfigs);
	}
}

加载应用本身的配置
/**
 * load configuration of application.
 */
private void loadApplicationConfiguration(
		CompositePropertySource compositePropertySource, String dataIdPrefix,
		NacosConfigProperties properties, Environment environment) {
	// 获得后缀扩展类型
	String fileExtension = properties.getFileExtension();
	// 获得配置的分组信息
	String nacosGroup = properties.getGroup();
	// load directly once by default 默认直接加载一次
	loadNacosDataIfPresent(compositePropertySource, dataIdPrefix, nacosGroup,
			fileExtension, true);
	// load with suffix, which have a higher priority than the default
	// 有后缀的配置文件的优先级比默认高,要再根据后缀加载一次
	loadNacosDataIfPresent(compositePropertySource,
			dataIdPrefix + DOT + fileExtension, nacosGroup, fileExtension, true);
	// Loaded with profile, which have a higher priority than the suffix
	// profile的优先级比后缀更高,要再根据profile加载,多个profile的话要遍历加载
	for (String profile : environment.getActiveProfiles()) {
		String dataId = dataIdPrefix + SEP1 + profile + DOT + fileExtension;
		loadNacosDataIfPresent(compositePropertySource, dataId, nacosGroup,
				fileExtension, true);
	}
}

其实不管是加载哪种配置,最终都会调用loadNacosDataIfPresent(…)方法

private void loadNacosDataIfPresent(final CompositePropertySource composite,
    	final String dataId, final String group, String fileExtension,
    	boolean isRefreshable) {
    if (null == dataId || dataId.trim().length() < 1) {
    	return;
    }
    if (null == group || group.trim().length() < 1) {
    	return;
    }
    NacosPropertySource propertySource = this.loadNacosPropertySource(dataId, group,
    		fileExtension, isRefreshable);
    this.addFirstPropertySource(composite, propertySource, false);
    }
    
    
private NacosPropertySource loadNacosPropertySource(final String dataId,
			final String group, String fileExtension, boolean isRefreshable) {
	if (NacosContextRefresher.getRefreshCount() != 0) {
		if (!isRefreshable) {
		// 如果不需要动态刷新的,则直接从本地缓存中获取
			return NacosPropertySourceRepository.getNacosPropertySource(dataId,
					group);
		}
	}
	// 会从远程获取
	return nacosPropertySourceBuilder.build(dataId, group, fileExtension,
			isRefreshable);
}

/**
 * @param dataId Nacos dataId
 * @param group Nacos group
 */
NacosPropertySource build(String dataId, String group, String fileExtension,
		boolean isRefreshable) {
	// 加载配置
	Map<String, Object> p = loadNacosData(dataId, group, fileExtension);
	// 将获得配置放到NacosPropertySourceRepository内存仓库缓存中
	NacosPropertySource nacosPropertySource = new NacosPropertySource(group, dataId,
			p, new Date(), isRefreshable);
	NacosPropertySourceRepository.collectNacosPropertySource(nacosPropertySource);
	return nacosPropertySource;
}

我们再来看loadNacosData(dataId, group, fileExtension)
这里又经过了层层调用,最后进入一个关键类NacosConfigService(这个类就类似Nacos作为注册中心的逻辑的NamingService),这里调用了getConfigInner(…)方法

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

    // 优先使用本地配置,会尝试从指定目录读取,默认情况下路径为:${user.home}/nacos/config/
    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 {
        // 如果上面从本地没有读取到,则会从服务端获取
        String[] ct = worker.getServerConfig(dataId, group, tenant, timeoutMs);
        cr.setContent(ct[0]);

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

从远程服务读取配置信息,其实就是发起一个http请求,从nacos服务端获取数据

public String[] getServerConfig(String dataId, String group, String tenant, long readTimeout)
        throws NacosException {
    String[] ct = new String[2];
    if (StringUtils.isBlank(group)) {
        group = Constants.DEFAULT_GROUP;
    }

    HttpResult result = null;
    try {
        List<String> params = null;
        if (StringUtils.isBlank(tenant)) {
            params = new ArrayList<String>(Arrays.asList("dataId", dataId, "group", group));
        } else {
            params = new ArrayList<String>(Arrays.asList("dataId", dataId, "group", group, "tenant", tenant));
        }
        // 默认访问路径:http://ip:port/nacos/v1/ns/configs
        result = agent.httpGet(Constants.CONFIG_CONTROLLER_PATH, null, params, agent.getEncode(), readTimeout);
    } catch (IOException e) {
        String message = String.format(
            "[%s] [sub-server] get server config exception, dataId=%s, group=%s, tenant=%s", agent.getName(),
            dataId, group, tenant);
        LOGGER.error(message, e);
        throw new NacosException(NacosException.SERVER_ERROR, e);
    }

    switch (result.code) {
        case HttpURLConnection.HTTP_OK:
            LocalConfigInfoProcessor.saveSnapshot(agent.getName(), dataId, group, tenant, result.content);
            ct[0] = result.content;
            if (result.headers.containsKey(CONFIG_TYPE)) {
                ct[1] = result.headers.get(CONFIG_TYPE).get(0);
            } else {
                ct[1] = ConfigType.TEXT.getType();
            }
            return ct;
        case HttpURLConnection.HTTP_NOT_FOUND:
            LocalConfigInfoProcessor.saveSnapshot(agent.getName(), dataId, group, tenant, null);
            return ct;
        case HttpURLConnection.HTTP_CONFLICT: {
            LOGGER.error(
                "[{}] [sub-server-error] get server config being modified concurrently, dataId={}, group={}, "
                    + "tenant={}", agent.getName(), dataId, group, tenant);
            throw new NacosException(NacosException.CONFLICT,
                "data being modified, dataId=" + dataId + ",group=" + group + ",tenant=" + tenant);
        }
        case HttpURLConnection.HTTP_FORBIDDEN: {
            LOGGER.error("[{}] [sub-server-error] no right, dataId={}, group={}, tenant={}", agent.getName(), dataId,
                group, tenant);
            throw new NacosException(result.code, result.content);
        }
        default: {
            LOGGER.error("[{}] [sub-server-error]  dataId={}, group={}, tenant={}, code={}", agent.getName(), dataId,
                group, tenant, result.code);
            throw new NacosException(result.code,
                "http error, code=" + result.code + ",dataId=" + dataId + ",group=" + group + ",tenant=" + tenant);
        }
    }
}

客户端动态感知

上面分析了初始化的时候客户端如何加载配置,那么当服务端的配置信息变更的时候,客户端又是如何动态感知的呢?

NacosConfigService

当NacosConfigService被加载的时候,构造方法中会进行一些处理

public NacosConfigService(Properties properties) throws NacosException {
    ValidatorUtils.checkInitParam(properties);
    String encodeTmp = properties.getProperty(PropertyKeyConst.ENCODE);
    if (StringUtils.isBlank(encodeTmp)) {
        this.encode = Constants.ENCODE;
    } else {
        this.encode = encodeTmp.trim();
    }
    initNamespace(properties);

    // 封装了MetricsHttpAgent,能够实现数据信息上报到metrics
    this.agent = new MetricsHttpAgent(new ServerHttpAgent(properties));
    this.agent.start();
    // 这里会初始化一个客户端工作类
    this.worker = new ClientWorker(this.agent, this.configFilterChainManager, properties);
}

再来看ClientWorker实例的初始化

@SuppressWarnings("PMD.ThreadPoolCreationRule")
public ClientWorker(final HttpAgent agent, final ConfigFilterChainManager configFilterChainManager,
        final Properties properties) {
    this.agent = agent;
    this.configFilterChainManager = configFilterChainManager;

    // Initialize the timeout parameter

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

    // 延后10ms执行检查配置的任务
    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);
}

重点关注方法checkConfigInfo(),该方法开始会检查配置

/**
 * Check config info.
 */
public void checkConfigInfo() {
    // Dispatch taskes.
    // 任务分片
    int listenerSize = cacheMap.get().size();
    // Round up the longingTaskCount.向上取整,保证所有的配置都会检查到 ParamUtil.getPerTaskConfigSize()默认为3000,
    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;
    }
}

这里可以看到,使用了上面初始化的executorService这个线程池来进行长轮询任务
那么到底是如何进行长轮询的呢?接着看LongPollingRunnable类的实现。

/**
 * 长轮询线程
 */
class LongPollingRunnable implements Runnable {

    // 通过taskId,可以找到该线程对应的需要检查的那部分配置分片
    private final 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
            // 这里的cacheMap中的数据是存在内存缓存中的,不是本地的。
            for (CacheData cacheData : cacheMap.get().values()) {
                // 只负责处理自己分片的配置
                if (cacheData.getTaskId() == taskId) {
                    cacheDatas.add(cacheData);
                    try {
                        // 检查本地的配置,并设置cacheData.isUseLocalConfigInfo()属性
                        checkLocalConfig(cacheData);
                        if (cacheData.isUseLocalConfigInfo()) {
                            // 如果成立,会触发监听,比较md5
                            cacheData.checkListenerMd5();
                        }
                    } catch (Exception e) {
                        LOGGER.error("get local config info error", e);
                    }
                }
            }

            // check server config 检查当前分片下的配置在服务端是否发生变化
            // 从服务端获取发生了变化的配置的key(chengedGroupKeys表示服务端告诉客户端,哪些配置发生了变化)
            List<String> changedGroupKeys = checkUpdateDataIds(cacheDatas, inInitializingCacheList);
            if (!CollectionUtils.isEmpty(changedGroupKeys)) {
                LOGGER.info("get changedGroupKeys:" + changedGroupKeys);
            }

            // 遍历发生了变化的key,并根据key去服务端请求最新配置,并更新到内存缓存中
            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.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);
        }
    }
}

首先注意到,构造方法中需要传入参数taskId,这个taskId是用来区分当前任务应该处理的那部分分片的配置,因为cacheData中也会存储分片的id,就是比如一共有4000个配置,那么按照默认大小,会分成2片,同时会生成2个LongPollingRunnable线程来更新配置,而线程中就是通过这个传入taskId来确定当前线程应该处理的那部分配置。

checkLocalConfig(…)方法
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);

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

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

    // When it changed.
    // 当使用本地配置,且本地文件存在,但是当前内存中的版本和本地文件的版本不一致时,会进入判断
    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);
        LOGGER.warn(
                "[{}] [failover-change] failover file changed. dataId={}, group={}, tenant={}, md5={}, content={}",
                agent.getName(), dataId, group, tenant, md5, ContentUtils.truncateContent(content));
    }
}
checkUpdateDataIds(…)方法
/**
 * Fetch the dataId list from server.
 * 从服务端获取dataId列表,仅仅返回dataId
 * @param cacheDatas              CacheDatas for config infomations.
 * @param inInitializingCacheList initial cache lists.
 * @return String include dataId and group (ps: it maybe null).
 * @throws Exception Exception.
 */
List<String> checkUpdateDataIds(List<CacheData> cacheDatas, List<String> inInitializingCacheList) throws Exception {
    StringBuilder sb = new StringBuilder();
    // 这里遍历该分片下的所有配置,然后把所有不使用本地配置的配置拼接成string
    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()) {
                // It updates when cacheData occours in cacheMap by first time.
                inInitializingCacheList
                        .add(GroupKey.getKeyTenant(cacheData.dataId, cacheData.group, cacheData.tenant));
            }
        }
    }
    boolean isInitializingCacheList = !inInitializingCacheList.isEmpty();
    // 这里的sb表示该分片下的不使用本地的配置的拼接串
    return checkUpdateConfigStr(sb.toString(), isInitializingCacheList);
}

checkUpdateConfigStr(…)方法
/**
 * Fetch the updated dataId list from server.
 * 从服务端后去发生了变化的配置的dataId列表
 * @param probeUpdateString       updated attribute string value.
 * @param isInitializingCacheList initial cache lists.
 * @return The updated dataId list(ps: it maybe null).
 * @throws IOException Exception.
 */
List<String> checkUpdateConfigStr(String probeUpdateString, boolean isInitializingCacheList) throws Exception {

    Map<String, String> params = new HashMap<String, String>(2);
    params.put(Constants.PROBE_MODIFY_REQUEST, probeUpdateString);
    Map<String, String> headers = new HashMap<String, String>(2);
    headers.put("Long-Pulling-Timeout", "" + timeout);

    // told server do not hang me up if new initializing cacheData added in
    if (isInitializingCacheList) {
        headers.put("Long-Pulling-Timeout-No-Hangup", "true");
    }

    if (StringUtils.isBlank(probeUpdateString)) {
        return Collections.emptyList();
    }

    try {
        // 发起长轮询请求
        // In order to prevent the server from handling the delay of the client's long task,
        // increase the client's read timeout to avoid this problem.
        // TODO-- 这里超时时间默认是多少?根据默认值,应该是45s?
        long readTimeoutMs = timeout + (long) Math.round(timeout >> 1);
        // 请求路径:http://ip:port/nacos/v1/ns/configs/listener
        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());
        }
    } catch (Exception e) {
        setHealthServer(false);
        LOGGER.error("[" + agent.getName() + "] [check-update] get changed dataId exception", e);
        throw e;
    }
    return Collections.emptyList();
}
服务端处理长轮询请求

上面已经分析到客户端如何发送长轮询请求了,那么当服务端接收到请求后,是如何处理的呢?
首先确认请求路径为http://ip:port/nacos/v1/ns/configs/listener
根据路径找到提供rest请求的类ConfigController

/**
 * The client listens for configuration changes.
 * 服务端提供配置修改监听的请求入口
 */
@PostMapping("/listener")
@Secured(action = ActionTypes.READ, parser = ConfigResourceParser.class)
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);

    Map<String, String> clientMd5Map;
    try {
        // 客户端会传递多个dataId
        clientMd5Map = MD5Util.getClientMd5Map(probeModify);
    } catch (Throwable e) {
        throw new IllegalArgumentException("invalid probeModify");
    }

    // do long-polling 进行长轮询
    inner.doPollingConfig(request, response, clientMd5Map, probeModify.length());
}
inner.doPollingConfig(request, response, clientMd5Map, probeModify.length());
/**
 * 轮询接口.
 */
public String doPollingConfig(HttpServletRequest request, HttpServletResponse response,
        Map<String, String> clientMd5Map, int probeRequestSize) throws IOException {

    // Long polling.
    // 根据请求头里是否有Long-Pulling-Timeout判断是否是长轮询请求
    if (LongPollingService.isSupportLongPolling(request)) {
        // 支持长轮询的情况下,需要阻塞请求的返回
        longPollingService.addLongPollingClient(request, response, clientMd5Map, probeRequestSize);
        return HttpServletResponse.SC_OK + "";
    }

    // ------------------------------以下为短轮询的逻辑-------------------------------------------
    
    // Compatible with short polling logic.
    // 兼容短轮询的逻辑,遍历配置的md5,返回发生了变化的配置的key
    List<String> changedGroups = MD5Util.compareMd5(request, response, clientMd5Map);

    // Compatible with short polling 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);

    // Befor 2.0.4 version, return value is put into header.
    if (versionNum < START_LONG_POLLING_VERSION_NUM) {
        response.addHeader(Constants.PROBE_MODIFY_RESPONSE, oldResult);
        response.addHeader(Constants.PROBE_MODIFY_RESPONSE_NEW, newResult);
    } else {
        request.setAttribute("content", newResult);
    }

    Loggers.AUTH.info("new content:" + newResult);

    // Disable cache.
    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 + "";
}

从方法中可以看出,这里兼容了短轮询的逻辑,如果是普通的请求,那么就会直接通过response中设置属性content字段进行返回,客户端通过读取content字段的数据来更新。
再继续看长轮询的逻辑。

/**
 * Add LongPollingClient.
 * 将请求加到长轮询队列中
 *
 * @param req              HttpServletRequest.
 * @param rsp              HttpServletResponse.
 * @param clientMd5Map     clientMd5Map.
 * @param probeRequestSize probeRequestSize.
 */
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");
    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()) { // 如果是固定的长轮询请求,则仅仅重新设置超时时间,不做其他操作
        timeout = Math.max(10000, getFixedPollingInterval());
        // Do nothing but set fix polling timeout.
    } else {
        long start = System.currentTimeMillis();
        // 找到需要更新的配置的key的列表
        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)) {
            LogUtil.CLIENT_LOG.info("{}|{}|{}|{}|{}|{}|{}", System.currentTimeMillis() - start, "nohangup",
                    RequestUtil.getRemoteIp(req), "polling", clientMd5Map.size(), probeRequestSize,
                    changedGroups.size());
            return;
        }
    }
    String ip = RequestUtil.getRemoteIp(req);

    // Must be called by http thread, or send response.
    final AsyncContext asyncContext = req.startAsync();

    // AsyncContext.setTimeout() is incorrect, Control by oneself
    asyncContext.setTimeout(0L);

    // 开启一个长轮询线程
    ConfigExecutor.executeLongPolling(
            new ClientLongPolling(asyncContext, clientMd5Map, ip, probeRequestSize, timeout, appName, tag));
}

这里的主要逻辑为:

1.从请求中获取参数,并重新设置请求过期时间,保证在客户端请求超时前进行返回
2.如果isFixedPolling()==true,则重新设定超时时间,不做其他的操作
3.如果isFixedPolling()==false,则会直接比较一次配置信息,如果配置有变更,则直接返回给客户端,不进行长轮询。
4.如果配置没有修改,但是noHangUpFlag == true,表示该请求不需要挂起,那么就直接返回。
5.如果上面的流程都没有将请求返回,那么就会通过req这个客户端过来的请求创建一个AsyncContext,并通过线程执行ClientLongPolling任务。

那么继续来看ClientLongPolling的执行逻辑,因为是线程,肯定看他的run()方法

@Override
public void run() {
    // 调度一个延时执行的任务,在这段延时的时间内,会监听配置的修改,如果触发了事件,会从这里获取allSubs队列中找到相应的长轮询连接,并返回。
    asyncTimeoutFuture = ConfigExecutor.scheduleLongPolling(new Runnable() {
        @Override
        public void run() {
            try {
                getRetainIps().put(ClientLongPolling.this.ip, System.currentTimeMillis());

                // Delete subsciber's relations.
                // 从队列中删除当前长轮询连接实例
                allSubs.remove(ClientLongPolling.this);

                if (isFixedPolling()) {
                    // 如果是固定的长轮询,则通过md5比较,找到需要更新的配置的key
                    LogUtil.CLIENT_LOG
                            .info("{}|{}|{}|{}|{}|{}", (System.currentTimeMillis() - createTime), "fix",
                                    RequestUtil.getRemoteIp((HttpServletRequest) asyncContext.getRequest()),
                                    "polling", clientMd5Map.size(), probeRequestSize);
                    List<String> changedGroups = MD5Util
                            .compareMd5((HttpServletRequest) asyncContext.getRequest(),
                                    (HttpServletResponse) asyncContext.getResponse(), clientMd5Map);
                    if (changedGroups.size() > 0) {
                        sendResponse(changedGroups);
                    } else {
                        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队列中
    allSubs.add(this);
}

这段代码其实就两个逻辑,一个是执行一个延时调度任务,一个是将当前长轮询连接放入allSubs队列中。
这里看调度的任务的逻辑。
如果isFixedPolling()==true,则会对配置进行比较,如果有变化,则返回变化的配置的key,其他则返回null,表示没有需要更新的配置。

那么这里有个问题,当长轮询任务在等待调度的时候,如果配置发生了变化,是如何动态告知客户端的呢?其实显然猜想的到,肯定有地方会进行事件的监听,当配置更改时,发布监听的事件,从而能够实时的动态告知客户端配置的变化。

LongPollingService的初始化
/**
 * 初始化的时候会注册监听
 */
@SuppressWarnings("PMD.ThreadPoolCreationRule")
public LongPollingService() {
    // 初始化存放客户端长轮询连接的队列
    allSubs = new ConcurrentLinkedQueue<ClientLongPolling>();

    ConfigExecutor.scheduleLongPolling(new StatTask(), 0L, 10L, TimeUnit.SECONDS);

    // Register LocalDataChangeEvent to NotifyCenter.
    // 将LocalDataChangeEvent这个事件注册到NotifyCenter中
    NotifyCenter.registerToPublisher(LocalDataChangeEvent.class, NotifyCenter.ringBufferSize);

    // Register A Subscriber to subscribe LocalDataChangeEvent.
    // 注册一个LocalDataChangeEvent事件的订阅
    NotifyCenter.registerSubscriber(new Subscriber() {

        @Override
        public void onEvent(Event event) {
            if (isFixedPolling()) {
                // Ignore.
            } else {
                if (event instanceof LocalDataChangeEvent) {
                    LocalDataChangeEvent evt = (LocalDataChangeEvent) event;
                    // 当触发LocalDataChangeEvent事件的时候,会调度DataChangeTask任务。
                    ConfigExecutor.executeLongPolling(new DataChangeTask(evt.groupKey, evt.isBeta, evt.betaIps));
                }
            }
        }

        @Override
        public Class<? extends Event> subscribeType() {
            return LocalDataChangeEvent.class;
        }
    });
}

通过LongPollingService的构造方法,发现该类在初始化的时候,会注册一个LocalDataChangeEvent事件的监听,当收到LocalDataChangeEvent事件后会调度DataChangeTask任务。
DataChangeTask的run()方法如下

@Override
public void run() {
    try {
        ConfigCacheService.getContentBetaMd5(groupKey);
        // 遍历allSubs队列,
        for (Iterator<ClientLongPolling> iter = allSubs.iterator(); iter.hasNext(); ) {
            ClientLongPolling clientSub = iter.next();
            // 根据当前触发事件的配置找到对应的长轮询连接
            if (clientSub.clientMd5Map.containsKey(groupKey)) {
                // If published tag is not in the beta list, then it skipped.
                if (isBeta && !CollectionUtils.contains(betaIps, clientSub.ip)) {
                    continue;
                }

                // If published tag is not in the tag list, then it skipped.
                if (StringUtils.isNotBlank(tag) && !tag.equals(clientSub.tag)) {
                    continue;
                }

                getRetainIps().put(clientSub.ip, System.currentTimeMillis());
                // 将当前触发了事件的长轮询连接从allSubs中剔除
                iter.remove(); // Delete subscribers' relationships.
                LogUtil.CLIENT_LOG
                        .info("{}|{}|{}|{}|{}|{}|{}", (System.currentTimeMillis() - changeTime), "in-advance",
                                RequestUtil
                                        .getRemoteIp((HttpServletRequest) clientSub.asyncContext.getRequest()),
                                "polling", clientSub.clientMd5Map.size(), clientSub.probeRequestSize, groupKey);

                // 直接发送返回信息,并且会把延时调度任务当前的asyncTimeoutFuture结束
                clientSub.sendResponse(Arrays.asList(groupKey));
            }
        }
    } catch (Throwable t) {
        LogUtil.DEFAULT_LOG.error("data change error: {}", ExceptionUtil.getStackTrace(t));
    }
}

该方法里会遍历allSubs这个队列,并从里面找到当前触发的配置变更事件对应的那个长轮询连接,然后将该连接从allSubs队列中去除,并且通过clientSub.sendResponse(Arrays.asList(groupKey));方法直接返回。
而在clientSub.sendResponse(Arrays.asList(groupKey));这个方法里,会判断如果当前这个长轮询连接的asyncTimeoutFuture延时调度任务还没取消,则会将该任务取消,因为配置已经变更,并且要立刻返回,继续执行延时任务没有意义。

触发LocalDataChangeEvent事件

上面我们说了,LongPollingService会注册一个LocalDataChangeEvent事件的监听,那么哪里会触发这个监听呢?
在ConfigCacheService中,只要涉及到config配置信息的修改的,都会发布LocalDataChangeEvent事件
类似于:

/**
 * Update md5 value.
 *
 * @param groupKey       groupKey string value.
 * @param md5            md5 string value.
 * @param lastModifiedTs lastModifiedTs long value.
 */
public static void updateMd5(String groupKey, String md5, long lastModifiedTs) {
    CacheItem cache = makeSure(groupKey);
    if (cache.md5 == null || !cache.md5.equals(md5)) {
        cache.md5 = md5;
        cache.lastModifiedTs = lastModifiedTs;
        // 发布LocalDataChangeEvent事件
        NotifyCenter.publishEvent(new LocalDataChangeEvent(groupKey));
    }
}

总结

以上就是对Naocs作为配置中心的源码的解析,如果有不正确的地方,请指正!

  • 5
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值