配置中心nacos的实现原理
在了解nacos配置中心的实现原理之前,可以先思考一个问题,如果我们自己实现一个配置中心,需要考虑到哪些问题呢?
- 服务器配置持久化存储
- 客户端远程访问服务端的数据
- 客户端本地缓存配置信息
- 客户端与服务器端进行数据交互
有一个问题需要弄明白,Nacos 客户端是怎么实时获取到 Nacos 服务端的最新数据的
其实客户端和服务端之间的数据交互,无外乎两种情况:
- 服务端推数据给客户端
- 客户端从服务端拉数据
Nacos 的设计方式客户端主动去服务端拉取数据。这主要是为了避免,服务端为了维持心跳而耗费资源,而采用拉的方式,客户端只需要通过一个无状态的 http 请求即可获取到服务端的数据
基于猜想我们分析一下Nacos的源码
nacos是通过NacosFactory创建的ConfigService 来接收数据的,NacosFactory的createConfigService方法如下所示:
public static ConfigService createConfigService(Properties properties) throws NacosException {
return ConfigFactory.createConfigService(properties);
}
public static ConfigService createConfigService(Properties properties) throws NacosException {
try {
Class<?> driverImplClass = Class.forName("com.alibaba.nacos.client.config.NacosConfigService");
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);
}
}
本质上是通过反射实例化了一个NacosConfigService,需要注意的是,这里的ConfigService并不是单例模式
我们进入NacosConfigService的构造器方法:
public NacosConfigService(Properties properties) throws NacosException {
...
agent = new MetricsHttpAgent(new ServerHttpAgent(properties));
agent.start();
worker = new ClientWorker(agent, configFilterChainManager, properties);
}
在这里NacosConfigService主要是初始化了两个对象,他们分别是:
- HttpAgent
- ClientWorker
HttpAgent作为参数传入到ClientWorker 中的,可以猜测到里面会用到agent做一些远程通信相关的事情。接下来我们看一下ClientWorker的构造器方法:
public ClientWorker(final HttpAgent agent, final ConfigFilterChainManager configFilterChainManager, final Properties properties) {
this.agent = agent;
this.configFilterChainManager = configFilterChainManager;
// Initialize the timeout parameter
init(properties);
//初始化一个定时调度的线程池,重写了threadfactory方法
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;
}
});
//初始化一个定时调度的线程池,从里面的name名字来看,似乎和长轮训有关系。而这个长轮训应该是和nacos服务端的长轮训
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毫秒
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() 方法(检查配置信息)
-
第二个线程池似乎和nacos服务端之间的长轮询有关
我们顺着初始化的代码,继续往下看
checkConfigInfo()方法:用来检查服务端的配置信息是否发生了变化。如果发生了变化,则触发listener通知,源码如下:
public void checkConfigInfo() {
// 分任务
int listenerSize = cacheMap.get().size();
// 向上取整为批数,监听的配置数量除以3000,得到一个整数,代表长轮训任务的数量
int longingTaskCount = (int) Math.ceil(listenerSize / ParamUtil.getPerTaskConfigSize());
//currentLongingTaskCount表示当前的长轮训任务数量,如果小于计算的结果,则可以继续创建
if (longingTaskCount > currentLongingTaskCount) {
for (int i = (int) currentLongingTaskCount; i < longingTaskCount; i++) {
// 要判断任务是否在执行 任务列表现在是无序的。变化过程可能有问题
executorService.execute(new LongPollingRunnable(i));
}
currentLongingTaskCount = longingTaskCount; //更新当前长轮训任务数量
}
}
上面的代码可以看出长轮询的代码定义在LongPollingRunnable中,接下来我们去看看LongPollingRunnable的run方法
这个方法传递了一个taskid, tasked用来区分cacheMap中的任务批次, 保存到cacheDatas这个集合中,源码如下:
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) { //对cacheMap中的数据进行分批
cacheDatas.add(cacheData);
try {
checkLocalConfig(cacheData); //通过本地文件中缓存的数据和cacheData集合中的数据进行比对,判断是否出现数据变化
if (cacheData.isUseLocalConfigInfo()) {//如果有数据有变化,需要通知监听器
cacheData.checkListenerMd5();
}
} catch (Exception e) {
LOGGER.error("get local config info error", e);
}
}
}
...
}
checkLocalConfig方法主要是通过本地文件中缓存的数据和cacheData集合中的数据进行比对,判断是否出现数据变化,如果数据发生变化则拉取服务器数据
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);
//本地缓存文件存在,并且isUseLocalConfigInfo为false
if (!cacheData.isUseLocalConfigInfo() && path.exists()) {
//更新cacheData中的值
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;
}
// 有 -> 没有。不通知业务监听器,从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);
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));
}
}
当判断数据发生了变化之后,下一步就需要去服务器上获取最新的数据,检查哪些数据发生了变化,步骤如下:
//1.通过checkUpdateDataIds 从服务端获取发生变化的数据的DataID列表,保存在List<String>集合中
List<String> changedGroupKeys = checkUpdateDataIds(cacheDatas, inInitializingCacheList);
//2.遍历这些集合,调用getServerConfig()方法从远程服务器获得对应的内容,根据dataId、group、tenant等信息,使用http请求从远程服务器上获得配置信息,读取到数据之后缓存到本地文件中
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);//2
CacheData cache = cacheMap.get().get(GroupKey.getKeyTenant(dataId, group, tenant));
//3.更新本地的cache,设置为服务器端返回的内容
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);
}
}
//4.最后遍历cacheDatas,找到变化的数据进行通知
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);
上面源码中的步骤一通过checkUpdateDataIds()方法从服务端获取发生变化的数据,具体过程是什么样的呢?
List<String> checkUpdateDataIds(List<CacheData> cacheDatas, List<String> inInitializingCacheList) throws IOException {
StringBuilder sb = new StringBuilder();
for (CacheData cacheData : cacheDatas) {
//1.首先从cacheDatas集合中找到isUseLocalConfigInfo为false的缓存
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();
//2.调用checkUpdateConfigStr()方法,通过长轮训的方式,从远程服务器获得变化的数据进行返回
return checkUpdateConfigStr(sb.toString(), isInitializingCacheList);
}
checkUpdateConfigStr()方法,通过长轮训的方式,从远程服务器获得变化的数据进行返回,源码如下:
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");
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 {
HttpResult result = agent.httpPost(Constants.CONFIG_CONTROLLER_PATH + "/listener", headers, params,
agent.getEncode(), timeout);
if (HttpURLConnection.HTTP_OK == result.code) {
setHealthServer(true);
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();
}
至此nacos客户端获得配置中心数据的关键流程就全部结束了,最后我们用一个流程图总结: