Nacos——注册中心源码详解

Nacos——注册中心源码详解

  • 一、服务注册
    • 客户端
      • 入口
      • 服务注册方法
    • 服务端
      • 1.注册表
      • 2.注册接口信息
      • 3.注册方法
      • 4.注册流程(以临时实例为例)
      • 5.创建空的service加入注册表
      • 6.添加实例
      • 7.临时实例的添加
      • 8.临时实例的集群同步
    • 3.总结
      • 问题一:为什么客户端注册会先开启心跳后发起注册请求?
      • 问题二:服务端注册怎么保证线程安全?
      • 问题三:服务端注册怎么保证性能?(临时实例)
  • 二、心跳机制
    • 临时实例
      • 客户端
        • 1.开启入口
        • 2.心跳开启
        • 3.心跳执行逻辑
      • 服务端
        • 1.心跳请求的处理
        • 2.心跳检测处理
    • 永久实例
      • 1.入口
      • 2.获取所有永久实例加入阻塞队列
      • 3.从队列中获取实例并封装
      • 4.与实例尝试建立连接
      • 5.超时判断
      • 6.正常处理
      • 7.最终判断
    • 总结
  • 三、服务发现
    • 客户端主动获取
      • 客户端
        • 1.入口
        • 2.第一次获取
        • 3.定时延迟任务
      • 服务端
    • 服务端主动推送
      • 服务端
      • 客户端
    • 总结

本文基于1.4.3版本
GitHub地址:https://github.com/alibaba/nacos/releases

一、服务注册

客户端

入口

NamingExample来看

Nacos——注册中心源码详解_第1张图片

反射初始化NacosNamingService

Nacos——注册中心源码详解_第2张图片

服务注册方法

NacosNamingService.registerInstance

1.心跳参数校验
2.最终的服务名格式:serviceName@@groupName
3.如果是临时实例则会开启心跳包
4.服务注册 (参数组装调用API请求注册)

Nacos——注册中心源码详解_第3张图片
Nacos——注册中心源码详解_第4张图片

调用API请求注册 NamingProxy.reqApi

