Nacos源码分析【临时节点】

作为后起之秀,Nacos基本要取代Eureka的位置,我没有在项目中使用过,只在本地跑了一下,处于好奇我想浅显的分析Nacos的实现,看看是否比Eureka更优秀

官网图
在这里插入图片描述
从图中可以看到,nacos似乎比eureka多了一致性协议,eureka是p2p,也就是非强一致性,貌似nacos解决了这个问题,nacos还有控制台,嗯这个确实更方便了

一、服务注册

1.1 客户端

eureka是利用SmartLifeCycle接口的生命周期方法來完成调用的。
nacos是利用的事件监听机制来触发的。都是固定的套路,首先就是分析NacosDiscoveryAutoConfiguration ,然后里面有个@Bean NacosAutoServiceRegistration.

在这里插入图片描述
spring的时间监听某个时间是通过泛型来判断的ApplicationListener<WebServerInitializedEvent>,也就是说是是web容器初始化完成就开始调用的。然后就是常规的封装注册信息,然后调用http接口

 //临时节点特殊处理
 if (instance.isEphemeral()) {
            BeatInfo beatInfo = new BeatInfo();
            beatInfo.setServiceName(NamingUtils.getGroupedName(serviceName, groupName));
            beatInfo.setIp(instance.getIp());
            beatInfo.setPort(instance.getPort());
            beatInfo.setCluster(instance.getClusterName());
            beatInfo.setWeight(instance.getWeight());
            beatInfo.setMetadata(instance.getMetadata());
            beatInfo.setScheduled(false);
            long instanceInterval = instance.getInstanceHeartBeatInterval();
            beatInfo.setPeriod(instanceInterval == 0 ? DEFAULT_HEART_BEAT_INTERVAL : instanceInterval);

            beatReactor.addBeatInfo(NamingUtils.getGroupedName(serviceName, groupName), beatInfo);
        }

//调用http接口		
serverProxy.registerService(NamingUtils.getGroupedName(serviceName, groupName), groupName, instance);

在调用http接口的时候,如果有多个服务端,他是随机选取的,如果随机选取的服务端调用失败,那么开始顺序的调用其他服务端接口


还有稍微有点特殊的地方就是,注册的时候它分临时节点还是持久化节点,如果是临时时节点他有额外的动作:立马加上心跳


心跳代码:

//搞了一个一次性的心跳
executorService.schedule(new BeatTask(beatInfo), 0, TimeUnit.MILLISECONDS);

//然后在BeatTask方法后有立马开启一个新的调度
run(){
	long result = serverProxy.sendBeat(beatInfo);//发送http请求
	long nextTime = result > 0 ? result : beatInfo.getPeriod();
	//立马又开启一个
	executorService.schedule(new BeatTask(beatInfo), nextTime, TimeUnit.MILLISECONDS);
}

我目前有2个疑问

  1. 为什么只有临时节点会立马开启一个心跳?
  2. 他的心跳这种定时任务,一旦上次发生异常,那么下次不会有心跳发生了(因为不会立马开启一个新的调度)

1.2 服务端

读了服务端代码感觉他的方法名字起的有一点乱,而且逻辑有点复杂,而且还不写注释。

InstanceControllerregister()是入口

1.1.2 创建空服务

服务的创建中存在很多逻辑,这也是我读源码的时候头疼的地方,功能耦合性太高,你创建服务就创建服务,可以再其他地方进行类似健康检查之类的工作,他基本都柔和到一起了,它主要包含下面三个大功能(一个创建服务动作,做了好多事情)

  • 创建Service,然后存在注册表中
  • 开启健康检查任务,主要是为了服务剔除
  • 加入listener private Map<String, CopyOnWriteArrayList<RecordListener>> listeners = new ConcurrentHashMap<>();listener的子类就是service,他有2个重要方法onChnageonDelete,主要就是处理节点增删改之后的处理逻辑。这里加入listener,其实也是等待这处理的意思。nacos注册节点大思想就是异步的思想,比如现在他要写一个instance到内存,他是直接提交一个notification然后开启一个while死循环,然后这些listener逐个处理

