1.开门见山
nacos使用引用的相关的jar包
<dependency>
<groupId>com.alibaba.boot</groupId>
<artifactId>nacos-config-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-spring-context</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-api</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-client</artifactId>
</dependency>
nacos配置中心中有个主要的分布式配置service接口就是ConfigService,对应的实现就是NacosConfigService,在真实的应用中,初始化的时候就需要实例化这个对象,然后加载到容器中.
初始化的地方在nacos-config-spring-autoconfigure的jar包里面,对应的入口是NacosConfigApplicationContextInitializer,早些版本的时候这个类是放在spring.factories中,后来给注释掉了.
现在这个Initializer是放置在NacosConfigEnvironmentProcessor里面进行调用的.
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
application.addInitializers(new ApplicationContextInitializer[]{new NacosConfigApplicationContextInitializer(this)});
if (this.enable(environment)) {
System.out.println("[Nacos Config Boot] : The preload log configuration is enabled");
this.initLogConfig(environment);
}
}
在调用initializer的时候,会进行NacosConfigService的初始化工作.
public ConfigService createConfigService(Properties properties) throws NacosException {
Properties copy = new Properties();
copy.putAll(properties);
String cacheKey = NacosUtils.identify(copy);
ConfigService configService = (ConfigService)this.configServicesCache.get(cacheKey);
if (configService == null) {
configService = this.doCreateConfigService(copy);
this.configServicesCache.put(cacheKey, configService);
}
return configService;
}
最终会调用NacosFactory.createConfigService(Properties properties),通过NacosFactory进行实话service对象.
下面这个就是具体的实例化的代码
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 var4) {
throw new NacosException(-400, var4);
}
}
NacosConfigService初始化
下面在来看看它的初始化的具体内容
public NacosConfigService(Properties properties) throws NacosException {
String encodeTmp = properties.getProperty("encode");
if (StringUtils.isBlank(encodeTmp)) {
this.encode = "UTF-8";
} else {
this.encode = encodeTmp.trim();
}
this.initNamespace(properties);
this.agent = new MetricsHttpAgent(new ServerHttpAgent(properties));
this.agent.start();
this.worker = new ClientWorker(this.agent, this.configFilterChainManager, properties);
}
在初始化的过程中,会把nacos的配置数据赋值到ServerListManager这个类当中,下面的跟server的交互的url前缀什么的都是通过这个类来获取到的.
1.初始化命名空间
2.把ServerHttpAgent包装成MetricsHttpAgent,两者都实现了HttpAgent.MetricsHttpAgent中添加了一些http请求的耗时记录.
3.创建ClientWorker
HttpAgent的具体方法:
public interface HttpAgent {
void start() throws NacosException;
HttpResult httpGet(String var1, List<String> var2, List<String> var3, String var4, long var5) throws IOException;
HttpResult httpPost(String var1, List<String> var2, List<String> var3, String var4, long var5) throws IOException;
HttpResult httpDelete(String var1, List<String> var2, List<String> var3, String var4, long var5) throws IOException;
String getName();
String getNamespace();
String getTenant();
String getEncode();
}
ClientWorker客户端的实例化
下面我们再看下ClientWorker的实例化过程
public void checkConfigInfo() {
int listenerSize = ((Map)this.cacheMap.get()).size();
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;
}
}
在轮询线程里面,会调用checkConfigInfo()方法,会拿取一部分任务执行ClientWorker.LongPollingRunnable(i).
接下来在看下ClientWorker中的内部类LongPollingRunnable,上面的任务执行
就会调用对应的run方法,
CacheData cacheData = (CacheData)var3.next();
if (cacheData.getTaskId() == this.taskId) {
cacheDatas.add(cacheData);
try {
ClientWorker.this.checkLocalConfig(cacheData);
if (cacheData.isUseLocalConfigInfo()) {
cacheData.checkListenerMd5();
}
} catch (Exception var13) {
ClientWorker.LOGGER.error("get local config info error", var13);
}
}
根据入参taskId获取对应的CacheData,然后校验本地配置,从代码上看,是根据命名空间,dataId,group,tenant组成的路径下,查看是否有对应的失败文件信息.
如果使用本地配置的话,然后校验MD5.
接着调用checkUpdateDataIds()检查变化的dataId列表,聚合不使用本地配置信息的dataId,然后调用checkUpdateConfigStr方法
nacos服务端listener的api
从上面的代码我们可以看到,是通过调用/v1/cs/configs/listener接口,来获取到服务端变化的dataId列表.
我们从github上面down下来nacos的源码,跟踪一下这个接口的具体流程.
最终会调用ConfigServletInner.doPollingConfig()轮询方法,判断的条件就是http的header请求参数里面是否会带有Long-Pulling-Timeout的参数,如果有就进入长轮询的流程里面,否则就只进行md5的校验,然后立即返回给客户端.
接下来我们看下长轮询的方法内容,这个是在LongPollingService类中,调用addLongPollingClient方法,进行长轮询的操作流程.
有一个isFixedPolling()的判断,这个底层数据是属于nacos元数据配置项里面的,要不是从config_info库里面加载,要不是从磁盘文件进行加载.
如果是true的话,只是计算timeout时间,主要是为了解决轮询超时问题,默认是10s.
如果是false,只校验配置文件的md5,然后返回给客户端.
接下来就把new的ClientLongPolling对象扔给ScheduledExecutorService任务线程里面去执行,最终执行LongPollingService的内部类ClientLongPolling的run方法.
public void run() {
asyncTimeoutFuture = ConfigExecutor.scheduleLongPolling(new Runnable() {
@Override
public void run() {
try {
getRetainIps().put(ClientLongPolling.this.ip, System.currentTimeMillis());
// Delete subsciber's relations.
allSubs.remove(ClientLongPolling.this);
if (isFixedPolling()) {
LogUtil.CLIENT_LOG
.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.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);
}
在开启线程前,会调用allSubs.add(this);把上面创建的实例放到队列里面Queue allSubs.
在线程里面会重新把队列里面的给移除掉
// Delete subsciber's relations.
allSubs.remove(ClientLongPolling.this);
在轮询的情况下,会获取已经变化的dataId组合,然后把变化列表响应给客户端.
public static List<String> compareMd5(HttpServletRequest request, HttpServletResponse response,
Map<String, String> clientMd5Map) {
List<String> changedGroupKeys = new ArrayList<String>();
String tag = request.getHeader("Vipserver-Tag");
for (Map.Entry<String, String> entry : clientMd5Map.entrySet()) {
String groupKey = entry.getKey();
String clientMd5 = entry.getValue();
String ip = RequestUtil.getRemoteIp(request);
boolean isUptodate = ConfigCacheService.isUptodate(groupKey, clientMd5, ip, tag);
if (!isUptodate) {
changedGroupKeys.add(groupKey);
}
}
return changedGroupKeys;
}
客户端调用/listener接口获取更新列表后的操作
List<String> changedGroupKeys = ClientWorker.this.checkUpdateDataIds(cacheDatas, inInitializingCacheList);
获取变化列表之后,循环调用server的具体配置
try {
String content = ClientWorker.this.getServerConfig(dataId, group, tenant, 3000L);
CacheData cache = (CacheData)((Map)ClientWorker.this.cacheMap.get()).get(GroupKey.getKeyTenant(dataId, group, tenant));
cache.setContent(content);
ClientWorker.LOGGER.info("[{}] [data-received] dataId={}, group={}, tenant={}, md5={}, content={}", new Object[]{ClientWorker.this.agent.getName(), dataId, group, tenant, cache.getMd5(), ContentUtils.truncateContent(content)});
} 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);
}
接下来我们看下ClientWorker.this.getServerConfig()这个方法,在方法里面,又调用了server端的另一个api接口,就是/v1/cs/configs,根据dataId group tenant获取到对应的配置内容,然后放置到CacheData中.
nacos后台变更数据
这是nacos后台数据变更后的请求数据,调用的是/nacos/v1/cs/configs,对照ConfigController的方法其实是publishConfig方法.
首先把变更后的数据同步到nacos的库里面.
然后发布了一个ConfigDataChangeEvent事件.
ConfigChangePublisher
.notifyConfigChange(new ConfigDataChangeEvent(false, dataId, group, tenant, time.getTime()));
nacos服务端相关配置变更是通过发布订阅的方式来进行的.
event接口
所有发布的事件都要继承这个抽象类.Subscriber是一个订阅的抽象类.
下面的这些都是实现了Subscriber的对象
所有的事件发布都是通过NotifyCenter类来进行的,包含注册订阅者都是通过这个类进行.
public static boolean publishEvent(final Event event) {
try {
return publishEvent(event.getClass(), event);
} catch (Throwable ex) {
LOGGER.error("There was an exception to the message publishing : {}", ex);
return false;
}
}
这是发布事件的地方.
nacos后台发布ConfigDataChangeEvent事件之后会跳转到AsyncNotifyService的onEvent方法
public void onEvent(Event event) {
// Generate ConfigDataChangeEvent concurrently
if (event instanceof ConfigDataChangeEvent) {
ConfigDataChangeEvent evt = (ConfigDataChangeEvent) event;
long dumpTs = evt.lastModifiedTs;
String dataId = evt.dataId;
String group = evt.group;
String tenant = evt.tenant;
String tag = evt.tag;
Collection<Member> ipList = memberManager.allMembers();
// In fact, any type of queue here can be
Queue<NotifySingleTask> queue = new LinkedList<NotifySingleTask>();
for (Member member : ipList) {
queue.add(new NotifySingleTask(dataId, group, tenant, tag, dumpTs, member.getAddress(),
evt.isBeta));
}
ConfigExecutor.executeAsyncNotify(new AsyncTask(nacosAsyncRestTemplate, queue));
}
}
遍历所有member,获取到对应addresss,这里我们可以看到new了一个NotifySingleTask
public NotifySingleTask(String dataId, String group, String tenant, String tag, long lastModified,
String target, boolean isBeta) {
super(dataId, group, tenant, lastModified);
this.target = target;
this.isBeta = isBeta;
try {
dataId = URLEncoder.encode(dataId, Constants.ENCODE);
group = URLEncoder.encode(group, Constants.ENCODE);
} catch (UnsupportedEncodingException e) {
LOGGER.error("URLEncoder encode error", e);
}
if (StringUtils.isBlank(tenant)) {
this.url = MessageFormat.format(URL_PATTERN, target, EnvUtil.getContextPath(), dataId, group);
} else {
this.url = MessageFormat
.format(URL_PATTERN_TENANT, target, EnvUtil.getContextPath(), dataId, group, tenant);
}
if (StringUtils.isNotEmpty(tag)) {
url = url + "&tag=" + tag;
}
failCount = 0;
// this.executor = executor;
}
在new的时候构造的url
private static final String URL_PATTERN =
"http://{0}{1}" + Constants.COMMUNICATION_CONTROLLER_PATH + "/dataChange" + "?dataId={2}&group={3}";
private static final String URL_PATTERN_TENANT =
"http://{0}{1}" + Constants.COMMUNICATION_CONTROLLER_PATH + "/dataChange"
+ "?dataId={2}&group={3}&tenant={4}";
会调用…/dataChange的api接口,这个接口是在CommunicationController类里面
@GetMapping("/dataChange")
public Boolean notifyConfigInfo(HttpServletRequest request, @RequestParam("dataId") String dataId,
@RequestParam("group") String group,
@RequestParam(value = "tenant", required = false, defaultValue = StringUtils.EMPTY) String tenant,
@RequestParam(value = "tag", required = false) String tag) {
dataId = dataId.trim();
group = group.trim();
String lastModified = request.getHeader(NotifyService.NOTIFY_HEADER_LAST_MODIFIED);
long lastModifiedTs = StringUtils.isEmpty(lastModified) ? -1 : Long.parseLong(lastModified);
String handleIp = request.getHeader(NotifyService.NOTIFY_HEADER_OP_HANDLE_IP);
String isBetaStr = request.getHeader("isBeta");
if (StringUtils.isNotBlank(isBetaStr) && trueStr.equals(isBetaStr)) {
dumpService.dump(dataId, group, tenant, lastModifiedTs, handleIp, true);
} else {
dumpService.dump(dataId, group, tenant, tag, lastModifiedTs, handleIp);
}
return true;
}
nacos后台数据变更的时候,会调用上面的api接口,数据变更后会dump到磁盘上去,包含getConfig的时候也是从磁盘上获取的.
dump数据的时候会发布LocalDataChangeEvent事件,而这个时间的监听者就是LongPollingService
NotifyCenter.registerSubscriber(new Subscriber() {
@Override
public void onEvent(Event event) {
if (isFixedPolling()) {
// Ignore.
} else {
if (event instanceof LocalDataChangeEvent) {
LocalDataChangeEvent evt = (LocalDataChangeEvent) event;
ConfigExecutor.executeLongPolling(new DataChangeTask(evt.groupKey, evt.isBeta, evt.betaIps));
}
}
}
@Override
public Class<? extends Event> subscribeType() {
return LocalDataChangeEvent.class;
}
});
监听到事件以后会把DataChangeTask放到异步线程里面去执行.
从代码中我们可以看到,是遍历所有的订阅者,然后调用
clientSub.sendResponse(Arrays.asList(groupKey));
响应数据给客户端.
而这个队列中的所有订阅者就是客户端发起轮询的时候,保存进来的.
首先客户端会定时轮询查看是否有数据的变更,如果有,那么就更新本地配置文件.
当后台数据变更的时候,通过事件的发布订阅也会通过http的方式通知给客户端.
配置更新既有客户端的主动拉取,也有服务端的主动推送,两者兼有.