1.单机注册中心,失败重试(默认3次,前提是nacos异常)
2.集群注册中心,随机挑选一个注册,失败则轮询其他注册中心
3.最终调用callServer方法 (API_URL: IP:PORT/nacos/v1/ns/instance

Nacos——注册中心源码详解_第5张图片

callServer方法如下:

Nacos——注册中心源码详解_第6张图片

服务端

1.注册表

先了解一下服务端保存实例信息的结构(下面我们简称叫注册表):

难理解的就是为什么一个服务会有多个集群,不应该一个服务就一个集群吗?

这里可以理解为按机房划分集群,不管有多少个集群都属于你该服务的。比如上海机房有一个SH 集群,深圳机房有一个SZ集群,请求的时候你可以按地区请求最近的集群实例,如果整个地区集群都不可用那么可以请求其他地区的集群实例。

网上找的示例图:
Nacos——注册中心源码详解_第7张图片

2.注册接口信息

注册接口:/nacos/v1/ns/instance

请求参数:

Nacos——注册中心源码详解_第8张图片

3.注册方法

InstanceController.register

Nacos——注册中心源码详解_第9张图片

4.注册流程(以临时实例为例)

ServiceManager.registerInstance

1.创建一个空的service放入注册表,为其 开启一个心跳检测,并将这个service加入监听列表
2.拿到创建好的service
3.完成实例的注册表更新,并完成nacos集群同步

Nacos——注册中心源码详解_第10张图片

5.创建空的service加入注册表

让我们看看是怎么创建空的service的

ServiceManager.createEmptyService

Nacos——注册中心源码详解_第11张图片

ServiceManager.putServiceAndInit

Nacos——注册中心源码详解_第12张图片

6.添加实例

ServiceManager.addInstance

Nacos——注册中心源码详解_第13张图片

里面最重要的就是consistencyService.put(key, instances) 方法

consistencyService有很多种实现,根据实例的类型来判断具体走哪种实现方式,这里我们以临时实例为例,主要看看DistroConsistencyServiceImpl

Nacos——注册中心源码详解_第14张图片

DistroConsistencyServiceImpl.put 临时实例的注册方法

Nacos——注册中心源码详解_第15张图片

7.临时实例的添加

onPut方法:

1.会将任务放入Notifier内部的阻塞队列中,Notifier是个Runnable(异步执行任务)
2.最后会回到Service.onChange方法更新实例,内部调用updateIPs方法,这里面需要注意更新后会触发一个服务变更事件(后面有用)

Service.updateIPs

public void updateIPs(Collection instances, boolean ephemeral) {
        // 准备一个Map,key是cluster,值是集群下的Instance集合
        Map> ipMap = new HashMap<>(clusterMap.size());
        // 获取服务的所有cluster名称
        for (String clusterName : clusterMap.keySet()) {
            ipMap.put(clusterName, new ArrayList<>());
        }
        
        for (Instance instance : instances) {
            try {
                if (instance == null) {
                    Loggers.SRV_LOG.error("[NACOS-DOM] received malformed ip: null");
                    continue;
                }
                // 判断实例是否包含clusterName,没有的话用默认cluster
                if (StringUtils.isEmpty(instance.getClusterName())) {
                    instance.setClusterName(UtilsAndCommons.DEFAULT_CLUSTER_NAME);
                }
                // 判断cluster是否存在,不存在则创建新的cluster
                if (!clusterMap.containsKey(instance.getClusterName())) {
                    Loggers.SRV_LOG
                            .warn("cluster: {} not found, ip: {}, will create new cluster with default configuration.",
                                    instance.getClusterName(), instance.toJson());
                    Cluster cluster = new Cluster(instance.getClusterName(), this);
                    cluster.init();
                    getClusterMap().put(instance.getClusterName(), cluster);
                }
                // 获取当前cluster实例的集合,不存在则创建新的
                List clusterIPs = ipMap.get(instance.getClusterName());
                if (clusterIPs == null) {
                    clusterIPs = new LinkedList<>();
                    ipMap.put(instance.getClusterName(), clusterIPs);
                }
                // 添加新的实例到 Instance 集合
                clusterIPs.add(instance);
            } catch (Exception e) {
                Loggers.SRV_LOG.error("[NACOS-DOM] failed to process ip: " + instance, e);
            }
        }
        
        for (Map.Entry> entry : ipMap.entrySet()) {
            //make every ip mine
            List entryIPs = entry.getValue();
            // 将实例集合更新到 clusterMap(注册表)
            clusterMap.get(entry.getKey()).updateIps(entryIPs, ephemeral);
        }
        
        setLastModifiedMillis(System.currentTimeMillis());
        //触发服务变更事件
        getPushService().serviceChanged(this);
        StringBuilder stringBuilder = new StringBuilder();
        
        for (Instance instance : allIPs()) {
            stringBuilder.append(instance.toIpAddr()).append("_").append(instance.isHealthy()).append(",");
        }
        
        Loggers.EVT_LOG.info("[IP-UPDATED] namespace: {}, service: {}, ips: {}", getNamespaceId(), getName(),
                stringBuilder.toString());
        
    }

8.临时实例的集群同步

distroProtocol.sync()临时实例集群同步

  1. 遍历集群中其他节点
  2. 定义一个DistroDelayTask异步任务放入一个ConcurrentHashMap中,会有一个ScheduledExecutorService线程池定时从这个map中取任务执行
    Nacos——注册中心源码详解_第16张图片

线程池的定义在NacosDelayTaskExecuteEngine中:
Nacos——注册中心源码详解_第17张图片

上诉线程池执行的任务就是NacosDelayTaskExecuteEngine.processTasks()如下:

protected void processTasks() {
        // 获取任务map中所有的key
        Collection keys = getAllTaskKeys();
        //遍历key 并执行任务
        for (Object taskKey : keys) {
             // 取一个任务便从map中移除一个任务
            AbstractDelayTask task = removeTask(taskKey);
            if (null == task) {
                continue;
            }
            NacosTaskProcessor processor = getProcessor(taskKey);
            if (null == processor) {
                getEngineLog().error("processor not found for task, so discarded. " + task);
                continue;
            }
            try {
                // ReAdd task if process failed
                // 尝试执行同步任务,如果失败会重试
                if (!processor.process(task)) {
                    retryFailedTask(taskKey, task);
                }
            } catch (Throwable e) {
                getEngineLog().error("Nacos task execute error : " + e.toString(), e);
                // 如果失败会重试
                retryFailedTask(taskKey, task);
            }
        }
    }
 
  

DistroDelayTaskProcessor.process:

任务的执行被放入到process方法中,并被封装成DistroSyncChangeTask异步任务,又被塞到一个不知名封装好的地方(是一个阻塞队列,同样有地方取出来执行,我们直接看这个任务的执行)

Nacos——注册中心源码详解_第18张图片

DistroSyncChangeTask.run

1.syncData方法最终会到NamingProxy.syncData方法,执行HTTP请求,同步数据
2.如果失败了,则又会调用NacosDelayTaskExecuteEngine.addTask()方法重新将DistroDelayTask任务放进ConcurrentHashMap中,重复上述的processTasks方法

Nacos——注册中心源码详解_第19张图片

Nacos——注册中心源码详解_第20张图片

3.总结

Nacos——注册中心源码详解_第21张图片

  • 客户端:启动则获取自身配置信息,发起http请求注册,临时实例同时会开启心跳机制(下面会说),服务端是单机的情况下请求失败会重试三次,服务端是单机的集群的情况下请求失败会轮询请求
  • 服务端
    1. 本地通过一个Map保存所有服务信息,注册的实质就是往map里面添加信息
    2. 会先创建空的服务,后更新服务中的实例信息
    3. 服务创建后会初始化服务,启动心跳检测
    4. 往服务中添加实例的时候会判断实例是永久实例还是临时实例,不同类型的实例有不同的处理方式
    5. 注册后同时会发布服务变更事件(后面说,先记着这个事件)

问题一:为什么客户端注册会先开启心跳后发起注册请求?

因为心跳是异步定时执行,就算后续的注册发生某意外注册失败,心跳机制还可以弥补注册(因为心跳也可以注册),如果是先发起注册后开启心跳,有可能注册发生某意外就直接终止了,心跳还没开启

问题二:服务端注册怎么保证线程安全?

服务器注册会先创建一个空的服务,后对该服务填充信息初始化,保存服务的map用ConcurrentHashMap修饰的,所以此过程是线程安全的,后续再对服务内实例更新的时候,采用synchronized对该服务做了加锁操作

问题三:服务端注册怎么保证性能?(临时实例)

前置操作时采用ConcurrentHashMap和synchronized锁服务,前者是最优的线程安全map,后者锁的是“服务”颗粒度一定程度的保证了性能,后续均采用了异步更新,如本地注册表更新采用了阻塞队列异步执行,临时实例集群同步过程中同样采用了阻塞队列异步执行机制,因为为阻塞的异步执行,所以保值性能的同时也保证了资源不会占用异常

二、心跳机制

临时实例:实例发起心跳请求,服务端处理请求,并需要进行心跳检测

永久实例:服务端主动发起健康检测

临时实例

Nacos——注册中心源码详解_第22张图片

客户端

临时实例在注册的时候会开启心跳包,这个在前面有说(默认5s心跳)

1.开启入口

NacosNamingService.registerInstance()

buildBeatInfo就是心跳信息的封装,我们主要看addBeatInfo方法
Nacos——注册中心源码详解_第23张图片

2.心跳开启

BeatReactor.addBeatInfo()

把心跳任务BeatTask丢到了延迟线程池里面执行,所以主要执行逻辑在BeatTask

Nacos——注册中心源码详解_第24张图片

3.心跳执行逻辑

BeatTask.run()

1.发送HTTP心跳请求 URL地址为:/nacos/v1/ns/instance/beat
2.如果当前实例在注册中心未找到就重新注册
3.不管结果如何添加心跳任务,继续定时发起心跳(继续将任务丢到线程池里面执行)

Nacos——注册中心源码详解_第25张图片

心跳请求如下,URL地址为:/nacos/v1/ns/instance/beat

Nacos——注册中心源码详解_第26张图片

服务端

1.心跳请求的处理

从上面请求URL:/nacos/v1/ns/instance/beat ,我们很容易能找到处理请求逻辑:

InstanceControllerregister.beat()

我这里省略了前面的校验逻辑,直接看主体逻辑:
1.从注册表中获取实例信息,若无则重新注册
2.从注册表中获取服务,若无则直接异常(实例都注册完了,服务还找不到是不合理的)
3.开启异步任务将临时实例状态 置为健康状态,然后返回

Nacos——注册中心源码详解_第27张图片

2.心跳检测处理

实例如果宕机或者其他什么请求无法发送心跳,那么服务端自然也要对这个实例进行处理,就在服务初始化的时候,会开启会实例的心跳检测任务,上面也有提到过

ServiceManager.putServiceAndInit()
Nacos——注册中心源码详解_第28张图片

Service.init()

这个内部呢,就会有个ClientBeatCheckTask任务被放入了线程池,5s执行一次

Nacos——注册中心源码详解_第29张图片
Nacos——注册中心源码详解_第30张图片

ClientBeatCheckTask.run()

ClientBeatCheckTask任务主要做了两件事:

  1. 找到心跳超时的实例,改变其健康状态,并发布serviceChange事件(后面说),还有实例心跳超时事件
  2. 找到满足删除条件的实例,从注册表中删除该实例信息(HTTP请求调用API,异步删除)
  3. 默认15s超时,30s剔除

Nacos——注册中心源码详解_第31张图片

永久实例

Nacos——注册中心源码详解_第32张图片

1.入口

入口和上面临时实例入口差不多,但永久实例是在集群初始化的时候,而临时实例是在服务初始化的时候

Cluster.init如下:
1.检测任务就是HealthCheckTask,这是个异步任务
2.延迟任务第一次(2000ms+5000ms以内随机数)执行,后续在1000ms-5000ms内浮动

Nacos——注册中心源码详解_第33张图片

Nacos——注册中心源码详解_第34张图片

Nacos——注册中心源码详解_第35张图片

HealthCheckTask任务如下:

  1. process方法有多种实现意味着有多种检测方案(这里以TCP为例)
  2. 不管结果如何继续延迟执行,约等于是个定时器(因为每次延迟时间不同)
  3. HealthCheckTask实例化的时候同时初始化了TcpSuperSenseProcessor,该方法是一个Runnable,会执行TcpSuperSenseProcessor.run方法(以TCP为例)

Nacos——注册中心源码详解_第36张图片
可以看到主动检测有多种方法:
Nacos——注册中心源码详解_第37张图片

2.获取所有永久实例加入阻塞队列

TcpSuperSenseProcessor .process()

这里会遍历所有永久实例并将实例封装成Beat加入到阻塞队列中
Nacos——注册中心源码详解_第38张图片

3.从队列中获取实例并封装

上面把永久实例信息放到了阻塞队列中,那么就肯定有方法去取,那是哪里呢?还记得前面说过TcpSuperSenseProcessor本身也是个异步任务吗?

TcpSuperSenseProcessor.run方法如下:

  1. 会先从阻塞队列中取出实例信息并封装,然后尝试与实例建立socket连接
  2. 最后判断连接状态,连接上了就进去实例健康处理并断开连接
  3. 同时会有一个异步的延迟任务,去检测这段时间内是否连接上过,这段时间内没连接上过说明连接超时了
  4. 可以看到这个run方法是个死循环

Nacos——注册中心源码详解_第39张图片

TcpSuperSenseProcessor.processTask方法如下:

  1. 从阻塞队列中取实例信息,并封装成TaskProcessor异步任务
  2. 批量提交任务,就是执行TaskProcessor异步任务

Nacos——注册中心源码详解_第40张图片

4.与实例尝试建立连接

TaskProcessor.run()

  1. 主要就是尝试建立socket连接
  2. 开启一个超时检测的延迟任务TimeOutTask(500ms)

Nacos——注册中心源码详解_第41张图片

5.超时判断

TimeOutTask.run()

因为已经延迟执行了,就判断这段时间内是否连接上过,没有就代表超时,超时会进入finishCheck方法

Nacos——注册中心源码详解_第42张图片

6.正常处理

前面都执行完了,就到TcpSuperSenseProcessor.run里面最后的PostProcessor异步任务了,连接成功会进入finishCheck方法

PostProcessor.run()

Nacos——注册中心源码详解_第43张图片

7.最终判断

Beat.finishCheck

连接成功或超时连接都会进到这里处理,不管如何都会发布服务变更事件,只会改变实例状态不会剔除实例
Nacos——注册中心源码详解_第44张图片

总结

  • 临时实例:
    • 采用客户端心跳检测模式,心跳检测周期5秒
    • 心跳间隔超过15秒(默认)则标记为不健康
    • 心跳间隔超过30秒(默认)则从服务列表删除
  • 永久实例:
    • 采用服务端主动健康检测方式
    • 周期为2000 + 5000毫秒内的随机数
    • 检测异常只会标记为不健康,不会删除

三、服务发现

实例是如何得知其他实例的信息呢?毕竟需要远程调用嘛
两种方式:1.客户端主动获取(定时更新)、2.服务端主动推送(长连接推送变更信息)

客户端主动获取

Nacos——注册中心源码详解_第45张图片

客户端

1.入口

NacosNamingService.getAllInstances

该方法就是获取所需的服务信息

Nacos——注册中心源码详解_第46张图片

2.第一次获取

HostReactor.getServiceInfo

1.先是故障转移机制判断是否去本地文件中读取信息,读到则返回
2.再去本地服务列表读取信息(本地缓存),没读到则创建一个空的服务,然后立刻去nacos中读取更新
3.读到了就返回,同时开启定时更新,定时向服务端同步信息 (正常1s,异常最多60s一次)

Nacos——注册中心源码详解_第47张图片

HostReactor.updateServiceNow

Nacos——注册中心源码详解_第48张图片

HostReactor.updateService

属性的serverProxy,这里面就是接口调用请求了

Nacos——注册中心源码详解_第49张图片

3.定时延迟任务

HostReactor.scheduleUpdateIfAbsent

这里全先判断定时任务是否已经在异步任务列表中了,不在才会添加一个UpdateTask任务延迟执行

Nacos——注册中心源码详解_第50张图片

在这里插入图片描述

UpdateTask.run

UpdateTask类就是一个异步执行类,里面会调用updateService方法更新服务信息,同时结束又会开启延迟,延迟的时间跟请求失败的次数有关,最多60s,正常是1s一次

Nacos——注册中心源码详解_第51张图片

无论是updateService方法、refreshOnly方法,还是刚开始的直接去nacos拉取信息的方法都会调用serverProxy.queryList方法,这个方法就是HTTP请求获取信息:

获取服务信息列表URL:/nacos/v1/ns/instance/list

NamingProxy.queryList

Nacos——注册中心源码详解_第52张图片

服务端

InstanceController.list()

服务端这边处理请求就比较简单了,除去参数获取以及相关校验就剩服务列表的获取了

Nacos——注册中心源码详解_第53张图片

InstanceController.doSrvIpxt如下:

这里记住有个PushService

Nacos——注册中心源码详解_第54张图片

服务端主动推送

既然是主动推送那么就需要两个条件:1.建立长连接2.触发推送的事件

Nacos——注册中心源码详解_第55张图片

服务端

上面InstanceController.doSrvIpxt中的pushService.addClient就是把客户端UDP、IP等信息封装成PushClient对象存储在PushService类中,方便以后服务变更后推送消息

PushService类实现ApplicationListener接口,监听ServiceChangeEvent(服务变更事件)

Nacos——注册中心源码详解_第56张图片

ServiceChangeEvent事件处理就在当前类下:

Nacos——注册中心源码详解_第57张图片

事件触发则是PushService.serviceChanged方法,这个方法之前我们就见过,在服务注册里面,心跳里面也有,服务变更就会调用这个方法,触发事件让服务端主动推送服务变更信息
Nacos——注册中心源码详解_第58张图片

客户端

客户端是在PushReceiver类里面,这个类是个Runnable会在HostReactor中被实例化

Nacos——注册中心源码详解_第59张图片

Nacos——注册中心源码详解_第60张图片

PushReceiver.run()

收到服务端的信息就会交给HostReactor.processServiceJson处理

HostReactor.processServiceJson就会更新本地缓存的信息,上述客户端主动拉取的时候也会调用这个方法更新

Nacos——注册中心源码详解_第61张图片

HostReactor.processServiceJson

中间一大段省略了哈,最重要的就是那几步:

  1. 更新本地缓存
  2. 发布实例变更事件
  3. 写入磁盘(故障转移机制)
    Nacos——注册中心源码详解_第62张图片

总结

服务的发现有两种方式

客户端主动获取

  • 会先读取缓存,缓存内读取不到则会去服务端获取,同时开启一个定时任务定时更新
  • 定时任务1s一次,异常时会延长时间最长60s
  • 拉取URL:/nacos/v1/ns/instance/list

服务端主动推送

  • 服务端和客户端在启动后会建立一个长连接
  • 服务端服务变更后会发布服务变更事件ServiceChangeEvent,会通过长连接将变更后的信息发送给客户端
  • 客户端更新的方式是hostReactor.processServiceJson方法会写入缓存、发布实例变更事件、写入磁盘

你可能感兴趣的:(nacos,java,分布式,微服务,nacos)