补充知识:关于注册,必须了解它的注册表,他的结构也非常复杂,必须通过官网的图解结合才能理解

在这里插入图片描述
翻译成代码就是Map<String, Map<String, Service>> serviceMap,其中第一个key是namespace,第二个key是service名字,最终存的是Service
该类中有一个重要字段private Map<String, Cluster> clusterMap = new HashMap<>();,这里就牵扯出了Cluster的概念。Cluster中有三个字段

 //一个cluster有多个实例
 @JSONField(serialize = false)
 private Set<Instance> persistentInstances = new HashSet<>();

 @JSONField(serialize = false)
 private Set<Instance> ephemeralInstances = new HashSet<>();

 //持有属于service的引用
 @JSONField(serialize = false)
 private Service service;
  • namespace:是起到环境隔离的效果,比如dev stage prod
  • service name:指的就是一个具体的服务,该服务可能有N个实例
  • cluster:这个概念是最难理解的,下面是官网的图,可以理解成是的集合一组实例的集合,通过集群进行数据同步,而且可以看出serive和cluster是相互持有引用的
    在这里插入图片描述
//创建空服务的入口
 createEmptyService(namespaceId, serviceName, instance.isEphemeral());
 
 //创建空服务最终调用的方法 
 public void createServiceIfAbsent(String namespaceId, String serviceName, boolean local, Cluster cluster) throws NacosException {
        Service service = getService(namespaceId, serviceName);
        //判断是否有服务
        if (service == null) {
            Loggers.SRV_LOG.info("creating empty service {}:{}", namespaceId, serviceName);
            service = new Service();         
            if (cluster != null) {
            	//如果cluster不为空,那么service和cluster相互更新引用
                cluster.setService(service);
                service.getClusterMap().put(cluster.getName(), cluster);
            }
            service.validate();
            if (local) {
            	//临时节点
                putServiceAndInit(service);
            } else {
            	//持久化节点
                addOrReplaceService(service);
            }
        }
    }

下面针对创建空服务三个功能进行具体分析
1. 存在注册表中

serviceMap.get(service.getNamespaceId()).put(service.getName(), service);

2. 健康检查
nacos的定时任务粒度是service,也就是每个service就开启一个定时任务,这种控制带来的就是更灵敏。但是占用内存更高。
心跳是每5秒执行一次。
如果超过15秒没有心跳那么就会发布心跳超时任务
如果超过30秒没有心跳那么就删除该节点


3. 添加到listener中

//临时
consistencyService.listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), true), service);
//持久化
consistencyService.listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), false), service);

什么是listener你?,看下面的源码,他就是一个map,key主要由namespace和service组成, service本身就是集成RecordListener,所以CopyOnWriteArrayList中存放的就是service,可以理解成他就是一个service的集合,

Map<String, CopyOnWriteArrayList<RecordListener>> listeners

单独分析listener没有意义,必须要结合notifier来分析



1.1.3 存储instance

上面已经创建了service,相当于给instance创建了个容器,下面就是朝容器里面放instance,这里又分为临时节点和持久化节点。
主要做了2部分内容,一个是向阻塞队列中插入值,然后有个while循环一直以阻塞的方式取改队列,然后和上面的listener结合起来进行节点状态变更的处理,主要就是判断是增加了节点,删除了节点等。另外一部分好像是服务同步的。

入口

consistencyService.put(key, instances);//这个就是存储instance和服务同步的入口

onPut(key, value);//加入一个新的notifier,然后异步的处理加入新的instance
taskDispatcher.addTask(key);//处理服务同步

这次只针对临时节点分析

