文章目录
前言
前序文章介绍了nacos配置中心的基本应用和进阶应用,为了更流畅的应用,本文将介绍nacos配置中心的基本原理。
一、nacos变更监听
1、客户端
每个版本的nacos代码都有一定的差异点,但整体架构是不变的,本文就1.4.2版本进行解读
<dependency>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-client</artifactId>
<version>1.4.2</version>
</dependency>
<dependency>
<groupId>com.alibaba.boot</groupId>
<artifactId>nacos-config-spring-boot-starter</artifactId>
<version>0.2.12</version>
</dependency>
1.1、自动装配
从springboot的自动装配出发,nacos-starter下面的spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.alibaba.boot.nacos.config.autoconfigure.NacosConfigAutoConfiguration
#org.springframework.context.ApplicationContextInitializer=\
# com.alibaba.boot.nacos.config.autoconfigure.NacosConfigApplicationContextInitializer
# 环境变量后置处理器
org.springframework.boot.env.EnvironmentPostProcessor=\
com.alibaba.boot.nacos.config.autoconfigure.NacosConfigEnvironmentProcessor,\
com.alibaba.boot.nacos.config.support.MultiProfilesYamlConfigParseSupport
org.springframework.context.ApplicationListener=\
com.alibaba.boot.nacos.config.logging.NacosLoggingListener
通过后置处理器NacosConfigEnvironmentProcessor,往spring里添加nacos初始化器
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment,
SpringApplication application) {
// spring里添加nacos初始化器
application.addInitializers(new NacosConfigApplicationContextInitializer(this));
// 加载nacos元数据
nacosConfigProperties = NacosConfigPropertiesUtils
.buildNacosConfigProperties(environment);
if (enable()) {
System.out.println(
"[Nacos Config Boot] : The preload log configuration is enabled");
loadConfig(environment);
NacosConfigLoader nacosConfigLoader = NacosConfigLoaderFactory.getSingleton(nacosConfigProperties, environment, builder);
LogAutoFreshProcess.build(environment, nacosConfigProperties, nacosConfigLoader, builder).process();
}
}
接下来初始化器NacosConfigApplicationContextInitializer开始初始化
// NacosConfigApplicationContextInitializer
public void initialize(ConfigurableApplicationContext context) {
singleton.setApplicationContext(context);
environment = context.getEnvironment();
nacosConfigProperties = NacosConfigPropertiesUtils
.buildNacosConfigProperties(environment);
final NacosConfigLoader configLoader = NacosConfigLoaderFactory.getSingleton(
nacosConfigProperties, environment, builder);
if (!enable()) {
logger.info("[Nacos Config Boot] : The preload configuration is not enabled");
}
else {
// If it opens the log level loading directly will cache
// DeferNacosPropertySource release
if (processor.enable()) {
processor.publishDeferService(context);
configLoader
.addListenerIfAutoRefreshed(processor.getDeferPropertySources());
}
else {
// 远程访问nacos配置中心读取配置数据
configLoader.loadConfig();
// 设置监听器来监听nacos配置中心数据的变更并更新到本地
configLoader.addListenerIfAutoRefreshed();
}
}
final ConfigurableListableBeanFactory factory = context.getBeanFactory();
if (!factory
.containsSingleton(NacosBeanUtils.GLOBAL_NACOS_PROPERTIES_BEAN_NAME)) {
factory.registerSingleton(NacosBeanUtils.GLOBAL_NACOS_PROPERTIES_BEAN_NAME,
configLoader.getGlobalProperties());
}
}
loadConfig
// NacosConfigLoader
public void loadConfig() {
MutablePropertySources mutablePropertySources = environment.getPropertySources();
List<NacosPropertySource> sources = reqGlobalNacosConfig(globalProperties,
nacosConfigProperties.getType());
for (NacosConfigProperties.Config config : nacosConfigProperties.getExtConfig()) {
List<NacosPropertySource> elements = reqSubNacosConfig(config,
globalProperties, config.getType());
sources.addAll(elements);
}
// 如果远程nacos的配置数据比本地配置数据的优先级高,则执行以下方法
if (nacosConfigProperties.isRemoteFirst()) {
for (ListIterator<NacosPropertySource> itr = sources.listIterator(sources.size()); itr.hasPrevious();) {
// 这里是个关键点,可以确保远程的配置数据会被优先使用
mutablePropertySources.addAfter(
StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, itr.previous());
}
} else {
for (NacosPropertySource propertySource : sources) {
mutablePropertySources.addLast(propertySource);
}
}
}
addListenerIfAutoRefreshed
// 添加nacos配置中心监听器
public void addListenerIfAutoRefreshed() {
addListenerIfAutoRefreshed(nacosPropertySources);
}
public void addListenerIfAutoRefreshed(
final List<DeferNacosPropertySource> deferNacosPropertySources) {
for (DeferNacosPropertySource deferNacosPropertySource : deferNacosPropertySources) {
NacosPropertySourcePostProcessor.addListenerIfAutoRefreshed(
deferNacosPropertySource.getNacosPropertySource(),
deferNacosPropertySource.getProperties(),
deferNacosPropertySource.getEnvironment());
}
}
public static void addListenerIfAutoRefreshed(final NacosPropertySource nacosPropertySource, Properties properties, final ConfigurableEnvironment environment) {
if (nacosPropertySource.isAutoRefreshed()) {
final String dataId = nacosPropertySource.getDataId();
final String groupId = nacosPropertySource.getGroupId();
final String type = nacosPropertySource.getType();
NacosServiceFactory nacosServiceFactory = NacosBeanUtils.getNacosServiceFactoryBean(beanFactory);
try {
// 创建configService
ConfigService configService = nacosServiceFactory.createConfigService(properties);
Listener listener = new AbstractListener() {
public void receiveConfigInfo(String config) {
String name = nacosPropertySource.getName();
NacosPropertySource newNacosPropertySource = new NacosPropertySource(dataId, groupId, name, config, type);
newNacosPropertySource.copy(nacosPropertySource);
MutablePropertySources propertySources = environment.getPropertySources();
propertySources.replace(name, newNacosPropertySource);
}
};
if (configService instanceof EventPublishingConfigService) {
((EventPublishingConfigService)configService).addListener(dataId, groupId, type, listener);
} else {
configService.addListener(dataId, groupId, listener);
}
} catch (NacosException var9) {
throw new RuntimeException("ConfigService can't add Listener with properties : " + properties, var9);
}
}
}
nacosServiceFactory.createConfigService(properties)
public ConfigService createConfigService(Properties properties) throws NacosException {
Properties copy = new Properties();
copy.putAll(properties);
// 走策略模式返回相应的ConfigService,这里是ConfigCreateWorker
return (ConfigService)
((AbstractCreateWorker)this.createWorkerManager.get(CacheableEventPublishingNacosServiceFactory.ServiceType.CONFIG)).run(copy, (Object)null);
}
class ConfigCreateWorker extends AbstractCreateWorker<ConfigService> {
ConfigCreateWorker() {
}
// ConfigCreateWorker
public ConfigService run(Properties properties, ConfigService service) throws NacosException {
String cacheKey = NacosUtils.identify(properties);
// 从缓存获取,因为是第一次进来,所有不存在
ConfigService configService = (ConfigService)CacheableEventPublishingNacosServiceFactory.this.configServicesCache.get(cacheKey);
if (configService == null) {
if (service == null) {
// 创建ConfigService
service = NacosFactory.createConfigService(properties);
}
configService = new EventPublishingConfigService(service, properties, CacheableEventPublishingNacosServiceFactory.getSingleton().context, CacheableEventPublishingNacosServiceFactory.getSingleton().nacosConfigListenerExecutor);
CacheableEventPublishingNacosServiceFactory.this.configServicesCache.put(cacheKey, configService);
}
return (ConfigService)configService;
}
}
NacosFactory.createConfigService(properties),通过配置信息创建ConfigService
public class ConfigFactory {
/**
* Create Config
*
* @param properties init param
* @return ConfigService
* @throws NacosException Exception
*/
public static ConfigService createConfigService(Properties properties) throws NacosException {
try {
Class<?> driverImplClass = Class.forName("com.alibaba.nacos.client.config.NacosConfigService");
// 获取带Properties参数的构造函数
Constructor constructor = driverImplClass.getConstructor(Properties.class);
// 反射创建
ConfigService vendorImpl = (ConfigService) constructor.newInstance(properties);
return vendorImpl;
} catch (Throwable e) {
throw new NacosException(NacosException.CLIENT_INVALID_PARAM, e);
}
}
}
创建的configService为NacosConfigService
public class NacosConfigService implements ConfigService {
private static final long POST_TIMEOUT = 3000L;
private static final String EMPTY = "";
// Http请求代理
private HttpAgent agent;
// 长轮询
private ClientWorker worker;
private String namespace;
private String encode;
// 创建一个 配置过滤器链管理器
private ConfigFilterChainManager configFilterChainManager = new ConfigFilterChainManager();
public NacosConfigService(Properties properties) throws NacosException {
String encodeTmp = properties.getProperty(PropertyKeyConst.ENCODE);
if (StringUtils.isBlank(encodeTmp)) {
encode = Constants.ENCODE;
} else {
encode = encodeTmp.trim();
}
// 初始化 namespace
initNamespace(properties);
// 初始化一个HttpAgent,这里又用到了装饰起模式,实际工作的类是ServerHttpAgent,
// MetricsHttpAgent内部也是调用了ServerHttpAgent的方法,增加了监控统计的信息
agent = new MetricsHttpAgent(new ServerHttpAgent(properties));
agent.start();
// 创建客户端工作对象,结合agent实现长轮询机制
worker = new ClientWorker(agent, configFilterChainManager, properties);
}
}
1.2 、ClientWorker
public ClientWorker(final HttpAgent agent, ConfigFilterChainManager configFilterChainManager, Properties properties) {
this.agent = agent;
this.configFilterChainManager = configFilterChainManager;
// Initialize the timeout parameter
this.init(properties);
// 初始化一个定时调度的线程池,Worker执行器
this.executor = Executors.newScheduledThreadPool(1, new ThreadFactory() {
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName("com.alibaba.nacos.client.Worker." + agent.getName());
t.setDaemon(true);
return t;
}
});
// 初始化一个定时调度的线程池,长轮询执行器
this.executorService = Executors.newScheduledThreadPool(Runtime.getRuntime().availableProcessors(), new ThreadFactory() {
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName("com.alibaba.nacos.client.Worker.longPolling." + agent.getName());
t.setDaemon(true);
return t;
}
});
// 设置定时任务的执行频率,执行延迟时间为1毫秒、延迟时间为10毫秒,调用checkConfigInfo这个方法,定时去检测配置是否发生了变化
this.executor.scheduleWithFixedDelay(new Runnable() {
public void run() {
try {
ClientWorker.this.checkConfigInfo();
} catch (Throwable var2) {
ClientWorker.LOGGER.error("[" + agent.getName() + "] [sub-check] rotate check error", var2);
}
}
}, 1L, 10L, TimeUnit.MILLISECONDS);
}
private static double perTaskConfigSize = 3000.0;
// 配置信息检查
public void checkConfigInfo() {
// 分任:分批处理
int listenerSize = cacheMap.get().size();
// 向上取整为批数,监听的配置数量除以3000,得到一个整数,代表长轮训任务的数量
int longingTaskCount = (int) Math.ceil(listenerSize / ParamUtil.getPerTaskConfigSize());
if (longingTaskCount > currentLongingTaskCount) {
for (int i = (int) currentLongingTaskCount; i < longingTaskCount; i++) {
// 长轮询, i 为当前批次,用于筛选过滤出属于当前批次的cacheData
executorService.execute(new LongPollingRunnable(i));
}
currentLongingTaskCount = longingTaskCount;
}
}
1.3、 长轮询实现机制
LongPollingRunnable
- 对任务按照批次分类
- 检查当前批次的缓存和本地文件的数据是否一致,如果发生了变化,则触发监听。
class LongPollingRunnable implements Runnable {
// 当前批次id,用于筛选过滤出属于当前批次的cacheData
private int taskId;
public LongPollingRunnable(int taskId) {
this.taskId = taskId;
}
@Override
public void run() {
List<CacheData> cacheDatas = new ArrayList<CacheData>();
List<String> inInitializingCacheList = new ArrayList<String>();
try {
// check local config
// 获取属于当前批次的cacheData
for (CacheData cacheData : cacheMap.get().values()) {
if (cacheData.getTaskId() == taskId) {
cacheDatas.add(cacheData);
try {
// 检查本地配置
checkLocalConfig(cacheData);
if (cacheData.isUseLocalConfigInfo()) { // 使用本地配置信息
// 检查cacheData和内存缓存文件是否不一致,如果不一致,通知所有Listener
cacheData.checkListenerMd5();
}
} catch (Exception e) {
LOGGER.error("get local config info error", e);
}
}
}
// check server config
// 长轮询:将当前批次的所有cacheData通过Http请求发送给服务端,并附带30s超时时间
// 1.服务端数据无变化,请求超时,changedGroupKeys = Collections.emptyList()
// 2.服务端数据存在变更,循环遍历,通过getServerConfig获取并更新本地缓存,触发事件监听
List<String> changedGroupKeys = checkUpdateDataIds(cacheDatas, inInitializingCacheList);
// 遍历发送变更的groupKey
for (String groupKey : changedGroupKeys) {
String[] key = GroupKey.parseKey(groupKey);
String dataId = key[0];
String group = key[1];
String tenant = null;
if (key.length == 3) {
tenant = key[2];
}
try {
// 重新获取服务端配置,本更新本地配置文件缓存内容
String content = getServerConfig(dataId, group, tenant, 3000L);
// 更新本地内存配置
CacheData cache = cacheMap.get().get(GroupKey.getKeyTenant(dataId, group, tenant));
cache.setContent(content);
LOGGER.info("[{}] [data-received] dataId={}, group={}, tenant={}, md5={}, content={}",
agent.getName(), dataId, group, tenant, cache.getMd5(),
ContentUtils.truncateContent(content));
} catch (NacosException ioe) {
String message = String.format(
"[%s] [get-update] get changed config exception. dataId=%s, group=%s, tenant=%s",
agent.getName(), dataId, group, tenant);
LOGGER.error(message, ioe);
}
}
// 遍历cacheDatas,找到发生变化的数据进行通知
for (CacheData cacheData : cacheDatas) {
if (!cacheData.isInitializing() || inInitializingCacheList
.contains(GroupKey.getKeyTenant(cacheData.dataId, cacheData.group, cacheData.tenant))) {
// 检查cacheData和内存缓存文件是否不一致,如果不一致,通知所有Listener
cacheData.checkListenerMd5();
cacheData.setInitializing(false);
}
}
inInitializingCacheList.clear();
executorService.execute(this);
} catch (Throwable e) {
// If the rotation training task is abnormal, the next execution time of the task will be punished
LOGGER.error("longPolling error : ", e);
// 如果发生异常,延迟taskPenaltyTime后执行当前任务
executorService.schedule(this, taskPenaltyTime, TimeUnit.MILLISECONDS);
}
}
}
1.3.1、check local config – 检查本地配置
1、是否使用本地配置checkLocalConfig
2、检查变更并通知checkListenerMd5
private void checkLocalConfig(CacheData cacheData) {
final String dataId = cacheData.dataId;
final String group = cacheData.group;
final String tenant = cacheData.tenant;
File path = LocalConfigInfoProcessor.getFailoverFile(agent.getName(), dataId, group, tenant);
// 没有 -> 有
if (!cacheData.isUseLocalConfigInfo() && path.exists()) {
String content = LocalConfigInfoProcessor.getFailover(agent.getName(), dataId, group, tenant);
final String md5 = MD5Utils.md5Hex(content, Constants.ENCODE);
cacheData.setUseLocalConfigInfo(true);
cacheData.setLocalConfigInfoVersion(path.lastModified());
cacheData.setContent(content);
String encryptedDataKey = LocalEncryptedDataKeyProcessor
.getEncryptDataKeyFailover(agent.getName(), dataId, group, tenant);
cacheData.setEncryptedDataKey(encryptedDataKey);
LOGGER.warn(
"[{}] [failover-change] failover file created. dataId={}, group={}, tenant={}, md5={}, content={}",
agent.getName(), dataId, group, tenant, md5, ContentUtils.truncateContent(content));
return;
}
// 有 -> 没有。不通知业务监听器,从server拿到配置后通知。
// If use local config info, then it doesn't notify business listener and notify after getting from server.
if (cacheData.isUseLocalConfigInfo() && !path.exists()) {
cacheData.setUseLocalConfigInfo(false);
LOGGER.warn("[{}] [failover-change] failover file deleted. dataId={}, group={}, tenant={}", agent.getName(),
dataId, group, tenant);
return;
}
// 有变更
if (cacheData.isUseLocalConfigInfo() && path.exists() && cacheData.getLocalConfigInfoVersion() != path
.lastModified()) {
String content = LocalConfigInfoProcessor.getFailover(agent.getName(), dataId, group, tenant);
final String md5 = MD5Utils.md5Hex(content, Constants.ENCODE);
cacheData.setUseLocalConfigInfo(true);
cacheData.setLocalConfigInfoVersion(path.lastModified());
cacheData.setContent(content);
String encryptedDataKey = LocalEncryptedDataKeyProcessor
.getEncryptDataKeyFailover(agent.getName(), dataId, group, tenant);
cacheData.setEncryptedDataKey(encryptedDataKey);
LOGGER.warn(
"[{}] [failover-change] failover file changed. dataId={}, group={}, tenant={}, md5={}, content={}",
agent.getName(), dataId, group, tenant, md5, ContentUtils.truncateContent(content));
}
}
// 遍历用户自己添加的监听器,如果发现数据的md5值不同,则发送通知
void checkListenerMd5() {
for (ManagerListenerWrap wrap : listeners) {
if (!md5.equals(wrap.lastCallMd5)) {
safeNotifyListener(dataId, group, content, md5, wrap);
}
}
}
1.3.2、check server config 检查服务端配置
checkUpdateDataIds
向服务器端发起检查请求,判断自己本地的配置和服务端的配置是否一致
/**
* 从Server获取值变化了的DataID列表。返回的对象里只有dataId和group是有效的。 保证不返回NULL。
*/
List<String> checkUpdateDataIds(List<CacheData> cacheDatas, List<String> inInitializingCacheList) throws IOException {
StringBuilder sb = new StringBuilder();
// 把需要检查的配置项,拼接成一个字符串
for (CacheData cacheData : cacheDatas) {
if (!cacheData.isUseLocalConfigInfo()) {
sb.append(cacheData.dataId).append(WORD_SEPARATOR);
sb.append(cacheData.group).append(WORD_SEPARATOR);
if (StringUtils.isBlank(cacheData.tenant)) {
sb.append(cacheData.getMd5()).append(LINE_SEPARATOR);
} else {
sb.append(cacheData.getMd5()).append(WORD_SEPARATOR);
sb.append(cacheData.getTenant()).append(LINE_SEPARATOR);
}
if (cacheData.isInitializing()) {
// cacheData 首次出现在cacheMap中&首次check更新
inInitializingCacheList
.add(GroupKey.getKeyTenant(cacheData.dataId, cacheData.group, cacheData.tenant));
}
}
}
boolean isInitializingCacheList = !inInitializingCacheList.isEmpty();
// 检查更新配置字符串
return checkUpdateConfigStr(sb.toString(), isInitializingCacheList);
}
checkUpdateConfigStr
从Server获取值变化了的DataID列表。返回的对象里只有dataId和group是有效的。 保证不返回NULL
/**
* 从Server获取值变化了的DataID列表。返回的对象里只有dataId和group是有效的。 保证不返回NULL。
*/
List<String> checkUpdateConfigStr(String probeUpdateString, boolean isInitializingCacheList) throws IOException {
List<String> params = Arrays.asList(Constants.PROBE_MODIFY_REQUEST, probeUpdateString);
List<String> headers = new ArrayList<String>(2);
headers.add("Long-Pulling-Timeout");
// 设置超时时间,默认30s
headers.add("" + timeout);
// told server do not hang me up if new initializing cacheData added in
// 是否初始化缓存列表
if (isInitializingCacheList) {
headers.add("Long-Pulling-Timeout-No-Hangup");
headers.add("true");
}
// 判断可能发生变更的字符串是否为空,如果是,则直接返回
if (StringUtils.isBlank(probeUpdateString)) {
return Collections.emptyList();
}
try {
// 发送带超时时间的Http请求,请求路径:/v1/cs/configs/listener
HttpResult result = agent.httpPost(Constants.CONFIG_CONTROLLER_PATH + "/listener", headers, params,
agent.getEncode(), timeout);
if (HttpURLConnection.HTTP_OK == result.code) {
setHealthServer(true);
// 解析更新数据 ID 响应
return parseUpdateDataIdResponse(result.content);
} else {
setHealthServer(false);
LOGGER.error("[{}] [check-update] get changed dataId error, code: {}", agent.getName(), result.code);
}
} catch (IOException e) {
setHealthServer(false);
LOGGER.error("[" + agent.getName() + "] [check-update] get changed dataId exception", e);
throw e;
}
// 超时返回 Collections.emptyList()
return Collections.emptyList();
}
2、服务端
2.1、处理客户端长轮询的请求
通过前面的分析可以得到,客户端会发送一个 /v1/cs/configs/listener 的请求
@Controller
// Constants.CONFIG_CONTROLLER_PATH = /v1/cs/configs
@RequestMapping(Constants.CONFIG_CONTROLLER_PATH)
public class ConfigController {
/**
* 比较MD5
*/
@RequestMapping(value = "/listener", method = RequestMethod.POST)
public void listener(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
request.setAttribute("org.apache.catalina.ASYNC_SUPPORTED", true);
// 获取需要比较的字符串
String probeModify = request.getParameter("Listening-Configs");
if (StringUtils.isBlank(probeModify)) {
throw new IllegalArgumentException("invalid probeModify");
}
// 解码
probeModify = URLDecoder.decode(probeModify, Constants.ENCODE);
// key -> groupKey value -> md5
Map<String, String> clientMd5Map;
try {
// 获取客户端传输过来的md5值
clientMd5Map = MD5Util.getClientMd5Map(probeModify);
} catch (Throwable e) {
throw new IllegalArgumentException("invalid probeModify");
}
// do long-polling
// 长轮询
inner.doPollingConfig(request, response, clientMd5Map, probeModify.length());
}
}
ConfigServletInner.doPollingConfig
这个方法主要是用来做长轮训和短轮询的判断
- 如果是长轮训,直接走addLongPollingClient方法
- 如果是短轮询,直接比较服务端的数据,如果存在md5不一致,直接把数据返回。
@Service
public class ConfigServletInner {
/**
* 轮询接口
*/
public String doPollingConfig(HttpServletRequest request, HttpServletResponse response,
Map<String, String> clientMd5Map, int probeRequestSize)
throws IOException, ServletException {
// 长轮询
if (LongPollingService.isSupportLongPolling(request)) {
// 添加长轮询客户端
longPollingService.addLongPollingClient(request, response, clientMd5Map, probeRequestSize);
return HttpServletResponse.SC_OK + "";
}
// else 兼容短轮询逻辑
List<String> changedGroups = MD5Util.compareMd5(request, response, clientMd5Map);
// 兼容短轮询result
String oldResult = MD5Util.compareMd5OldResult(changedGroups);
String newResult = MD5Util.compareMd5ResultString(changedGroups);
String version = request.getHeader(Constants.CLIENT_VERSION_HEADER);
if (version == null) {
version = "2.0.0";
}
int versionNum = Protocol.getVersionNumber(version);
/**
* 2.0.4版本以前, 返回值放入header中
*/
if (versionNum < START_LONGPOLLING_VERSION_NUM) {
response.addHeader(Constants.PROBE_MODIFY_RESPONSE, oldResult);
response.addHeader(Constants.PROBE_MODIFY_RESPONSE_NEW, newResult);
} else {
request.setAttribute("content", newResult);
}
// 禁用缓存
response.setHeader("Pragma", "no-cache");
response.setDateHeader("Expires", 0);
response.setHeader("Cache-Control", "no-cache,no-store");
response.setStatus(HttpServletResponse.SC_OK);
return HttpServletResponse.SC_OK + "";
}
}
LongPollingService.addLongPollingClient
- 获得客户端传递过来的超时时间,并且进行本地计算,提前500ms返回响应,这就能解释为什么客户端响应超时时间是29.5+了。如果 isFixedPolling=true ,不会提前返回响应
- md5比较,如果不一致,通过 generateResponse 将结果返回
- 如果配置文件没有发生变化,则通过 scheduler.execute 启动了一个定时任务,将客户端的长轮询请求封装成一个叫 ClientLongPolling 的任务,交给 scheduler 去执行
public void addLongPollingClient(HttpServletRequest req, HttpServletResponse rsp, Map<String, String> clientMd5Map,
int probeRequestSize) {
// 获取客户端长轮训的超时时间
String str = req.getHeader(LongPollingService.LONG_POLLING_HEADER);
// 不允许断开的标记
String noHangUpFlag = req.getHeader(LongPollingService.LONG_POLLING_NO_HANG_UP_HEADER);
// 应用名称
String appName = req.getHeader(RequestUtil.CLIENT_APPNAME_HEADER);
String tag = req.getHeader("Vipserver-Tag");
// 延期时间,默认为500ms
int delayTime = SwitchService.getSwitchInteger(SwitchService.FIXED_DELAY_TIME, 500);
// Add delay time for LoadBalance, and one response is returned 500 ms in advance to avoid client timeout.
// 提前500ms返回一个响应,避免客户端出现超时
long timeout = Math.max(10000, Long.parseLong(str) - delayTime);
if (isFixedPolling()) {
// Do nothing but set fix polling timeout.
timeout = Math.max(10000, getFixedPollingInterval());
} else {
long start = System.currentTimeMillis();
// 通过md5判断客户端请求过来的key是否有和服务器端有不一致的,如果有,则保存到changedGroups中。
List<String> changedGroups = MD5Util.compareMd5(req, rsp, clientMd5Map);
if (changedGroups.size() > 0) {
// 如果发现有变更,则直接把请求返回给客户端
generateResponse(req, rsp, changedGroups);
LogUtil.CLIENT_LOG.info("{}|{}|{}|{}|{}|{}|{}", System.currentTimeMillis() - start, "instant",
RequestUtil.getRemoteIp(req), "polling", clientMd5Map.size(), probeRequestSize,
changedGroups.size());
return;
} else if (noHangUpFlag != null && noHangUpFlag.equalsIgnoreCase(TRUE_STR)) {
// 如果noHangUpFlag为true,说明不需要挂起客户端,所以直接返回。
LogUtil.CLIENT_LOG.info("{}|{}|{}|{}|{}|{}|{}", System.currentTimeMillis() - start, "nohangup",
RequestUtil.getRemoteIp(req), "polling", clientMd5Map.size(), probeRequestSize,
changedGroups.size());
return;
}
}
// 获取请求端的ip
String ip = RequestUtil.getRemoteIp(req);
// Must be called by http thread, or send response.
// 把当前请求转化为一个异步请求(意味着此时tomcat线程被释放,也就是客户端的请求,需要通过asyncContext来手动触发返回,否则一直挂起)
// AsyncContext是Servlet3.0中提供的对象,调用startAsync获得AsyncContext对象之后,这个请求的响应会被延后,并释放容器分配的线程。
final AsyncContext asyncContext = req.startAsync();
// AsyncContext.setTimeout() is incorrect, Control by oneself
// 设置异步请求超时时间,0表示自己控制
asyncContext.setTimeout(0L);
// 执行长轮训请求
ConfigExecutor.executeLongPolling(new ClientLongPolling(asyncContext, clientMd5Map, ip, probeRequestSize, timeout, appName, tag));
}
ClientLongPolling
- LongPollingService 的内部类
- 通过scheduler.schedule实现了一个定时任务,它的delay时间正好是前面计算的29.5s
- 如果在29.5s之内,数据发生变化,需要提前通知。allSubs 和发布订阅有关系,订阅了数据变化的事件
class ClientLongPolling implements Runnable {
@Override
public void run() {
// 构建一个异步任务,延后29.5s执行
asyncTimeoutFuture = ConfigExecutor.scheduleLongPolling(new Runnable() {
@Override
public void run() {
try {
// 获取并设置客户端IP
getRetainIps().put(ClientLongPolling.this.ip, System.currentTimeMillis());
// 移除订阅关系
allSubs.remove(ClientLongPolling.this);
// 如果是固定间隔的长轮询
if (isFixedPolling()) {
LogUtil.CLIENT_LOG
.info("{}|{}|{}|{}|{}|{}", (System.currentTimeMillis() - createTime), "fix",
RequestUtil.getRemoteIp((HttpServletRequest) asyncContext.getRequest()),
"polling", clientMd5Map.size(), probeRequestSize);
// 通过md5值,获取当前所有变更的groups
List<String> changedGroups = MD5Util
.compareMd5((HttpServletRequest) asyncContext.getRequest(),
(HttpServletResponse) asyncContext.getResponse(), clientMd5Map);
if (changedGroups.size() > 0) {
// 如果大于0,表示有变更,直接响应
sendResponse(changedGroups);
} else {
// 否则返回null
sendResponse(null);
}
} else {
LogUtil.CLIENT_LOG
.info("{}|{}|{}|{}|{}|{}", (System.currentTimeMillis() - createTime), "timeout",
RequestUtil.getRemoteIp((HttpServletRequest) asyncContext.getRequest()),
"polling", clientMd5Map.size(), probeRequestSize);
sendResponse(null);
}
} catch (Throwable t) {
LogUtil.DEFAULT_LOG.error("long polling error:" + t.getMessage(), t.getCause());
}
}
}, timeoutTime, TimeUnit.MILLISECONDS);
// 把当前线程添加到订阅事件队列中
allSubs.add(this);
}
void sendResponse(List<String> changedGroups) {
// 取消超时任务
if (null != asyncTimeoutFuture) {
asyncTimeoutFuture.cancel(false);
}
generateResponse(changedGroups);
}
void generateResponse(List<String> changedGroups) {
if (null == changedGroups) {
asyncContext.complete();
return;
}
HttpServletResponse response = (HttpServletResponse)asyncContext.getResponse();
try {
// 获取resp
String respString = MD5Util.compareMd5ResultString(changedGroups);
// 禁用缓存
response.setHeader("Pragma", "no-cache");
response.setDateHeader("Expires", 0);
response.setHeader("Cache-Control", "no-cache,no-store");
response.setStatus(HttpServletResponse.SC_OK);
// 回写数据
response.getWriter().println(respString);
asyncContext.complete();
} catch (Exception se) {
pullLog.error(se.toString(), se);
asyncContext.complete();
}
}
}
2.2、控制台的变更与监听
当在控制台点击保存时,会调用nacos服务端的 /v1/cs/configs/ 请求,保存配置并且发送一个 LocalDataChangeEvent 事件,由LongPollingService 进行相应
@Service
public class LongPollingService extends AbstractEventListener {
/**
* 长轮询订阅关系
*/
final Queue<ClientLongPolling> allSubs;
@Override
public void onEvent(Event event) {
// 固定轮询时长的不处理
if (isFixedPolling()) {
// ignore
} else {
if (event instanceof LocalDataChangeEvent) {
// 接收 LocalDataChangeEvent
LocalDataChangeEvent evt = (LocalDataChangeEvent)event;
// 执行 DataChangeTask
scheduler.execute(new DataChangeTask(evt.groupKey, evt.isBeta, evt.betaIps));
}
}
}
class DataChangeTask implements Runnable {
@Override
public void run() {
try {
ConfigService.getContentBetaMd5(groupKey);
// 循环遍历 allSubs Queue<ClientLongPolling> allSubs;
for (Iterator<ClientLongPolling> iter = allSubs.iterator(); iter.hasNext(); ) {
ClientLongPolling clientSub = iter.next();
// 如果当前 ClientLongPolling 中的 clientMd5Map key中存在当前 groupKey,则进行通知
if (clientSub.clientMd5Map.containsKey(groupKey)) {
// 如果beta发布且不在beta列表直接跳过
if (isBeta && !betaIps.contains(clientSub.ip)) {
continue;
}
// 如果tag发布且不在tag列表直接跳过
if (StringUtils.isNotBlank(tag) && !tag.equals(clientSub.tag)) {
continue;
}
getRetainIps().put(clientSub.ip, System.currentTimeMillis());
// 删除订阅关系
iter.remove();
LogUtil.clientLog.info("{}|{}|{}|{}|{}|{}|{}",
(System.currentTimeMillis() - changeTime),
"in-advance",
RequestUtil.getRemoteIp((HttpServletRequest)clientSub.asyncContext.getRequest()),
"polling",
clientSub.clientMd5Map.size(), clientSub.probeRequestSize, groupKey);
// 发送服务配置变更groupKey,完成实时通知
clientSub.sendResponse(Arrays.asList(groupKey));
}
}
} catch (Throwable t) {
LogUtil.defaultLog.error("data change error:" + t.getMessage(), t.getCause());
}
}
void sendResponse(List<String> changedGroups) {
// 取消超时任务
if (null != asyncTimeoutFuture) {
asyncTimeoutFuture.cancel(false);
}
// 直接返回
generateResponse(changedGroups);
}
}
服务端一方面通过提前500MS返回当前的nacos对比结果,一方面通过监听实际的nacos变更时机来提前返回,实现的效果如消费队列线程在无消息的情况下阻塞挂起,然后监听队列是否有推送,如有就唤醒消费线程去继续消费队列。且通过异步机制,不消耗Tomcat的线程,是一种值得学习的技术方案。
二、nacos动态更新
1、基于springboot
在Springboot中,如果客户端监听到服务端的nacos的配置更新,则会发起NacosConfigReceivedEvent事件
- checkListenerMd5
// 对比MD5 检查配置是否发生变化
void checkListenerMd5() {
for (ManagerListenerWrap wrap : listeners) {
if (!md5.equals(wrap.lastCallMd5)) {
// 发生变化
safeNotifyListener(dataId, group, content, md5, wrap);
}
}
}
private void safeNotifyListener(final String dataId, final String group, final String content,
final String md5, final ManagerListenerWrap listenerWrap) {
final Listener listener = listenerWrap.listener;
Runnable job = new Runnable() {
@Override
public void run() {
ClassLoader myClassLoader = Thread.currentThread().getContextClassLoader();
ClassLoader appClassLoader = listener.getClass().getClassLoader();
try {
if (listener instanceof AbstractSharedListener) {
AbstractSharedListener adapter = (AbstractSharedListener) listener;
adapter.fillContext(dataId, group);
LOGGER.info("[{}] [notify-context] dataId={}, group={}, md5={}", name, dataId, group, md5);
}
// 执行回调之前先将线程classloader设置为具体webapp的classloader,以免回调方法中调用spi接口是出现异常或错用(多应用部署才会有该问题)。
Thread.currentThread().setContextClassLoader(appClassLoader);
ConfigResponse cr = new ConfigResponse();
cr.setDataId(dataId);
cr.setGroup(group);
cr.setContent(content);
configFilterChainManager.doFilter(null, cr);
String contentTmp = cr.getContent();
// 接收变化的配置信息
listener.receiveConfigInfo(contentTmp);
listenerWrap.lastCallMd5 = md5;
LOGGER.info("[{}] [notify-ok] dataId={}, group={}, md5={}, listener={} ", name, dataId, group, md5,
listener);
} catch (NacosException de) {
LOGGER.error("[{}] [notify-error] dataId={}, group={}, md5={}, listener={} errCode={} errMsg={}", name,
dataId, group, md5, listener, de.getErrCode(), de.getErrMsg());
} catch (Throwable t) {
LOGGER.error("[{}] [notify-error] dataId={}, group={}, md5={}, listener={} tx={}", name, dataId, group,
md5, listener, t.getCause());
} finally {
Thread.currentThread().setContextClassLoader(myClassLoader);
}
}
};
final long startNotify = System.currentTimeMillis();
try {
if (null != listener.getExecutor()) {
listener.getExecutor().execute(job);
} else {
job.run();
}
} catch (Throwable t) {
LOGGER.error("[{}] [notify-error] dataId={}, group={}, md5={}, listener={} throwable={}", name, dataId, group,
md5, listener, t.getCause());
}
final long finishNotify = System.currentTimeMillis();
LOGGER.info("[{}] [notify-listener] time cost={}ms in ClientWorker, dataId={}, group={}, md5={}, listener={} ",
name, (finishNotify - startNotify), dataId, group, md5, listener);
}
DelegatingEventPublishingListener
@Override
public void receiveConfigInfo(String content) {
onReceived(content);
publishEvent(content);
}
private void publishEvent(String content) {
// 构造NacosConfigReceivedEvent nacos配置更新事件并发送
NacosConfigReceivedEvent event = new NacosConfigReceivedEvent(configService,
dataId, groupId, content, configType);
applicationEventPublisher.publishEvent(event);
}
NacosValueAnnotationBeanPostProcessor
- 消费NacosConfigReceivedEvent事件
@Override
public void onApplicationEvent(NacosConfigReceivedEvent event) {
// In to this event receiver, the environment has been updated the
// latest configuration information, pull directly from the environment
// fix issue #142
for (Map.Entry<String, List<NacosValueTarget>> entry : placeholderNacosValueTargetMap
.entrySet()) {
String key = environment.resolvePlaceholders(entry.getKey());
String newValue = environment.getProperty(key);
if (newValue == null) {
continue;
}
List<NacosValueTarget> beanPropertyList = entry.getValue();
for (NacosValueTarget target : beanPropertyList) {
String md5String = MD5Utils.md5Hex(newValue, "UTF-8");
boolean isUpdate = !target.lastMD5.equals(md5String);
if (isUpdate) {
// 更新MD5
target.updateLastMD5(md5String);
// 处理EL表达式
Object evaluatedValue = resolveNotifyValue(target.nacosValueExpr, key, newValue);
// 更新对应的自动更新的值
if (target.method == null) {
setField(target, evaluatedValue);
}
else {
setMethod(target, evaluatedValue);
}
}
}
}
}
2、基于Springcloud
在Springcloud中,如果客户端监听到服务端的nacos的配置更新,则会发起NacosConfigReceivedEvent事件
checkListenerMd5中接收变化的配置信息listener.receiveConfigInfo(contentTmp),此时调用的是NacosContextRefresher
public void onApplicationEvent(ApplicationReadyEvent event) {
if (this.ready.compareAndSet(false, true)) {
this.registerNacosListenersForApplications();
}
}
private void registerNacosListenersForApplications() {
// 如果配置@RefreshScope
if (isRefreshEnabled()) {
for (NacosPropertySource propertySource : NacosPropertySourceRepository
.getAll()) {
if (!propertySource.isRefreshable()) {
continue;
}
String dataId = propertySource.getDataId();
registerNacosListener(propertySource.getGroup(), dataId);
}
}
}
private void registerNacosListener(final String groupKey, final String dataKey) {
String key = NacosPropertySourceRepository.getMapKey(dataKey, groupKey);
Listener listener = listenerMap.computeIfAbsent(key,
lst -> new AbstractSharedListener() {
@Override
public void innerReceive(String dataId, String group,
String configInfo) {
refreshCountIncrement();
nacosRefreshHistory.addRefreshRecord(dataId, group, configInfo);
// todo feature: support single refresh for listening
// 触发RefreshEvent事件,刷新nacos配置
applicationContext.publishEvent(
new RefreshEvent(this, null, "Refresh Nacos config"));
if (log.isDebugEnabled()) {
log.debug(String.format(
"Refresh Nacos config group=%s,dataId=%s,configInfo=%s",
group, dataId, configInfo));
}
}
});
try {
configService.addListener(dataKey, groupKey, listener);
}
catch (NacosException e) {
log.warn(String.format(
"register fail for nacos listener ,dataId=[%s],group=[%s]", dataKey,
groupKey), e);
}
}
RefreshEventListener
public class RefreshEventListener implements SmartApplicationListener {
private static Log log = LogFactory.getLog(RefreshEventListener.class);
private ContextRefresher refresh;
private AtomicBoolean ready = new AtomicBoolean(false);
public RefreshEventListener(ContextRefresher refresh) {
this.refresh = refresh;
}
@Override
public boolean supportsEventType(Class<? extends ApplicationEvent> eventType) {
return ApplicationReadyEvent.class.isAssignableFrom(eventType)
|| RefreshEvent.class.isAssignableFrom(eventType);
}
@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ApplicationReadyEvent) {
handle((ApplicationReadyEvent) event);
}
else if (event instanceof RefreshEvent) {
// 执行此handle
handle((RefreshEvent) event);
}
}
public void handle(ApplicationReadyEvent event) {
this.ready.compareAndSet(false, true);
}
public void handle(RefreshEvent event) {
if (this.ready.get()) { // don't handle events before app is ready
log.debug("Event received " + event.getEventDesc());
// 执行刷新
Set<String> keys = this.refresh.refresh();
log.info("Refresh keys changed: " + keys);
}
}
}
ContextRefresher
public synchronized Set<String> refresh() {
// 刷新环境变量
Set<String> keys = refreshEnvironment();
// 刷新配置
this.scope.refreshAll();
return keys;
}
// 动态刷新配置的事件发送方法
public synchronized Set<String> refreshEnvironment() {
Map<String, Object> before = extract(
this.context.getEnvironment().getPropertySources());
addConfigFilesToEnvironment();
Set<String> keys = changes(before,
extract(this.context.getEnvironment().getPropertySources())).keySet();
this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys));
return keys;
}
@ManagedOperation(description = "Dispose of the current instance of all beans "
+ "in this scope and force a refresh on next method execution.")
public void refreshAll() {
super.destroy();
// RefreshScopeRefreshedEvent刷新事件
this.context.publishEvent(new RefreshScopeRefreshedEvent());
}
EnvironmentChangeEvent事件监听器ConfigurationPropertiesRebinder
@Component
@ManagedResource
public class ConfigurationPropertiesRebinder
implements ApplicationContextAware, ApplicationListener<EnvironmentChangeEvent> {
private ConfigurationPropertiesBeans beans;
private ApplicationContext applicationContext;
private Map<String, Exception> errors = new ConcurrentHashMap<>();
public ConfigurationPropertiesRebinder(ConfigurationPropertiesBeans beans) {
this.beans = beans;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
this.applicationContext = applicationContext;
}
@Override
public void onApplicationEvent(EnvironmentChangeEvent event) {
if (this.applicationContext.equals(event.getSource())
// Backwards compatible
|| event.getKeys().equals(event.getSource())) {
rebind();
}
}
/**
* A map of bean name to errors when instantiating the bean.
* @return The errors accumulated since the latest destroy.
*/
public Map<String, Exception> getErrors() {
return this.errors;
}
@ManagedOperation
public void rebind() {
this.errors.clear();
for (String name : this.beans.getBeanNames()) {
rebind(name);
}
}
@ManagedOperation
public boolean rebind(String name) {
if (!this.beans.getBeanNames().contains(name)) {
return false;
}
if (this.applicationContext != null) {
try {
Object bean = this.applicationContext.getBean(name);
if (AopUtils.isAopProxy(bean)) {
bean = ProxyUtils.getTargetObject(bean);
}
if (bean != null) {
// TODO: determine a more general approach to fix this.
// see https://github.com/spring-cloud/spring-cloud-commons/issues/571
if (getNeverRefreshable().contains(bean.getClass().getName())) {
return false; // ignore
}
// 销毁bean后重新初始化,因为环境变量已经更新成最新的nacos,所以初始化后配置也会跟着更新
this.applicationContext.getAutowireCapableBeanFactory()
.destroyBean(bean);
this.applicationContext.getAutowireCapableBeanFactory()
.initializeBean(bean, name);
return true;
}
}
catch (RuntimeException e) {
this.errors.put(name, e);
throw e;
}
catch (Exception e) {
this.errors.put(name, e);
throw new IllegalStateException("Cannot rebind to " + name, e);
}
}
return false;
}
@ManagedAttribute
public Set<String> getNeverRefreshable() {
String neverRefresh = this.applicationContext.getEnvironment().getProperty(
"spring.cloud.refresh.never-refreshable",
"com.zaxxer.hikari.HikariDataSource");
return StringUtils.commaDelimitedListToSet(neverRefresh);
}
@ManagedAttribute
public Set<String> getBeanNames() {
return new HashSet<>(this.beans.getBeanNames());
}
}
总结
自此分析完nacos配置中心的变更监听和动态更新,有很多设计思维值得学习,长轮询、异步线程挂起、动态刷新、MD5比较器。这些都是可以带到工作中,让自己的代码变得更加优雅。
备注:后续附上执行流程图