Nacos源码分析【临时节点】

本文详细分析了Nacos服务注册的实现,包括客户端和服务端的流程,强调了Nacos的一致性协议和控制台优势。在服务注册过程中,Nacos使用事件监听和心跳机制,服务端处理包括创建服务、健康检查和监听器。服务续约时,若心跳失败,服务会重新注册。服务同步通过异步处理确保数据一致。相较于Eureka,Nacos提供了更强的一致性和管理功能。
摘要由CSDN通过智能技术生成

作为后起之秀,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
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值