这里主要是取阻塞队列之后的处理比较重要。主要切入方法是updateIPs,针对这个源码有几个局部变量我分析一下

  • clusterMap:key是cluster name,value是cluster对象
  • ipMap:key是cluster名字,value是instance列表,这个方法看上去很长,其实都是在组织ipMap
public void updateIPs(Collection<Instance> instances, boolean ephemeral) {
        Map<String, List<Instance>> ipMap = new HashMap<>(clusterMap.size());
        for (String clusterName : clusterMap.keySet()) {
            ipMap.put(clusterName, new ArrayList<>());
        }

        for (Instance instance : instances) {
            ...................
        }
		//上面代码都是在组织ipMap
		//下面的代码就是循环调用cluster的updateIPs方法
        for (Map.Entry<String, List<Instance>> entry : ipMap.entrySet()) {
            //make every ip mine
            List<Instance> entryIPs = entry.getValue();
            clusterMap.get(entry.getKey()).updateIPs(entryIPs, ephemeral);
        }
    }

搞了这么一大圈就是就是调用cluster的updateIPs方法,因为cluster是instance的集合,nacos中添加一个instance做的逻辑太复杂了,按照道理添加实例无非就是向注册表中插入一个instance,但是他搞这么麻烦主要是他的那段代码也是为了适应更新操作。

  • 找出更新的节点,然后进行处理
  • 找出新增的节点,然后进行处理
  • 找出要剔除的节点,然后进行处理
  • 整个过程处处体现了copyOnWrite的思想
 public void updateIPs(List<Instance> ips, boolean ephemeral) {

        Set<Instance> toUpdateInstances = ephemeral ? ephemeralInstances : persistentInstances;

        HashMap<String, Instance> oldIPMap = new HashMap<>(toUpdateInstances.size());

        for (Instance ip : toUpdateInstances) {
            oldIPMap.put(ip.getDatumKey(), ip);
        }

        List<Instance> updatedIPs = updatedIPs(ips, oldIPMap.values());
        if (updatedIPs.size() > 0) {
           ......
        }

        List<Instance> newIPs = subtract(ips, oldIPMap.values());
        if (newIPs.size() > 0) {
           ....
        }

        List<Instance> deadIPs = subtract(oldIPMap.values(), ips);

        if (deadIPs.size() > 0) {
           ....
        }

        toUpdateInstances = new HashSet<>(ips);

        if (ephemeral) {
            ephemeralInstances = toUpdateInstances;
        } else {
            persistentInstances = toUpdateInstances;
        }
    }

1.1.4 服务同步

核心代码在下面,关于syncData的源码,在后面在分析

 long timestamp = System.currentTimeMillis();
 boolean success = NamingProxy.syncData(data, task.getTargetServer());
 if (!success) {
     SyncTask syncTask = new SyncTask();
     syncTask.setKeys(task.getKeys());
     syncTask.setRetryCount(task.getRetryCount() + 1);
     syncTask.setLastExecuteTime(timestamp);
     syncTask.setTargetServer(task.getTargetServer());
     retrySync(syncTask);
 } else {
     // clear all flags of this task:
     for (String key : task.getKeys()) {
         taskMap.remove(buildKey(key, task.getTargetServer()));
     }
 }

至此整个临时节点的服务注册就算分析完了,我大致在总结一遍

  1. 创建空service对象,在空service对象中开启定时剔除任务,向listener中添加对象为了后面异步存储instance服务(注册表结构复杂,然后一个方法里面 功能太多)
  2. 存储instance对象,这里采用异步的方式从阻塞队列中取数据,然后还处理(异步是特色,copyOnWrite对整体性能进一步提高,但是我总是感觉一个方法里面的功能还是太多,导致功能不清晰)
  3. 增加同步任务(这个整体功能清晰,有spring的感觉)

二、服务续约

在分析服务端代码的时候发现,如果在心跳的时候发现没有instance,会调用注册接口,这个是为了啥?

