NacosConfigService
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);
this.agent = new MetricsHttpAgent(new ServerHttpAgent(properties));
this.agent.start();
this.worker = new ClientWorker(this.agent, this.configFilterChainManager, properties);
}
-
初始化了一个
HttpAgent,内部封装了一些操作HTTP请求的方法,实际工作的类是ServerHttpAgent,调用start()方法最终会执行ServerListManager.start():会注册一个 getServersTask 任务,每隔 30s 执行一次来刷新 server list。 -
ClientWorker客户端工作类。
ClientWorker
进入 ClientWorker 的构造方法:
public ClientWorker(final HttpAgent agent, final ConfigFilterChainManager configFilterChainManager,
final Properties properties) {
// 维护 HttpAgent
this.agent = agent;
// 初始化配置过滤管理器
this.configFilterChainManager = configFilterChainManager;
// 初始化配置: 长轮询超时时间 30s
init(properties);
// 初始化一个定时调度的线程池, 跟 Worker 有关
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;
}
});
// 定时调用 checkConfigInfo 这个方法, 首次执行延迟时间为1毫秒, 周期为10毫秒
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);
}
在 ClientWorker 里,创建了两个线程池:
executor线程池只拥有一个线程,每隔 10ms 会执行一次checkConfigInfo()方法。executorService线程池,从名称看应该是做长轮询的。
checkConfigInfo()
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++) {
// 在 executorService 线程池中启动一个 LongPoollingRunnable 任务
executorService.execute(new LongPollingRunnable(i));
}
currentLongingTaskCount = longingTaskCount;
}
}
-
cacheMap:用来存储监听变更的缓存集合,结构如下:/** * groupKey -> cacheData. */ private final AtomicReference<Map<String, CacheData>> cacheMap = new AtomicReference<Map<String, CacheData>>( new HashMap<String, CacheData>());为保证多线程场景数据一致性,
cacheMap采用了AtomicReference原子变量实现。cacheMap是个Map结构,key为groupKey,是由 dataId,group,tenant(租户)拼接的字符串;value 为CacheData对象,每个 dataId 都会持有一个CacheData 对象。 -
使用
ClientWorker构造方法中创建的executorService线程池执行LongPollingRunnable任务,默认情况下,每个LongPollingRunnable任务执行 3000 个监听配置集,超过则启动多个LongPollingRunnable任务执行。 -
currentLongingTaskCount用于保存当前已启动的LongPollingRunnable任务数。
LongPollingRunnable
这是一个线程任务,直接进入 run() 方法查看:
@Override
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) {
// 对任务按批次分类, 把任务 id 相同的数据保存到 cacheDatas
cacheDatas.add(cacheData);
try {
// 检查本地配置文件
checkLocalConfig(cacheData);
// 如果 isUseLocalConfigInfo 为 true, 表示缓存和本地配置不一致
if (cacheData.isUseLocalConfigInfo()) {
// 检查 MD5,通知客户端监听
cacheData.checkListenerMd5();
}
} catch (Exception e) {
LOGGER.error("get local config info error", e);
}
}
}
// check server config
List<String> changedGroupKeys = checkUpdateDataIds(cacheDatas, inInitializingCacheList);
if (!CollectionUtils.isEmpty(changedGroupKeys)) {
LOGGER.info("get changedGroupKeys:" + changedGroupKeys);
}
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);
}
}
check failover config
先查看第一部分 check failover config,检查本地配置,主要是 checkLocalConfig() 和 checkListenerMd5() 两个方法。
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);
// 1. 如果 isUseLocalConfigInfo 为 false,表示不使用本地配置,并且本地缓存文件路径存在
if (!cacheData.isUseLocalConfigInfo() && path.exists()) {
// 从本地缓存文件获取配置
String content = LocalConfigInfoProcessor.getFailover(agent.getName(), dataId, group, tenant);
final String md5 = MD5Utils.md5Hex(content, Constants.ENCODE);
// 设置 isUseLocalConfigInfo 为 true
cacheData.setUseLocalConfigInfo(true);
// 更新 cacheData 文件更新时间和内容
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;
}
// 2. 如果 isUseLocalConfigInfo 为 true,使用本地配置,并且本地缓存文件路径不存在
if (cacheData.isUseLocalConfigInfo() && !path.exists()) {
// 设置 isUseLocalConfigInfo 为 false
cacheData.setUseLocalConfigInfo(false);
LOGGER.warn("[{}] [failover-change] failover file deleted. dataId={}, group={}, tenant={}", agent.getName(),
dataId, group, tenant);
return;
}
// 3. 如果 isUseLocalConfigInfo 为 true,使用本地配置,并且本地缓存文件路径存在, 且缓存的时间跟文件的更新时间不一致
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);
// 设置 isUseLocalConfigInfo 为 true
cacheData.setUseLocalConfigInfo(true);
// 更新 cacheData 文件更新时间和内容
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));
}
}
检查本地配置,分三种情况:
- 如果
isUseLocalConfigInfo为 false,表示不使用本地配置,并且本地缓存文件路径存在,则从本地缓存文件获取配置, 设置isUseLocalConfigInfo为 true,并更新cacheData文件更新时间和内容。 - 如果
isUseLocalConfigInfo为 true,使用本地配置,并且本地缓存文件路径不存在,则设置isUseLocalConfigInfo为 false。 - 如果
isUseLocalConfigInfo为 true,使用本地配置,并且本地缓存文件路径存在,且缓存的时间跟文件的更新时间不一致,则从本地缓存文件获取配置, 设置isUseLocalConfigInfo为 true,并更新cacheData文件更新时间和内容。
checkListenerMd5()
void checkListenerMd5() {
for (ManagerListenerWrap wrap : listeners) {
if (!md5.equals(wrap.lastCallMd5)) {
safeNotifyListener(dataId, group, content, type, md5, wrap);
}
}
}
遍历添加的监听器,如果发现数据的 md5 值不一致,则发送数据变更通知。
safeNotifyListener()
private void safeNotifyListener(final String dataId, final String group, final String content, final String type,
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() 方法
listener.receiveConfigInfo(contentTmp);
// compare lastContent and content
if (listener instanceof AbstractConfigChangeListener) {
Map data = ConfigChangeHandler.getInstance()
.parseChangeData(listenerWrap.lastContent, content, type);
ConfigChangeEvent event = new ConfigChangeEvent(data);
((AbstractConfigChangeListener) listener).receiveConfigChange(event);
listenerWrap.lastContent = content;
}
listenerWrap.lastCallMd5 = md5;
LOGGER.info("[{}] [notify-ok] dataId={}, group={}, md5={}, listener={} ", name, dataId, group, md5,
listener);
}
// 省略部分...
}
};
final long startNotify = System.currentTimeMillis();
try {
if (null != listener.getExecutor()) {
listener.getExecutor().execute(job);
} else {
job.run();
}
}
// 省略部分...
}
safeNotifyListener() 方法单独起线程,向所有对 dataId 注册过监听的客户端推送变更后的数据内容。客户端接收通知,直接实现 receiveConfigInfo() 方法接收回调数据,处理自身业务就可以了。
check server config
再看第二部分 check server config ,检查服务配置。
checkUpdateDataIds()
从服务端获取配置变化了的列表,返回:
dataId、group、tenant
List<String> checkUpdateDataIds(List<CacheData> cacheDatas, List<String> inInitializingCacheList) throws Exception {
StringBuilder sb = new StringBuilder();
for (CacheData cacheData : cacheDatas) {
// 找到 isUseLocalConfigInfo 为 false 的缓存数据
if (!cacheData.isUseLocalConfigInfo()) {
// 把需要检查的配置项,拼接成一个字符串: dataId、group、MD5
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();
return checkUpdateConfigStr(sb.toString(), isInitializingCacheList);
}
checkUpdateConfigStr
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.
long readTimeoutMs = timeout + (long) Math.round(timeout >> 1);
// 向服务端发送 http 请求: /v1/cs/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();
}
getServerConfig()
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;
}
HttpRestResult<String> result = null;
try {
Map<String, String> params = new HashMap<String, String>(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 请求:/v1/cs/configs
result = agent.httpGet(Constants.CONFIG_CONTROLLER_PATH, null, params, agent.getEncode(), readTimeout);
} catch (Exception ex) {
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, ex);
throw new NacosException(NacosException.SERVER_ERROR, ex);
}
switch (result.getCode()) {
case HttpURLConnection.HTTP_OK:
// 保存本地配置
LocalConfigInfoProcessor.saveSnapshot(agent.getName(), dataId, group, tenant, result.getData());
ct[0] = result.getData();
if (result.getHeader().getValue(CONFIG_TYPE) != null) {
ct[1] = result.getHeader().getValue(CONFIG_TYPE);
} else {
ct[1] = ConfigType.TEXT.getType();
}
return ct;
// 省略部分...
}
}
- 根据配置变化了的列表从服务端获取配置,发送
HTTP请求:/v1/cs/configs - 默认超时时间 3s
- 保存本地配置快照
总结
Nacos 客户端缓存配置动态更新主要流程:
- 在
ClientWorker种会创建两个线程池,executor线程池只有一个线程,会定时 10ms 执行一次checkConfigInfo(),executorService执行长轮询任务。 - 对本地缓存配置分批次拆分,每个批次处理 3000 个配置集,超过则启动多个
LongPollingRunnable任务执行,由executorService线程池来执行LongPollingRunnable任务。 - 先把每一个批次的缓存配置和本地文件配置对比:
- 如果不一致,则更新
isUseLocalConfigInfo为 true,比较MD5值不一致则向客户端发送数据变更通知(Listener)。 - 如果一致,则远程请求服务端检查变化了的配置项。返回
dataId、group、tenant。
- 如果不一致,则更新
- 客户端收到变更的配置列表,再从服务端获取最新配置。
- 将最新配置保存到本地配置快照。