目录
初始化加载配置过程
首先从SpringBoot的启动方法分析入口
org.springframework.boot.SpringApplication#run(java.lang.Class<?>[], java.lang.String[])
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
return new SpringApplication(primarySources).run(args);
}
org.springframework.boot.SpringApplication#SpringApplication(org.springframework.core.io.ResourceLoader, java.lang.Class<?>...)
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();
}
org.springframework.boot.SpringApplication#run(java.lang.String...)
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;
}
在run方法中,我们这里着重关注下prepareContext方法。这个方法里有个applyInitializers方法
org.springframework.boot.SpringApplication#applyInitializers
protected void applyInitializers(ConfigurableApplicationContext context) {
// 遍历初始化时加载进来所有实现ApplicationContextInitializer接口的类,并执行initialize方法
for (ApplicationContextInitializer initializer : getInitializers()) {
Class<?> requiredType = GenericTypeResolver.resolveTypeArgument(initializer.getClass(),
ApplicationContextInitializer.class);
Assert.isInstanceOf(requiredType, context, "Unable to call initializer.");
initializer.initialize(context);
}
}
在启动服务的过程中,可以从控制台中看到PropertySourceBootstrapConfiguration这个类去读取了Nacos的对应配置,该类也实现了ApplicationContextInitializer接口,那么我们看下它的initialize方法。
org.springframework.cloud.bootstrap.config.PropertySourceBootstrapConfiguration#initialize
public void initialize(ConfigurableApplicationContext applicationContext) {
List<PropertySource<?>> composite = new ArrayList<>();
AnnotationAwareOrderComparator.sort(this.propertySourceLocators);
boolean empty = true;
ConfigurableEnvironment environment = applicationContext.getEnvironment();
// 遍历所有实现PropertySourceLocator接口的实现类(在使用Nacos为配置中心的依赖下,其实现类只有NacosPropertySourceLocator)
for (PropertySourceLocator locator : this.propertySourceLocators) {
// 这里面就是调用实现类的locate方法,
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.addAll(sourceList);
empty = false;
}
if (!empty) {
MutablePropertySources propertySources = environment.getPropertySources();
String logConfig = environment.resolvePlaceholders("${logging.config:}");
LogFile logFile = LogFile.get(environment);
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);
}
}
com.alibaba.cloud.nacos.client.NacosPropertySourceLocator#locate
public PropertySource<?> locate(Environment env) {
nacosConfigProperties.setEnvironment(env);
// 通过反射创建NacosConfigService
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);
// 获取name的值
String name = nacosConfigProperties.getName();
// 获取prefix的值
String dataIdPrefix = nacosConfigProperties.getPrefix();
if (StringUtils.isEmpty(dataIdPrefix)) {
// prefix为空,则dataIdPrefix为name的值
dataIdPrefix = name;
}
if (StringUtils.isEmpty(dataIdPrefix)) {
// 再次判断为空,则直接获取spring.application.name的值
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;
}
com.alibaba.cloud.nacos.client.NacosPropertySourceLocator#loadSharedConfiguration
private void loadSharedConfiguration(
CompositePropertySource compositePropertySource) {
List<NacosConfigProperties.Config> sharedConfigs = nacosConfigProperties
.getSharedConfigs();
// 加载共享配置(即配置shared-configs)
if (!CollectionUtils.isEmpty(sharedConfigs)) {
checkConfiguration(sharedConfigs, "shared-configs");
loadNacosConfiguration(compositePropertySource, sharedConfigs);
}
}
com.alibaba.cloud.nacos.client.NacosPropertySourceLocator#loadExtConfiguration
private void loadExtConfiguration(CompositePropertySource compositePropertySource) {
List<NacosConfigProperties.Config> extConfigs = nacosConfigProperties
.getExtensionConfigs();
// 加载扩展配置(即配置extension-configs)
if (!CollectionUtils.isEmpty(extConfigs)) {
checkConfiguration(extConfigs, "extension-configs");
loadNacosConfiguration(compositePropertySource, extConfigs);
}
}
com.alibaba.cloud.nacos.client.NacosPropertySourceLocator#loadApplicationConfiguration
private void loadApplicationConfiguration(
CompositePropertySource compositePropertySource, String dataIdPrefix,
NacosConfigProperties properties, Environment environment) {
// 获取文件后缀名,即file-extension(默认properties)
String fileExtension = properties.getFileExtension();
// 获取分组名,即group(默认DEFAULT_GROUP)
String nacosGroup = properties.getGroup();
// load directly once by default
// 加载dataIdPrefix作为dataId的配置
loadNacosDataIfPresent(compositePropertySource, dataIdPrefix, nacosGroup,
fileExtension, true);
// load with suffix, which have a higher priority than the default
// 加载dataIdPrefix + fileExtension作为dataId的配置
loadNacosDataIfPresent(compositePropertySource,
dataIdPrefix + DOT + fileExtension, nacosGroup, fileExtension, true);
// Loaded with profile, which have a higher priority than the suffix
for (String profile : environment.getActiveProfiles()) {
String dataId = dataIdPrefix + SEP1 + profile + DOT + fileExtension;
// 加载dataIdPrefix + profile + fileExtension作为dataId的配置
loadNacosDataIfPresent(compositePropertySource, dataId, nacosGroup,
fileExtension, true);
}
}
可以看出,配置加载的顺序为共享配置 -> 扩展配置 -> 应用配置,其中在加载应用配置时,具体的优先级顺序为前缀 + 环境 + 后缀 > 前缀 + 后缀 > 前缀(注:前缀为dataIdPrefix,环境为profile,后缀为fileExtension)。
三种加载方式加载的方法都是loadNacosDataIfPresent
com.alibaba.cloud.nacos.client.NacosPropertySourceLocator#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);
}
com.alibaba.cloud.nacos.client.NacosPropertySourceLocator#loadNacosPropertySource
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);
}
本地缓存的获取过程就是将dataId,group当做key从Map中获取,这里着重看一下从远程获取的方法。
com.alibaba.cloud.nacos.client.NacosPropertySourceBuilder#build
NacosPropertySource build(String dataId, String group, String fileExtension,
boolean isRefreshable) {
// 加载配置
Map<String, Object> p = loadNacosData(dataId, group, fileExtension);
NacosPropertySource nacosPropertySource = new NacosPropertySource(group, dataId,
p, new Date(), isRefreshable);
// 放入到本地缓存中
NacosPropertySourceRepository.collectNacosPropertySource(nacosPropertySource);
return nacosPropertySource;
}
com.alibaba.cloud.nacos.client.NacosPropertySourceBuilder#loadNacosData
private Map<String, Object> loadNacosData(String dataId, String group,
String fileExtension) {
String data = null;
try {
// 具体的执行方法为NacosConfigService类的getConfigInner方法
data = configService.getConfig(dataId, group, timeout);
if (StringUtils.isEmpty(data)) {
log.warn(
"Ignore the empty nacos configuration and get it based on dataId[{}] & group[{}]",
dataId, group);
return EMPTY_MAP;
}
if (log.isDebugEnabled()) {
log.debug(String.format(
"Loading nacos data, dataId: '%s', group: '%s', data: %s", dataId,
group, data));
}
Map<String, Object> dataMap = NacosDataParserHandler.getInstance()
.parseNacosData(data, fileExtension);
return dataMap == null ? EMPTY_MAP : dataMap;
}
catch (NacosException e) {
log.error("get data from Nacos error,dataId:{}, ", dataId, e);
}
catch (Exception e) {
log.error("parse data from Nacos error,dataId:{},data:{},", dataId, data, e);
}
return EMPTY_MAP;
}
com.alibaba.nacos.client.config.NacosConfigService#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);
// 优先使用本地配置,尝试从指定目录获取(/data/config-data),默认情况下路径为:${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 {
// 本地配置未获取到,调用服务端获取(/v1/cs/configs)
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));
// 若从服务端也未获取到,则直接采用本地快照的形式(/snapshot)
content = LocalConfigInfoProcessor.getSnapshot(agent.getName(), dataId, group, tenant);
cr.setContent(content);
configFilterChainManager.doFilter(null, cr);
content = cr.getContent();
return content;
}
上述是客户端启动时加载配置的一个过程,但是当配置信息更改时,客户端是如何动态刷新的呢?
配置动态刷新过程
客户端发送长轮询请求
在客户端启动加载配置的过程中,可以看到在com.alibaba.cloud.nacos.client.NacosPropertySourceLocator#locate这个方法中最开始利用了反射创建了NacosConfigService,这里我们看下他的构造函数
com.alibaba.nacos.client.config.NacosConfigService#NacosConfigService
public NacosConfigService(Properties properties) throws NacosException {
String encodeTmp = properties.getProperty(PropertyKeyConst.ENCODE);
if (StringUtils.isBlank(encodeTmp)) {
encode = Constants.ENCODE;
} else {
encode = encodeTmp.trim();
}
initNamespace(properties);
// 利用装饰器模式封装了MetricsHttpAgen,真正做处理的为ServerHttpAgent,其内部就是利用一个核心线程数为1的线程池每5秒调用服务端的login接口(/v1/auth/users/login)
agent = new MetricsHttpAgent(new ServerHttpAgent(properties));
agent.start();
// 创建一个ClientWorker对象
worker = new ClientWorker(agent, configFilterChainManager, properties);
}
这里具体来看下ClientWorker是如何创建的。
com.alibaba.nacos.client.config.impl.ClientWorker#ClientWorker
public ClientWorker(final HttpAgent agent, final ConfigFilterChainManager configFilterChainManager, final Properties properties) {
this.agent = agent;
this.configFilterChainManager = configFilterChainManager;
// Initialize the timeout parameter
init(properties);
// 初始化一个核心线程数为1的线程池
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;
}
});
// 初始化用于长轮询的线程池,核心线程数为Runtime.getRuntime().availableProcessors()
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;
}
});
// 首次延迟1ms,后续延迟10ms执行检查配置信息
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);
}
com.alibaba.nacos.client.config.impl.ClientWorker#checkConfigInfo
public void checkConfigInfo() {
// 任务分片
int listenerSize = cacheMap.get().size();
// 向上取整为批数,保证每个任务都能检查到(ParamUtil.getPerTaskConfigSize()默认为3000,假设有4000个配置,那么就会分为2片)
int longingTaskCount = (int) Math.ceil(listenerSize / ParamUtil.getPerTaskConfigSize());
if (longingTaskCount > currentLongingTaskCount) {
for (int i = (int) currentLongingTaskCount; i < longingTaskCount; i++) {
// 执行任务(LongPollingRunnable实现了Runnable,具体执行方法为run方法)
executorService.execute(new LongPollingRunnable(i));
}
currentLongingTaskCount = longingTaskCount;
}
}
com.alibaba.nacos.client.config.impl.ClientWorker.LongPollingRunnable#run
public void run() {
List<CacheData> cacheDatas = new ArrayList<CacheData>();
List<String> inInitializingCacheList = new ArrayList<String>();
try {
// check failover config
for (CacheData cacheData : cacheMap.get().values()) {
// 只处理当前分片的配置
if (cacheData.getTaskId() == taskId) {
cacheDatas.add(cacheData);
try {
// 检查本地配置
checkLocalConfig(cacheData);
// 判断是否使用本地配置,如果为true,则进行md5比较
if (cacheData.isUseLocalConfigInfo()) {
cacheData.checkListenerMd5();
}
} catch (Exception e) {
LOGGER.error("get local config info error", e);
}
}
}
// 检查当前分片下的配置在服务端是否已发生了变化,并返回发生变化的key,
List<String> changedGroupKeys = checkUpdateDataIds(cacheDatas, inInitializingCacheList);
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);
}
}
// 遍历该分片下的配置,如果该配置的isInitializing为false(即非首次出现在cacheMap中) 或 inInitializingCacheList中存在该配置,则比较md5,并且设置isInitializing为false
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);
}
}
com.alibaba.nacos.client.config.impl.ClientWorker#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);
// 不使用本地配置并且本地配置的文件存在,则使用本地配置,并且设置isUseLocalConfig为true(即使用本地配置)
if (!cacheData.isUseLocalConfigInfo() && path.exists()) {
String content = LocalConfigInfoProcessor.getFailover(agent.getName(), dataId, group, tenant);
String md5 = MD5.getInstance().getMD5String(content);
cacheData.setUseLocalConfigInfo(true);
cacheData.setLocalConfigInfoVersion(path.lastModified());
cacheData.setContent(content);
LOGGER.warn("[{}] [failover-change] failover file created. dataId={}, group={}, tenant={}, md5={}, content={}",
agent.getName(), dataId, group, tenant, md5, ContentUtils.truncateContent(content));
return;
}
// 使用本地配置并且本地配置的文件不存在,直接设置isUseLocalConfig为false(不使用本地配置)
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);
String md5 = MD5.getInstance().getMD5String(content);
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));
}
}
com.alibaba.nacos.client.config.impl.ClientWorker#checkUpdateDataIds
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);
}
com.alibaba.nacos.client.config.impl.ClientWorker#checkUpdateConfigStr
List<String> checkUpdateConfigStr(String probeUpdateString, boolean isInitializingCacheList) throws IOException {
List<String> params = new ArrayList<String>(2);
params.add(Constants.PROBE_MODIFY_REQUEST);
params.add(probeUpdateString);
List<String> headers = new ArrayList<String>(2);
headers.add("Long-Pulling-Timeout");
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 {
// 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.
// 发起长轮询请求,请求路径为:/nacos/v1/ns/configs/listener
long readTimeoutMs = timeout + (long) Math.round(timeout >> 1);
HttpResult result = agent.httpPost(Constants.CONFIG_CONTROLLER_PATH + "/listener", headers, params,
agent.getEncode(), readTimeoutMs);
if (HttpURLConnection.HTTP_OK == result.code) {
setHealthServer(true);
// 成功则解析响应内容,返回变化的groupKey
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;
}
return Collections.emptyList();
}
服务端处理长轮询请求
com.alibaba.nacos.config.server.controller.ConfigController#listener
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");
}
log.info("listen config id:" + probeModify);
probeModify = URLDecoder.decode(probeModify, Constants.ENCODE);
Map<String, String> clientMd5Map;
try {
// 解析字符串
clientMd5Map = MD5Util.getClientMd5Map(probeModify);
} catch (Throwable e) {
throw new IllegalArgumentException("invalid probeModify");
}
log.info("listen config id 2:" + probeModify);
// do long-polling
// 执行长轮询
inner.doPollingConfig(request, response, clientMd5Map, probeModify.length());
}
com.alibaba.nacos.config.server.controller.ConfigServletInner#doPollingConfig
public String doPollingConfig(HttpServletRequest request, HttpServletResponse response,
Map<String, String> clientMd5Map, int probeRequestSize)
throws IOException {
// 根据请求头判断是否存在Long-Pulling-Timeout,如果有则为长轮询
if (LongPollingService.isSupportLongPolling(request)) {
// 长轮询,阻塞请求的返回
longPollingService.addLongPollingClient(request, response, clientMd5Map, probeRequestSize);
return HttpServletResponse.SC_OK + "";
}
// 短轮询:遍历配置的md5,返回发生变化的groupKey
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);
}
Loggers.AUTH.info("new 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 + "";
}
这里实现了长轮询加短轮询的方式,普通请求直接调用的短轮询,直接将结果放到content中返回,由客户端直接解析,下面具体看下长轮询方式
com.alibaba.nacos.config.server.service.LongPollingService#addLongPollingClient
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);
/**
* 提前500ms返回响应,为避免客户端超时 @qiaoyi.dingqy 2013.10.22改动 add delay time for LoadBalance
*/
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();
// 通过比较md5返回发生变化的groupKey
List<String> changedGroups = MD5Util.compareMd5(req, rsp, clientMd5Map);
// 如果发生变化key列表大于0,直接返回数据,结束长轮询
if (changedGroups.size() > 0) {
generateResponse(req, rsp, changedGroups);
LogUtil.clientLog.info("{}|{}|{}|{}|{}|{}|{}",
System.currentTimeMillis() - start, "instant", RequestUtil.getRemoteIp(req), "polling",
clientMd5Map.size(), probeRequestSize, changedGroups.size());
return;
} else if (noHangUpFlag != null && noHangUpFlag.equalsIgnoreCase(TRUE_STR)) {
LogUtil.clientLog.info("{}|{}|{}|{}|{}|{}|{}", System.currentTimeMillis() - start, "nohangup",
RequestUtil.getRemoteIp(req), "polling", clientMd5Map.size(), probeRequestSize,
changedGroups.size());
return;
}
}
String ip = RequestUtil.getRemoteIp(req);
// 把当前请求转化为一个异步请求,此时意味着tomcat线程已经被释放,请求需要通过asyncContext手动触发返回,否则客户端会一直挂起
final AsyncContext asyncContext = req.startAsync();
// AsyncContext.setTimeout()的超时时间不准,所以只能自己控制
asyncContext.setTimeout(0L);
// 异步执行长轮询请求
scheduler.execute(
new ClientLongPolling(asyncContext, clientMd5Map, ip, probeRequestSize, timeout, appName, tag));
}
这里的主要逻辑如下:
1、从请求头中读取客户端设置的Long-Pulling-Timeout超时时间(默认30s),然后计算超时时间(10s和超时时间-500ms取最大值,这里计算完后默认为29.5s),保证在超时前能响应给客户端
2、判断isFixedPolling(固定长轮询)是否为true,为true重新计算超时时间(10s和设置的时间取最大值(默认10s),这里计算完后默认为10s);为false走第3点
3、isFixedPolling为false则通过md5比较配置,如果有变更的配置,直接返回给客户端,结束长轮询;无变更配置走第4点
4、配置无修改则判断请求头的Long-Pulling-Timeount-No-Hangup不为空并且为true的话,直接返回
5、如果上面的逻辑都没返回,则通过请求创建AsyncContext异步处理,并通过线程池创建一个新的长轮询任务
com.alibaba.nacos.config.server.service.LongPollingService.ClientLongPolling#run
public void run() {
asyncTimeoutFuture = scheduler.schedule(new Runnable() {
@Override
public void run() {
try {
getRetainIps().put(ClientLongPolling.this.ip, System.currentTimeMillis());
// 移除队列中当前的长轮询实例
allSubs.remove(ClientLongPolling.this);
if (isFixedPolling()) {
// 如果为固定长轮询,则通过md5比较找到更新的groupKey,返回
LogUtil.clientLog.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.clientLog.info("{}|{}|{}|{}|{}|{}",
(System.currentTimeMillis() - createTime),
"timeout", RequestUtil.getRemoteIp((HttpServletRequest)asyncContext.getRequest()),
"polling",
clientMd5Map.size(), probeRequestSize);
sendResponse(null);
}
} catch (Throwable t) {
LogUtil.defaultLog.error("long polling error:" + t.getMessage(), t.getCause());
}
}
}, timeoutTime, TimeUnit.MILLISECONDS);
// 添加该长轮询实例到队列中
allSubs.add(this);
}
从这段代码可以看出,长轮询则等待timeoutTime的时长,直接返回配置没有改变的结果。那么如果是固定的在timeoutTime时间后,直接返回配置没有改变的结果,那又是如何做到配置一有改变,客户端就立马能收到响应的呢?这时,我看见有这么一段代码:allSubs.add(this)。而在延迟执行的run方法中,对应的有allSubs.remove(ClientLongPolling.this),这是在干啥呢?
为了一探究竟,先要看看allSubs是啥。
allSubs是一个对列,那哪里用了这个队列呢?这个确实不好找了,但是我想到,这个队列里存放的是ClientLongPolling对象,如果要执行什么方法的话,肯定也是这个对象了里的方法。经过查找,发现最有希望的是sendResponse方法。通过查找该方法的上一级,可以看到在com.alibaba.nacos.config.server.service.LongPollingService.DataChangeTask这个方法里有进行调用。顺着DataChangeTask往上跟,可以看到LongPollingService注册了个LocalDataChangeEvent监听器。
// LongPollingService继承了AbstractEventListener,通过重写添加了监听器
public class LongPollingService extends AbstractEventListener {
public List<Class<? extends Event>> interest() {
List<Class<? extends Event>> eventTypes = new ArrayList<Class<? extends Event>>();
// 添加了LocalDataChangeEvent监听器
eventTypes.add(LocalDataChangeEvent.class);
return eventTypes;
}
public void onEvent(Event event) {
if (isFixedPolling()) {
// ignore
} else {
if (event instanceof LocalDataChangeEvent) {
LocalDataChangeEvent evt = (LocalDataChangeEvent)event;
// 通过线程池执行DataChangeTask任务
scheduler.execute(new DataChangeTask(evt.groupKey, evt.isBeta, evt.betaIps));
}
}
}
}
com.alibaba.nacos.config.server.service.LongPollingService.DataChangeTask#run
public void run() {
try {
ConfigService.getContentBetaMd5(groupKey);
// 遍历长轮询实例队列
for (Iterator<ClientLongPolling> iter = allSubs.iterator(); iter.hasNext(); ) {
ClientLongPolling clientSub = iter.next();
// 找到符合的长轮询实例
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());
// 从allSubs队列中移除该长轮询实例
iter.remove();
LogUtil.clientLog.info("{}|{}|{}|{}|{}|{}|{}",
(System.currentTimeMillis() - changeTime),
"in-advance",
RequestUtil.getRemoteIp((HttpServletRequest)clientSub.asyncContext.getRequest()),
"polling",
clientSub.clientMd5Map.size(), clientSub.probeRequestSize, groupKey);
// 发送返回信息(判断延时调度任务是否取消,没取消的话先取消,然后返回数据)
clientSub.sendResponse(Arrays.asList(groupKey));
}
}
} catch (Throwable t) {
LogUtil.defaultLog.error("data change error:" + t.getMessage(), t.getCause());
}
}
那么到这可以得出在延时的这段时间内,客户端如何动态感知配置发生变化呢?
猜想:肯定是配置文件修改之后,经过一系列的操作之后,触发了LocalDataChangeEvent事件,然后订阅了这个事件的LongPollingService会根据allSubs中监听的配置key进行对比,匹配则返回配置已被修改的结果,最终通过response进行响应,达到一有修改就立马返回到client端的效果。至于LocalDataChangeEvent事件如何发出,现在先不考虑,毕竟发布订阅模式做的就是解耦,那个是另一个功能的事情了。
况且在服务端这里可以看到一系列的isFixedPolling判断。这个意思为固定长轮询:在客户端进行请求的时候先不做处理(只设置超时时间,默认为10S),在延迟任务里会通过比较md5返回有变更的groupKey。因此在延时的这段期间内如果配置发生了变更不会推送给客户端,并且从事件监听com.alibaba.nacos.config.server.service.LongPollingService#onEvent这个方法中可以看到如果为固定长轮询是没有LocalDataChangeEvent事件任务处理的,只有非固定长轮询才会执行任务。
public void onEvent(Event event) {
if (isFixedPolling()) {
// ignore
} else {
if (event instanceof LocalDataChangeEvent) {
LocalDataChangeEvent evt = (LocalDataChangeEvent)event;
scheduler.execute(new DataChangeTask(evt.groupKey, evt.isBeta, evt.betaIps));
}
}
}
总结
通过查看整个配置中心的源码,可以看出Nacos的配置中心实现了客户端的拉+服务端的推,具体的步骤如下:
1、客户端发起请求
2、服务端收到请求后,通过md5比较是否发生了变更;若发生了变更,直接返回数据,回到步骤1,若没发生变更,直接将请求挂起
3、如果在这期间数据一直没变更,则结束本次请求;如果有发生变更,则服务端会轮询监听这个配置项请求实例,直接返回数据
值得注意的是,这里服务端不会返回content,只会返回groupKey(dataId+groupId)