if (instance == null) {
            instance = new Instance();
            instance.setPort(clientBeat.getPort());
            instance.setIp(clientBeat.getIp());
            instance.setWeight(clientBeat.getWeight());
            instance.setMetadata(clientBeat.getMetadata());
            instance.setClusterName(clusterName);
            instance.setServiceName(serviceName);
            instance.setInstanceId(instance.generateInstanceId());
            instance.setEphemeral(clientBeat.isEphemeral());

            serviceManager.registerInstance(namespaceId, serviceName, instance);
        }

        Service service = serviceManager.getService(namespaceId, serviceName);

        if (service == null) {
            throw new NacosException(NacosException.SERVER_ERROR, "service not found: " + serviceName + "@" + namespaceId);
        }

        service.processClientBeat(clientBeat);

processClientBeant的方法中,和eureka稍微不同的是,它也是采用异步处理

HealthCheckReactor.scheduleNow(clientBeatProcessor);//异步的去处理

instance.setLastBeat(System.currentTimeMillis());//心跳核心代码

三、服务同步

 public String onSyncDatum(HttpServletRequest request, HttpServletResponse response) throws Exception {
        Map<String, Datum<Instances>> dataMap =
            serializer.deserializeMap(entity.getBytes(), Instances.class);

        for (Map.Entry<String, Datum<Instances>> entry : dataMap.entrySet()) {
            if (KeyBuilder.matchEphemeralInstanceListKey(entry.getKey())) {
                String namespaceId = KeyBuilder.getNamespace(entry.getKey());
                String serviceName = KeyBuilder.getServiceName(entry.getKey());
                if (!serviceManager.containService(namespaceId, serviceName)
                    && switchDomain.isDefaultInstanceEphemeral()) {
                    //如果没有服务就创建一个控服务,在注册的时候已经分析过
                    serviceManager.createEmptyService(namespaceId, serviceName, true);
                }
				//前面注册的时候也分析过这个方法,这个方法就是加入一个notifier
                consistencyService.onPut(entry.getKey(), entry.getValue().value);
            }
        }
        return "ok";
    }

服务同步的代码和注册的代码基本是一致的。没有服务就创建空服务,然后加入一个notifier异步的处理新的instance。

四、服务下线

其实在服务注册的时候已经分析过了,他的服务剔除是以service为单位的。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Nacos 是一个开源的分布式配置中心和服务发现框架,它提供了服务注册、发现、配置管理等功能。下面是对 Nacos 源码的简要分析: 1. 项目结构:Nacos 代码库主要包括 core 模块、config 模块、discovery 模块等。core 模块提供了核心的数据结构和服务注册与发现的功能,config 模块实现了配置管理相关的功能,discovery 模块实现了服务发现的功能。 2. 注册与发现:Nacos 使用了基于 Raft 算法的一致性协议来实现注册与发现功能。核心模块中的 ServerListManager 负责管理服务列表的变更和更新,InstanceEventProcessor 负责处理服务实例事件。服务注册和发现的过程涉及到数据存储和同步,涉及到的类有 LocalServerData、MetadataManager、SnapshotManager 等。 3. 配置管理:Nacos 的配置管理功能由 config 模块实现。核心类是 ConfigServiceImpl,它负责处理配置的读写和监听。在配置写入时,会通过 ConfigChangePublisher 将变更发布给订阅者。ConfigChangeListeners 负责处理配置变更事件。 4. 数据存储:Nacos 的数据存储使用了内置的嵌入式数据库 Derby。Derby 提供了基于文件的持久化存储,用于存储配置数据、注册数据等。 5. 服务路由:Nacos 通过实现了 LoadBalancer 接口来实现服务路由的功能。LoadBalancer 负责选择可用的服务实例,实现了负载均衡的策略。 这只是对 Nacos 源码的简要分析Nacos源码结构比较复杂,涉及到的技术栈也比较丰富。如果你对具体的实现细节有更多的疑问,可以参考 Nacos 的官方文档或者深入阅读源码

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值