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作为配置中心的源码的解析,如果有不正确的地方,请指正!