源码阅读前的建议:
1.对配置中心client端源码要有了解。
文章承接上文,下面对上文中的几个请求到server端的接口进行详解。
注意:server端源码版本是 nacos-1.4.1,直接去git下载即可。
server 端启动类是:com.alibaba.nacos.Nacos
单机启动的话,需要加上jvm启动参数 -Dnacos.standalone=true进行启动。
我们直接看第一个请求,就是client向server获取配置的请求:
注意是GET请求的
com.alibaba.nacos.config.server.controller.ConfigController#getConfig
public void getConfig(HttpServletRequest request, HttpServletResponse response,
@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) {
...
tenant = NamespaceUtil.processNamespaceParameter(tenant);
final String clientIp = RequestUtil.getRemoteIp(request);
// 拉取配置
inner.doGetConfig(request, response, dataId, group, tenant, tag, clientIp);
}
// 比较直接的逻辑,直接就是返回配置信息
public String doGetConfig(String tenant, String tag, String clientIp) {
....
// 数据源的加载判断
if (PropertyUtil.isDirectRead()) {
// 数据库拉取配置
configInfoBase = persistService.findConfigInfo(dataId, group, tenant);
} else {
// 本地文件拉取配置
file = DiskUtil.targetFile(dataId, group, tenant);
}
// ...
}
获取配置的server端接口,直接就是返回配置文件信息。
反复注册配置监听:(这个部分请结合上部分client的端源码看)
com.alibaba.nacos.config.server.controller.ConfigController#listener
public void listener(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
Map<String, String> clientMd5Map;
try {
clientMd5Map = MD5Util.getClientMd5Map(probeModify);
} catch (Throwable e) {
throw new IllegalArgumentException("invalid probeModify");
}
// 长轮训
inner.doPollingConfig(request, response, clientMd5Map, probeModify.length());
}
public String doPollingConfig(HttpServletRequest request, HttpServletResponse response,
Map<String, String> clientMd5Map, int probeRequestSize) throws IOException {
// 是否支持长轮训. 支持的
if (LongPollingService.isSupportLongPolling(request)) {
//添加长轮训客户端
longPollingService.addLongPollingClient(request, response, clientMd5Map, probeRequestSize);
return HttpServletResponse.SC_OK + "";
}
// 不支持
// ...
}
public void addLongPollingClient(HttpServletRequest req, HttpServletResponse rsp, Map<String, String> clientMd5Map,
int probeRequestSize) {
int delayTime = SwitchService.getSwitchInteger(SwitchService.FIXED_DELAY_TIME, 500);
long timeout = Math.max(10000, Long.parseLong(str) - delayTime);
if (isFixedPolling()) {
timeout = Math.max(10000, getFixedPollingInterval());
} else {
long start = System.currentTimeMillis();
// 客户端 和 服务端 配置的 md5对比,,不一样立即返回
// 立即返回代表这个客户端的配置已经更新了,客户端需要立马拉取
List<String> changedGroups = MD5Util.compareMd5(req, rsp, clientMd5Map);
if (changedGroups.size() > 0) {
generateResponse(req, rsp, changedGroups);
return;
}
}
// 走到这 说明 client 和 server 配置是一致的,
String ip = RequestUtil.getRemoteIp(req);
// 开启异步servlet
final AsyncContext asyncContext = req.startAsync();
asyncContext.setTimeout(0L);
// 执行线程任务 ClientLongPolling timeout默认是30000
ConfigExecutor.executeLongPolling(
new ClientLongPolling(asyncContext, clientMd5Map, ip, probeRequestSize, timeout, appName, tag));
}
LongPollingService 的构造器:
public LongPollingService() {
allSubs = new ConcurrentLinkedQueue<ClientLongPolling>();
ConfigExecutor.scheduleLongPolling(new StatTask(), 0L, 10L, TimeUnit.SECONDS);
NotifyCenter.registerSubscriber(new Subscriber() {
@Override
public void onEvent(Event event) {
if (isFixedPolling()) {
// Ignore.
} else {
// 订阅了 LocalDataChangeEvent 事件
// 这个事件何时触发呢?
if (event instanceof LocalDataChangeEvent) {
LocalDataChangeEvent evt = (LocalDataChangeEvent) event;
// DataChangeTask线程任务类的作用是什么?在下面会进行说明
ConfigExecutor.executeLongPolling(new DataChangeTask(evt.groupKey, evt.isBeta, evt.betaIps));
}
}
}
@Override
public Class<? extends Event> subscribeType() {
return LocalDataChangeEvent.class;
}
});
}
ClientLongPolling线程任务类:
这个线程类的任务主要就是:反复注册client信息,保持住长连接。
// 内部的属性
final Queue<ClientLongPolling> allSubs;
final AsyncContext asyncContext;
// 客户端配置的md5
final Map<String, String> clientMd5Map;
final String ip;
final String appName;
final int probeRequestSize;
final long timeoutTime;
Future<?> asyncTimeoutFuture;
@Override
public void run() {
asyncTimeoutFuture = ConfigExecutor.scheduleLongPolling(new Runnable() {
@Override
public void run() {
try {
getRetainIps().put(ClientLongPolling.this.ip, System.currentTimeMillis());
// 2.在移除自己
allSubs.remove(ClientLongPolling.this);
if (isFixedPolling()) {
List<String> changedGroups = MD5Util
.compareMd5((HttpServletRequest) asyncContext.getRequest(),
(HttpServletResponse) asyncContext.getResponse(), clientMd5Map);
if (changedGroups.size() > 0) {
sendResponse(changedGroups);
} else {
sendResponse(null);
}
} else {
sendResponse(null);
}
} catch (Throwable t) {
}
}
// 延迟3秒执行
}, timeoutTime, TimeUnit.MILLISECONDS);
// 1.先注册自己
allSubs.add(this);
}
LocalDataChangeEvent 这个事件是何时触发的呢?
我们先看这个接口请求。
com.alibaba.nacos.config.server.controller.ConfigController#publishConfig
public Boolean publishConfig(HttpServletRequest request, HttpServletResponse response,
String dataId, String group,String tenant) throws NacosException {
final Timestamp time = TimeUtils.getCurrentTime();
String betaIps = request.getHeader("betaIps");
ConfigInfo configInfo = new ConfigInfo(dataId, group, tenant, appName, content);
configInfo.setType(type);
// ....
// 持久化更改的配置到数据库
// beta publish
persistService.insertOrUpdateBeta(configInfo, betaIps, srcIp, srcUser, time, true);
// 配置数据改变事件
ConfigChangePublisher.notifyConfigChange(
new ConfigDataChangeEvent(true, dataId, group, tenant, time.getTime())
);
return true;
}
public static void notifyConfigChange(ConfigDataChangeEvent event) {
// 发布事件
NotifyCenter.publishEvent(event);
}
private static boolean publishEvent(final Class<? extends Event> eventType, final Event event) {
EventPublisher publisher = INSTANCE.publisherMap.get(topic);
if (publisher != null) {
// 发布事件
return publisher.publish(event);
}
return false;
}
// com.alibaba.nacos.common.notify.DefaultPublisher#publish
// DefaultPublisher 是一个线程类,处理事件的
public boolean publish(Event event) {
checkIsStart();
// 放到队列里面,当前类继承 Thread 类
boolean success = this.queue.offer(event);
return true;
}
@Override
public void run() {
openEventHandler();
}
void openEventHandler() {
for (; ; ) {
if (shutdown) {
break;
}
final Event event = queue.take();
// 处理事件
receiveEvent(event);
}
}
void receiveEvent(Event event) {
final long currentEventSequence = event.sequence();
// Notification single event listener
for (Subscriber subscriber : subscribers) {
notifySubscriber(subscriber, event);
}
}
@Override
public void notifySubscriber(final Subscriber subscriber, final Event event) {
final Runnable job = new Runnable() {
@Override
public void run() {
// 订阅者
// xxx.service.notify.AsyncNotifyService#AsyncNotifyService
subscriber.onEvent(event);
}
};
job.run();
}
// ConfigDataChangeEvent 的订阅者
NotifyCenter.registerSubscriber(new Subscriber() {
@Override
public void onEvent(Event event) {
if (event instanceof ConfigDataChangeEvent) {
ConfigDataChangeEvent evt = (ConfigDataChangeEvent) event;
// 获取所有client
Collection<Member> ipList = memberManager.allMembers();
// 需要通知的client queue
Queue<NotifySingleTask> queue = new LinkedList<NotifySingleTask>();
for (Member member : ipList) {
queue.add(new NotifySingleTask(dataId, group, tenant, tag, dumpTs, member.getAddress(),evt.isBeta));
}
// 又是一个线程类任务 AsyncTask
ConfigExecutor.executeAsyncNotify(new AsyncTask(nacosAsyncRestTemplate, queue));
}
}
});
}
// 直接看 AsyncTask 的 run方法
public void run() {
executeAsyncInvoke();
}
private void executeAsyncInvoke() {
while (!queue.isEmpty()) {
NotifySingleTask task = queue.poll();
String targetIp = task.getTargetIP();
// 是否存在此机器
if (memberManager.hasMember(targetIp)) {
// 检查该机器的健康情况
boolean unHealthNeedDelay = memberManager.isUnHealth(targetIp);
// 机器不健康
if (unHealthNeedDelay) {
ConfigTraceService.logNotifyEvent(task.getDataId(), task.getGroup(), task.getTenant(), null,
task.getLastModified(), InetUtils.getSelfIP(), ConfigTraceService.NOTIFY_EVENT_UNHEALTH,
0, task.target);
// 重新放到队列里面执行
asyncTaskExecute(task);
} else {
Header header = Header.newInstance();
header.addParam(NotifyService.NOTIFY_HEADER_LAST_MODIFIED, String.valueOf(task.getLastModified()));
AuthHeaderUtil.addIdentityToHeader(header);
// 发送 POST /cs/communication/dataChange 请求,通知改变配置信息
// com.alibaba.nacos.config.server.controller.CommunicationController.notifyConfigInfo
restTemplate.get(task.url, header, Query.EMPTY, String.class, new AsyncNotifyCallBack(task));
}
}
}
}
这个请求是内部调用使用的,官网上并没有对该接口的解释信息,我们看一下它大致做了哪些事情。
com.alibaba.nacos.config.server.controller.CommunicationController#notifyConfigInfo
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) {
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");
// 会走这一步
dumpService.dump(dataId, group, tenant, tag, lastModifiedTs, handleIp);
return true;
}
public void dump(String dataId, String group, String tenant, String tag, long lastModified, String handleIp,
boolean isBeta) {
String groupKey = GroupKey2.getKey(dataId, group, tenant);
// 走这,DumpTask extrends AbstractDelayTask,并不是一个线程类
dumpTaskMgr.addTask(groupKey, new DumpTask(groupKey, tag, lastModified, handleIp, isBeta));
}
@Override
public void addTask(Object key, AbstractDelayTask newTask) {
// 这一步是核心
super.addTask(key, newTask);
MetricsMonitor.getDumpTaskMonitor().set(tasks.size());
}
// com.alibaba.nacos.common.task.engine.NacosDelayTaskExecuteEngine#addTask
@Override
public void addTask(Object key, AbstractDelayTask newTask) {
lock.lock();
try {
// 获取添加的 DumpTask
AbstractDelayTask existTask = tasks.get(key);
if (null != existTask) {
newTask.merge(existTask);
}
// 任务放到一个map里面,,让线程执行
// ConcurrentHashMap
tasks.put(key, newTask);
} finally {
lock.unlock();
}
}
// NacosDelayTaskExecuteEngine 的构造器里面:
public NacosDelayTaskExecuteEngine(String name, int initCapacity, Logger logger, long processInterval) {
super(logger);
// 上面的tasks
tasks = new ConcurrentHashMap<Object, AbstractDelayTask>(initCapacity);
processingExecutor = ExecutorFactory.newSingleScheduledExecutorService(new NameThreadFactory(name));
// 启动 ProcessRunnable
// 这个线程池用来处理配置更新任务,,100 毫秒 执行一次
processingExecutor
.scheduleWithFixedDelay(new ProcessRunnable(), processInterval, processInterval, TimeUnit.MILLISECONDS);
}
// 接下来看处理类即可 ProcessRunnable
private class ProcessRunnable implements Runnable {
@Override
public void run() {
try {
// 处理队列的任务
processTasks();
} catch (Throwable e) {
getEngineLog().error(e.toString(), e);
}
}
}
protected void processTasks() {
Collection<Object> keys = getAllTaskKeys();
for (Object taskKey : keys) {
// 取出任务 DumpTask
AbstractDelayTask task = removeTask(taskKey);
sTaskProcessor processor = getProcessor(taskKey);
// 执行任务 com.alibaba.nacos.config.server.service.dump.processor.DumpAllProcessor.process
// ReAdd task if process failed
if (!processor.process(task)) {
retryFailedTask(taskKey, task);
}
}
}
@Override
public boolean process(NacosTask task) {
long currentMaxId = persistService.findConfigMaxId();
long lastMaxId = 0;
while (lastMaxId < currentMaxId) {
// 取出所有的配置
Page<ConfigInfoWrapper> page = persistService.findAllConfigInfoFragment(lastMaxId, PAGE_SIZE);
if (page != null && page.getPageItems() != null && !page.getPageItems().isEmpty()) {
for (ConfigInfoWrapper cf : page.getPageItems()) {
// 持久化到磁盘
boolean result = ConfigCacheService
.dump(cf.getDataId(), cf.getGroup(), cf.getTenant(), cf.getContent(), cf.getLastModified(),cf.getType());
} else {
lastMaxId += PAGE_SIZE;
}
}
return true;
}
public static boolean dump(String dataId, String group, String tenant, String content, long lastModifiedTs,String type) {
String groupKey = GroupKey2.getKey(dataId, group, tenant);
CacheItem ci = makeSure(groupKey);
ci.setType(type);
final int lockResult = tryWriteLock(groupKey);
try {
final String md5 = MD5Utils.md5Hex(content, Constants.ENCODE);
if (md5.equals(ConfigCacheService.getContentMd5(groupKey))) {
} else if (!PropertyUtil.isDirectRead()) {
// 保存到磁盘
DiskUtil.saveToDisk(dataId, group, tenant, content);
}
// 更新文件md5,,发布更新事件 LocalDataChangeEvent
updateMd5(groupKey, md5, lastModifiedTs);
return true;
} catch (IOException ioe) {
} finally {
releaseWriteLock(groupKey);
}
}
public static void updateMd5(String groupKey, String md5, long lastModifiedTs) {
CacheItem cache = makeSure(groupKey);
if (cache.md5 == null || !cache.md5.equals(md5)) {
cache.md5 = md5;
cache.lastModifiedTs = lastModifiedTs;
// 发布了 LocalDataChangeEvent 事件!
NotifyCenter.publishEvent(new LocalDataChangeEvent(groupKey));
}
}
在这个地方看到了LocalDataChangeEvent事件的发布,LongPollingService构造器里面对这个事件进行了订阅,,此时就会触发订阅事件的执行,走DataChangeTask里面的逻辑。
DataChangeTask线程任务类:
配置主动变更就会触发此任务类的执行。
@Override
public void run() {
ConfigCacheService.getContentBetaMd5(groupKey);
// allSubs 存放的是 长轮训的客户端,上面说到过
for (Iterator<ClientLongPolling> iter = allSubs.iterator(); iter.hasNext(); ) {
ClientLongPolling clientSub = iter.next();
if (clientSub.clientMd5Map.containsKey(groupKey)) {
getRetainIps().put(clientSub.ip, System.currentTimeMillis());
iter.remove(); // Delete subscribers' relationships.
// 写会客户端变更的配置文件的信息。
// 客户端收到后,就会重新拉取此配置的最新配置
clientSub.sendResponse(Arrays.asList(groupKey));
}
}
}
整个过程比较长,也比较枯燥,分析也只是按照主要流程走的,画图比较好理解一些,这是笔者自己调试画的。
大致server端流程就是如此。
可以看出nacos中大量用到了线程池+Queue去完成一些任务处理,用到了事件模式进行通知等等,这些都是我们在业务代码里面可以学习使用的地方。
很早就想简单写写Nacos原理的文章,这次终于是完结了,后面也会坚持写一些其他类型的文章,共勉!