Nacos:配置中心
本次使用nacos的版本是1.4.1
在spring cloud中,要想使用nacos的配置管理功能,需要引入如下:
<!--nacos-config-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
作为一个starter,首先关注的就是它的META_INF/spring.factories
文件的内容,看看引入该starter会自动装配哪些bean
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
com.alibaba.cloud.nacos.NacosConfigBootstrapConfiguration
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.alibaba.cloud.nacos.NacosConfigAutoConfiguration,\
com.alibaba.cloud.nacos.endpoint.NacosConfigEndpointAutoConfiguration
org.springframework.boot.diagnostics.FailureAnalyzer=\
com.alibaba.cloud.nacos.diagnostics.analyzer.NacosConnectionFailureAnalyzer
org.springframework.boot.env.PropertySourceLoader=\
com.alibaba.cloud.nacos.parser.NacosJsonPropertySourceLoader,\
com.alibaba.cloud.nacos.parser.NacosXmlPropertySourceLoader
nacos作为配置中心,不仅可以适应多环境配置信息的灵活切换,同时也支持配置的动态刷新
初始化配置信息
根据starter中META_INF/spring.factories
文件的内容,可以知道,springBoot启动的时候,会自动加载com.alibaba.cloud.nacos.NacosConfigBootstrapConfiguration
这个类,从nacos服务端获取配置信息,该类注入了三个bean,NacosConfigProperties、NacosConfigManager、NacosPropertySourceLocator
,其中NacosConfigProperties
是收集我们的配置信息的,NacosConfigManager
的主要作用是创建了NacosConfigService
的实例,重点关注是NacosPropertySourceLocator
,它实现了org.springframework.cloud.bootstrap.config.PropertySourceLocator
接口,重写了locate方法实现了配置信息的初始化,NacosPropertySourceLocator#locate
方法是实现如下:
public PropertySource<?> locate(Environment env) {
this.nacosConfigProperties.setEnvironment(env);
// 从配置管理器中获取配置服务
ConfigService configService = this.nacosConfigManager.getConfigService();
if (null == configService) {
log.warn("no instance of config service found, can't load config from nacos");
return null;
} else {
long timeout = (long)this.nacosConfigProperties.getTimeout();
this.nacosPropertySourceBuilder = new NacosPropertySourceBuilder(configService, timeout);
String name = this.nacosConfigProperties.getName();
String dataIdPrefix = this.nacosConfigProperties.getPrefix();
if (StringUtils.isEmpty(dataIdPrefix)) {
dataIdPrefix = name;
}
// 没有配置前缀,默认使用spring.application.name的值
if (StringUtils.isEmpty(dataIdPrefix)) {
dataIdPrefix = env.getProperty("spring.application.name");
}
CompositePropertySource composite = new CompositePropertySource("NACOS");
// 先加载共享配置
this.loadSharedConfiguration(composite);
// 再加载拓展配置
this.loadExtConfiguration(composite);
// 最后加载应用配置
this.loadApplicationConfiguration(composite, dataIdPrefix, this.nacosConfigProperties, env);
return composite;
}
}
从这里也可以看出配置的优先级,后面加载的会覆盖前面的配置,其配置的优先级是:应用配置 > 拓展配置 > 共享配置
三种配置加载,最终都是通过
NacosPropertySourceLocator#loadNacosDataIfPresent
来实现的:
private void loadNacosDataIfPresent(CompositePropertySource composite, String dataId, String group, String fileExtension, boolean isRefreshable) {
if (null != dataId && dataId.trim().length() >= 1) {
if (null != group && group.trim().length() >= 1) {
// 核心代码,加载nacos服务端的配置信息
NacosPropertySource propertySource = this.loadNacosPropertySource(dataId, group, fileExtension, isRefreshable);
this.addFirstPropertySource(composite, propertySource, false);
}
}
}
接着看NacosPropertySourceLocator#loadNacosPropertySource
private NacosPropertySource loadNacosPropertySource(String dataId, String group, String fileExtension, boolean isRefreshable) {
return NacosContextRefresher.getRefreshCount() != 0L && !isRefreshable ? NacosPropertySourceRepository.getNacosPropertySource(dataId, group) : this.nacosPropertySourceBuilder.build(dataId, group, fileExtension, isRefreshable);
}
如果不用刷新,就使用本地文件系统存储的快照配置信息,否则就走动态刷新的途径获取配置信息。我们以动态刷新为例,往下走,来到NacosPropertySourceBuilder#build
NacosPropertySource build(String dataId, String group, String fileExtension, boolean isRefreshable) {
// 从nacos服务端加载配置信息
List<PropertySource<?>> propertySources = this.loadNacosData(dataId, group, fileExtension);
// 封装配置信息
NacosPropertySource nacosPropertySource = new NacosPropertySource(propertySources, group, dataId, new Date(), isRefreshable);
// 将配置信息收集到NacosPropertySourceRepository
NacosPropertySourceRepository.collectNacosPropertySource(nacosPropertySource);
return nacosPropertySource;
}
重点看加载配置信息的过程 NacosPropertySourceBuilder#loadNacosData
private List<PropertySource<?>> loadNacosData(String dataId, String group, String fileExtension) {
String data = null;
try {
// 通过configService加载配置信息
data = this.configService.getConfig(dataId, group, this.timeout);
if (StringUtils.isEmpty(data)) {
log.warn("Ignore the empty nacos configuration and get it based on dataId[{}] & group[{}]", dataId, group);
return Collections.emptyList();
}
if (log.isDebugEnabled()) {
log.debug(String.format("Loading nacos data, dataId: '%s', group: '%s', data: %s", dataId, group, data));
}
// 解析配置信息,并封装成List<PropertySource<?>>
return NacosDataParserHandler.getInstance().parseNacosData(dataId, data, fileExtension);
} catch (NacosException var6) {
log.error("get data from Nacos error,dataId:{} ", dataId, var6);
} catch (Exception var7) {
log.error("parse data from Nacos error,dataId:{},data:{}", new Object[]{dataId, data, var7});
}
return Collections.emptyList();
}
这个方法的逻辑比较清晰,首先加载配置信息,然后解析配置信息,我们关注加载配置信息的部分,来到NacosConfigService#
getConfig
public String getConfig(String dataId, String group, long timeoutMs) throws NacosException {
return this.getConfigInner(this.namespace, dataId, group, timeoutMs);
}
getConfigInner
方法里面会调用ClientWorker#getServerConfig
String[] ct = this.worker.getServerConfig(dataId, group, tenant, timeoutMs);
跟进去,看一下ClientWorker#getServerConfig
public String[] getServerConfig(String dataId, String group, String tenant, long readTimeout) throws NacosException {
String[] ct = new String[2];
if (StringUtils.isBlank(group)) {
group = "DEFAULT_GROUP";
}
HttpRestResult result = null;
try {
Map<String, String> params = new HashMap(3);
if (StringUtils.isBlank(tenant)) {
params.put("dataId", dataId);
params.put("group", group);
} else {
params.put("dataId", dataId);
params.put("group", group);
params.put("tenant", tenant);
}
// 通过http请求拉取配置信息
result = this.agent.httpGet("/v1/cs/configs", (Map)null, params, this.agent.getEncode(), readTimeout);
} catch (Exception var10) {
String message = String.format("[%s] [sub-server] get server config exception, dataId=%s, group=%s, tenant=%s", this.agent.getName(), dataId, group, tenant);
LOGGER.error(message, var10);
throw new NacosException(500, var10);
}
// 处理结果
switch(result.getCode()) {
// 正常返回
case 200:
// 保存快照到本地文件系统
LocalConfigInfoProcessor.saveSnapshot(this.agent.getName(), dataId, group, tenant, (String)result.getData());
ct[0] = (String)result.getData(); // 配置信息的内容
if (result.getHeader().getValue("Config-Type") != null) {
ct[1] = result.getHeader().getValue("Config-Type");
} else {
ct[1] = ConfigType.TEXT.getType();
}
return ct;
case 403:
LOGGER.error("[{}] [sub-server-error] no right, dataId={}, group={}, tenant={}", new Object[]{this.agent.getName(), dataId, group, tenant});
throw new NacosException(result.getCode(), result.getMessage());
case 404:
LocalConfigInfoProcessor.saveSnapshot(this.agent.getName(), dataId, group, tenant, (String)null);
return ct;
case 409:
LOGGER.error("[{}] [sub-server-error] get server config being modified concurrently, dataId={}, group={}, tenant={}", new Object[]{this.agent.getName(), dataId, group, tenant});
throw new NacosException(409, "data being modified, dataId=" + dataId + ",group=" + group + ",tenant=" + tenant);
default:
LOGGER.error("[{}] [sub-server-error] dataId={}, group={}, tenant={}, code={}", new Object[]{this.agent.getName(), dataId, group, tenant, result.getCode()});
throw new NacosException(result.getCode(), "http error, code=" + result.getCode() + ",dataId=" + dataId + ",group=" + group + ",tenant=" + tenant);
}
}
这里就是通过http请求/v1/cs/configs来拉取配置信息,同时配置信息保存到本地文件。到这里就很明了了,nacos客户端配置信息的获取,本质是通过http请求和nacos服务端进行通信。
动态刷新配置信息
根据starter中META_INF/spring.factories
文件的内容,可以知道,springBoot启动的时候,会自动加载com.alibaba.cloud.nacos.NacosConfigAutoConfiguration
,nacos的动态刷新配置入口就在这里,该类会自动注入NacosContextRefresher
,而NacosContextRefresher
实现了 ApplicationListener<ApplicationReadyEvent>
接口,所以在监听到ApplicationReadyEvent
事件时,会触发onApplicationEvent
方法,NacosContextRefresher#onApplicationEvent
方法如下
public void onApplicationEvent(ApplicationReadyEvent event) {
if (this.ready.compareAndSet(false, true)) {
this.registerNacosListenersForApplications();
}
}
registerNacosListenersForApplications
方法会调用该类的registerNacosListener
private void registerNacosListener(String groupKey, String dataKey) {
// 构建key
String key = NacosPropertySourceRepository.getMapKey(dataKey, groupKey);
Listener listener = (Listener)this.listenerMap.computeIfAbsent(key, (lst) -> {
return new AbstractSharedListener() {
public void innerReceive(String dataId, String group, String configInfo) {
NacosContextRefresher.refreshCountIncrement();
NacosContextRefresher.this.nacosRefreshHistory.addRefreshRecord(dataId, group, configInfo);
NacosContextRefresher.this.applicationContext.publishEvent(new RefreshEvent(this, (Object)null, "Refresh Nacos config"));
if (NacosContextRefresher.log.isDebugEnabled()) {
NacosContextRefresher.log.debug(String.format("Refresh Nacos config group=%s,dataId=%s,configInfo=%s", group, dataId, configInfo));
}
}
};
});
try {
// 添加监听器
this.configService.addListener(dataKey, groupKey, listener);
} catch (NacosException var6) {
log.warn(String.format("register fail for nacos listener ,dataId=[%s],group=[%s]", dataKey, groupKey), var6);
}
}
NacosPropertySourceRepository就是初始化配置信息后存放的位置,现在重点关注如下代码
// 添加监听器
this.configService.addListener(dataKey, groupKey, listener);
NacosConfigService#addListener会调用ClientWorker#addTenantListeners
public void addTenantListeners(String dataId, String group, List<? extends Listener> listeners) throws NacosException {
group = this.null2defaultGroup(group);
String tenant = this.agent.getTenant();
CacheData cache = this.addCacheDataIfAbsent(dataId, group, tenant);
Iterator var6 = listeners.iterator();
while(var6.hasNext()) {
Listener listener = (Listener)var6.next();
// 添加监听器
cache.addListener(listener);
}
}
这里又调用了CacheData#addListener,只是封装成CacheData.ManagerListenerWrap
,然后存储在private final CopyOnWriteArrayList<CacheData.ManagerListenerWrap> listeners
该属性中。
我们把焦点转移到ClientWorker:
public ClientWorker(final HttpAgent agent, ConfigFilterChainManager configFilterChainManager, Properties properties) {
this.agent = agent;
this.configFilterChainManager = configFilterChainManager;
this.init(properties);
this.executor = Executors.newScheduledThreadPool(1, new ThreadFactory() {
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName("com.alibaba.nacos.client.Worker." + agent.getName());
t.setDaemon(true);
return t;
}
});
this.executorService = Executors.newScheduledThreadPool(Runtime.getRuntime().availableProcessors(), new ThreadFactory() {
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName("com.alibaba.nacos.client.Worker.longPolling." + agent.getName());
t.setDaemon(true);
return t;
}
});
this.executor.scheduleWithFixedDelay(new Runnable() {
public void run() {
try {
ClientWorker.this.checkConfigInfo();
} catch (Throwable var2) {
ClientWorker.LOGGER.error("[" + agent.getName() + "] [sub-check] rotate check error", var2);
}
}
}, 1L, 10L, TimeUnit.MILLISECONDS);
}
在其构造器中,定义了两个线程池,其中线程池executor每隔10毫秒执行一次调度任务,线程池executorService执行检查配置更新任务,所以关注更新配置代码
ClientWorker.this.checkConfigInfo();
public void checkConfigInfo() {
// 监听器的数量
int listenerSize = this.cacheMap.size();
// 长轮询任务的数量,计算方法是:将需要刷新的数据分组,每3000个为一组,一组由一个线程处理,每组一个taskId
int longingTaskCount = (int)Math.ceil((double)listenerSize / ParamUtil.getPerTaskConfigSize());
if ((double)longingTaskCount > this.currentLongingTaskCount) {
for(int i = (int)this.currentLongingTaskCount; i < longingTaskCount; ++i) {
this.executorService.execute(new ClientWorker.LongPollingRunnable(i));
}
this.currentLongingTaskCount = (double)longingTaskCount;
}
}
这段代码刚看的时候有些费解,仔细想一下就会发现,只是进行了分组而已,应该是为了避免启动过多的线程
同时可以看到,这里执行的任务是ClientWorker的内部类LongPollingRunnable,LongPollingRunnable实现了Runnable接口,所以主要关注run方法
// LongPollingRunnable中run方法代码比较多,只拿了部分代码
// 检查配置信息发生变化的groupkeys,其实就是dataId和group
List<String> changedGroupKeys = ClientWorker.this.checkUpdateDataIds(cacheDatas, inInitializingCacheList);
if (!CollectionUtils.isEmpty(changedGroupKeys)) {
ClientWorker.LOGGER.info("get changedGroupKeys:" + changedGroupKeys);
}
// 下面就是根据groupkeys到服务端拉取发生变更的配置信息
Iterator var16 = changedGroupKeys.iterator();
while(var16.hasNext()) {
String groupKey = (String)var16.next();
String[] key = GroupKey.parseKey(groupKey);
String dataId = key[0];
String group = key[1];
String tenant = null;
if (key.length == 3) {
tenant = key[2];
}
try {
// 根据dataId和group到服务端拉取发生变更的配置信息
String[] ct = ClientWorker.this.getServerConfig(dataId, group, tenant, 3000L);
CacheData cache = (CacheData)ClientWorker.this.cacheMap.get(GroupKey.getKeyTenant(dataId, group, tenant));
// 更新配置信息
cache.setContent(ct[0]);
if (null != ct[1]) {
cache.setType(ct[1]);
}
ClientWorker.LOGGER.info("[{}] [data-received] dataId={}, group={}, tenant={}, md5={}, content={}, type={}", new Object[]{ClientWorker.this.agent.getName(), dataId, group, tenant, cache.getMd5(), ContentUtils.truncateContent(ct[0]), ct[1]});
} catch (NacosException var12) {
String message = String.format("[%s] [get-update] get changed config exception. dataId=%s, group=%s, tenant=%s", ClientWorker.this.agent.getName(), dataId, group, tenant);
ClientWorker.LOGGER.error(message, var12);
}
}
这里主要分为两大步骤:
- 检查配置信息发生变化的groupkeys,其实就是dataId和group
- 根据dataId和group到服务端拉取发生变更的配置信息
拉取配置信息,和初始化配置信息是一样的,通过http调用服务端开放的api,所以重点在于它是知道配置信息发生了变更的?也就是第一步是如何进行的。
首先是看到是调用了checkUpdateDataIds方法,该方法又调用了checkUpdateConfigStr方法
List<String> checkUpdateConfigStr(String probeUpdateString, boolean isInitializingCacheList) throws Exception {
Map<String, String> params = new HashMap(2);
params.put("Listening-Configs", probeUpdateString);
Map<String, String> headers = new HashMap(2);
// 在请求头添加长轮询的超时时间
headers.put("Long-Pulling-Timeout", "" + this.timeout);
if (isInitializingCacheList) {
headers.put("Long-Pulling-Timeout-No-Hangup", "true");
}
if (StringUtils.isBlank(probeUpdateString)) {
return Collections.emptyList();
} else {
try {
// 设置http请求的超时时间,大约是长轮询超时时间的1.5倍
long readTimeoutMs = this.timeout + (long)Math.round((float)(this.timeout >> 1));
HttpRestResult<String> result = this.agent.httpPost("/v1/cs/configs/listener", headers, params, this.agent.getEncode(), readTimeoutMs);
if (result.ok()) {
this.setHealthServer(true);
return this.parseUpdateDataIdResponse((String)result.getData());
}
this.setHealthServer(false);
LOGGER.error("[{}] [check-update] get changed dataId error, code: {}", this.agent.getName(), result.getCode());
} catch (Exception var8) {
this.setHealthServer(false);
LOGGER.error("[" + this.agent.getName() + "] [check-update] get changed dataId exception", var8);
throw var8;
}
return Collections.emptyList();
}
}
可以看到,nacos客户端感受服务端配置信息是否发生变更,是通过http长轮询实现的,长轮询的超时默认是ClientWorker类的timeout属性,timeout在init方法设置了值,而ClientWorker的构造器中会执行init方法,从而设置检查配置的时间,最少是10秒,默认是30秒
private void init(Properties properties) {
this.timeout = (long)Math.max(ConvertUtils.toInt(properties.getProperty("configLongPollTimeout"), 30000), 10000);
this.taskPenaltyTime = ConvertUtils.toInt(properties.getProperty("configRetryTime"), 2000);
this.enableRemoteSyncConfig = Boolean.parseBoolean(properties.getProperty("enableRemoteSyncConfig"));
}
在根据dataId和group到服务端拉取发生变更的配置信息后,是如何将配置信息更新到我们应用中的呢?注意到在将配置信息更新到了CacheData对象的content属性之后,调用了CacheData对象的checkListenerMd5方法
void checkListenerMd5() {
Iterator var1 = this.listeners.iterator();
// 遍历所有的监听器
while(var1.hasNext()) {
CacheData.ManagerListenerWrap wrap = (CacheData.ManagerListenerWrap)var1.next();
// 对比配置信息的md5,如果不一致,就是配置信息发生了变化,通知监听器处理
if (!this.md5.equals(wrap.lastCallMd5)) {
this.safeNotifyListener(this.dataId, this.group, this.content, this.type, this.md5, wrap);
}
}
}
可以看到,当配置信息发生了变化,就会通知监听器进行处理,CacheData#safeNotifyListener方法的部分代码
Thread.currentThread().setContextClassLoader(appClassLoader);
ConfigResponse cr = new ConfigResponse();
cr.setDataId(dataId);
cr.setGroup(group);
cr.setContent(content);
CacheData.this.configFilterChainManager.doFilter((IConfigRequest)null, cr);
String contentTmp = cr.getContent();
listener.receiveConfigInfo(contentTmp);
可以看到,这里会触发监听器的回调方法receiveConfigInfo,这样一来,监听器的持有者就可以获取到最新的配置信息了
配置信息的热部署
经过前面的分析,可以知道,当配置信息发生了变化,就会触发监听器的回调方法receiveConfigInfo
listener.receiveConfigInfo(contentTmp);
那么这个回调方法是如何处理的呢?它怎么就实现了配置信息的热部署呢?如果你跟踪源码的细心一点,就会留意到NacosContextRefresher的registerNacosListener方法,就是添加监听器的源头,其传递路径是这样的:
- NacosContextRefresher#registerNacosListener
- NacosConfigService#addListener
- ClientWorker#addTenantListeners
- CacheData#addListener
下面看一下NacosContextRefresher#registerNacosListener
private void registerNacosListener(String groupKey, String dataKey) {
String key = NacosPropertySourceRepository.getMapKey(dataKey, groupKey);
Listener listener = (Listener)this.listenerMap.computeIfAbsent(key, (lst) -> {
return new AbstractSharedListener() {
public void innerReceive(String dataId, String group, String configInfo) {
NacosContextRefresher.refreshCountIncrement();
NacosContextRefresher.this.nacosRefreshHistory.addRefreshRecord(dataId, group, configInfo);
NacosContextRefresher.this.applicationContext.publishEvent(new RefreshEvent(this, (Object)null, "Refresh Nacos config"));
if (NacosContextRefresher.log.isDebugEnabled()) {
NacosContextRefresher.log.debug(String.format("Refresh Nacos config group=%s,dataId=%s,configInfo=%s", group, dataId, configInfo));
}
}
};
});
try {
this.configService.addListener(dataKey, groupKey, listener);
} catch (NacosException var6) {
log.warn(String.format("register fail for nacos listener ,dataId=[%s],group=[%s]", dataKey, groupKey), var6);
}
}
这里调用的是innerReceive,并不是receiveConfigInfo,因为receiveConfigInfo方法调用了innerReceive方法,所以最终会调用innerReceive方法
public abstract class AbstractSharedListener implements Listener {
private volatile String dataId;
private volatile String group;
public AbstractSharedListener() {
}
public final void fillContext(String dataId, String group) {
this.dataId = dataId;
this.group = group;
}
public final void receiveConfigInfo(String configInfo) {
this.innerReceive(this.dataId, this.group, configInfo);
}
public Executor getExecutor() {
return null;
}
public abstract void innerReceive(String var1, String var2, String var3);
}
Nacos配置信息发生了变化,要怎样通知程序,我的配置发生了变化呢?答案是通过事件驱动来实现的,关键是在监听器的回调中发布了事件:
NacosContextRefresher.this.applicationContext.publishEvent(new RefreshEvent(this, (Object)null, "Refresh Nacos config"));
可以看到,当配置信息发生变化后,就会发布一个RefreshEvent事件,而RefreshEventListener会监听到该事件进行处理:
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ApplicationReadyEvent) {
handle((ApplicationReadyEvent) event);
}
else if (event instanceof RefreshEvent) {
handle((RefreshEvent) event);
}
}
public void handle(RefreshEvent event) {
if (this.ready.get()) { // don't handle events before app is ready
log.debug("Event received " + event.getEventDesc());
Set<String> keys = this.refresh.refresh();
log.info("Refresh keys changed: " + keys);
}
}
重点是这一行代码
Set<String> keys = this.refresh.refresh();
它是委托给ContextRefresher#refresh进行处理
public synchronized Set<String> refresh() {
Set<String> keys = refreshEnvironment();
this.scope.refreshAll();
return keys;
}
public synchronized Set<String> refreshEnvironment() {
// 修改之前的配置
Map<String, Object> before = extract(this.context.getEnvironment().getPropertySources());
// 更新配置
updateEnvironment();
// 发生变化的配置
Set<String> keys = changes(before, extract(this.context.getEnvironment().getPropertySources())).keySet();
// 发布EnvironmentChangeEvent事件
this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys));
return keys;
}
修改之前的配置
发生变更的key,因为我只修改了test.msg这个key对应的值
拿到了发生变化的key之后,就执行刷新逻辑,也就是这一行代码
this.scope.refreshAll();
来到RefreshScope#refreshAll
@ManagedOperation(description = "Dispose of the current instance of all beans "
+ "in this scope and force a refresh on next method execution.")
public void refreshAll() {
super.destroy();
this.context.publishEvent(new RefreshScopeRefreshedEvent());
}
根据描述,也可以知道,它会销毁当前所有bean的实例,并且在下一次方法执行的时候,强制刷新(也就是用新的配置,创建bean)
这样也就清晰了:
- 配置信息发生变化时,销毁bean
- 再次调用时,使用新的配置创建bean,配置信息也就实现了热部署