Nacos配置中心Server端源码解析

源码阅读前的建议:
1.对配置中心client端源码要有了解。

1.前序

文章承接上文,下面对上文中的几个请求到server端的接口进行详解。
注意:server端源码版本是 nacos-1.4.1,直接去git下载即可。

server 端启动类是:com.alibaba.nacos.Nacos
单机启动的话,需要加上jvm启动参数 -Dnacos.standalone=true进行启动。

我们直接看第一个请求,就是client向server获取配置的请求

1. GET /v1/cs/configs

注意是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端接口,直接就是返回配置文件信息。

2.POST /v1/cs/configs/listener

反复注册配置监听:(这个部分请结合上部分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));
            }
        }
    }
}

上面的流程有点长,简单画图表示一下:
在这里插入图片描述

3.POST /cs/communication/dataChange

这个请求是内部调用使用的,官网上并没有对该接口的解释信息,我们看一下它大致做了哪些事情。
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<Object, AbstractDelayTask> tasks;
        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原理的文章,这次终于是完结了,后面也会坚持写一些其他类型的文章,共勉!

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Nacos配置中心控制台的源码分析可以帮助我们深入理解其实现细节和工作原理。以下是一个大致的源码分析过程: 1. 入口类分析:首先,我们需要找到Nacos配置中心控制台的入口类。该类通常是一个Spring Boot应用的启动类,负责初始化和启动整个应用。我们可以查找包含main方法的类,或者在启动脚本中找到应用的入口点。 2. 依赖分析:接下来,我们需要分析应用所依赖的第三方库和框架。查看应用的pom.xml文件或者build.gradle文件,可以获取到所依赖的各个库和对应版本。这些依赖通常包括Spring框架、Nacos客户等。 3. 配置加载与解析Nacos配置中心控制台需要加载和解析配置,包括数据库配置、Nacos服务地址配置等。我们可以查找相关的配置文件或者代码片段,了解配置的加载和解析过程。 4. 控制器与路由:控制台通常提供了一些Web接口供前调用。我们可以查找控制器类,分析其中的方法和注解,了解各个接口的功能和路由规则。 5. 页面模板与前交互:配置中心控制台通常包含一些页面模板和与前的交互逻辑。我们可以查找相关的HTML、CSS和JavaScript文件,分析页面的结构和交互逻辑。 6. 调用Nacos API:控制台需要与Nacos服务器进行通信,调用Nacos的API获取和修改配置信息。我们可以查找相关的API调用,了解控制台是如何与Nacos服务器进行通信的。 通过以上分析,我们可以逐步了解Nacos配置中心控制台的实现细节和工作原理。需要注意的是,具体的源码分析过程会因项目结构和代码风格而有所不同。以上只是一个大致的指导,具体分析还需根据实际情况来进行。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值