环境介绍
本文基于1.4.3版本
GitHub地址:https://github.com/alibaba/nacos/releases
也可以直接看我的注释版本:GitHub(分支 v1.4.3),如果有帮助,麻烦给个star
一、Nacos 服务架构
二、Nacos 服务注册流程图(源码级别)
1. 服务注册:当微服务
实例启动时,它会将自己的信息(如 IP 地址、端口号、服务名称等)注册到注册中心。这通常需要发送一个注册请求到注册中心来完成。调用 Nacos Server POST
/nacos/v1/ns/instance
接口请求来完成注册。
2. 服务存储:注册中心接收到服务实例的注册信息后,会将其存储在注册表中。注册表是注册中心的核心组件,用于保存所有微服务实例的信息。在 Nacos 服务端是存储在下面这样一个数据结构中:
/**
* Map(namespace, Map(group::serviceName, Service)).
*/
private final Map<String, Map<String, Service>> serviceMap = new ConcurrentHashMap<>();
3. 服务发现:客户端微服务在调用某个服务时,会向注册中心发起服务发现请求。注册中心会根据请求中的服务名称等信息,从注册表中查找对应的服务实例信息,并返回给请求方。
4. 心跳检测:为了确保注册表中的服务实例信息的准确性,注册中心会定期向各个服务实例发送心跳检测请求。服务实例在接收到心跳检测请求后,会返回一个响应,表明它仍然在线。如果注册中心在一段时间内没有收到某个服务实例的响应,就会将其从注册表中移除。Nacos Client 会启动一个定时任务每 5 秒
发送一次心跳,最终是调用 Nacos PUT
/nacos/v1/ns/instance/beat
接口请求完成心跳发送。Nacos Server 会开启一个定时任务来检测注册服务的健康情况,对于超过 15 秒
没收到客户端心跳的实例,会设置为不健康状态
,即 healthy=false
,超过 30 秒
没收到心跳,则会剔除
该实例。Nacos Client 可以通过再次发送心跳恢复。
5. 服务下线:当微服务实例停止运行时,它会向注册中心发送一个下线请求。注册中心在接收到下线请求后,会将该服务实例从注册表中移除。最终是调用 Nacos Server DELETE
/nacos/v1/ns/instance
接口请求来完成下线。
6. 服务变更通知:如果注册表中的服务实例信息发生变化(如新增、下线、IP地址变更等),注册中心会向订阅了该服务的客户端或其他微服务实例发送变更通知。这样,客户端或其他微服务实例就能及时获取到最新的服务实例信息,更新本地服务实例列表。Nacos Server 处理服务变更通知的核心类是 com.alibaba.nacos.naming.push.PushService。
大致了解了nacos的工作原理,接下来深入源码看一下
三、Nacos源码之服务注册
3.1、客户端
3.1.1、入口
首先找到源码之example服务中的NamingExample
反射初始化NacosNamingService,NacosNamingService是NamingService的实现类
3.1.2、服务注册方法
NacosNamingService.registerInstance
- 心跳参数校验
- 最终的服务名格式:serviceName@@groupName
- 如果是临时实例则会开启心跳包
- 服务注册 (参数组装调用API请求注册)
调用API请求注册 NamingProxy.reqApi
- 单机注册中心,失败重试(
默认3次,前提是nacos异常
) - 集群注册中心,
随机挑选一个注册,失败则轮询其他注册中心
- 最终调用callServer方法 (API_URL:
IP:PORT/nacos/v1/ns/instance
)
callServer方法如下:
3.2、服务端
3.2.1、注册表
先了解一下服务端保存实例信息的结构(下面我们简称叫注册表):
难理解的就是为什么一个服务会有多个集群,不应该一个服务就一个集群吗?
这里可以理解为按机房划分集群,不管有多少个集群都属于你该服务的。比如上海机房有一个SH 集群,深圳机房有一个SZ集群,请求的时候你可以按地区请求最近的集群实例,如果整个地区集群都不可用那么可以请求其他地区的集群实例。
对应源码中的注册表(com.alibaba.nacos.naming.core.ServiceManager类中):
点进去Servce类中,可以看到集群定义:
点进集群Cluster类中,是用Set存的实例,分为两种实例,持久实例和临时实例:
对应微服务中配置和nacos配置中的字段信息:
3.2.2、注册接口信息
注册接口:/nacos/v1/ns/instance
请求参数:
名称 | 类型 | 是否必选 | 描述 |
---|---|---|---|
ip | 字符串 | 是 | 服务实例IP |
port | int | 是 | 服务实例port |
namespaceId | 字符串 | 否 | 命名空间ID |
weight | double | 否 | 权重 |
enabled | boolean | 否 | 是否上线 |
healthy | boolean | 否 | 是否健康 |
metadata | 字符串 | 否 | 扩展信息 |
clusterName | 字符串 | 否 | 集群名 |
serviceName | 字符串 | 是 | 服务名 |
groupName | 字符串 | 否 | 分组名 |
ephemeral | boolean | 否 | 是否是否是零时实例 |
3.2.3、注册方法
InstanceController.register
3.2.4、注册流程(以临时实例为例)
ServiceManager.registerInstance
- 创建一个空的service放入注册表,为其
开启一个心跳检测
,并将这个service加入监听列表 - 拿到创建好的service
- 完成实例的注册表更新,并完成nacos集群同步
3.2.5、创建空的service加入注册表
让我们看看是怎么创建空的service的
ServiceManager.createEmptyService:
ServiceManager.putServiceAndInit:
3.2.6、添加实例
ServiceManager.addInstance:
里面最重要的就是consistencyService.put(key, instances) 方法
consistencyService有很多种实现,根据实例的类型来判断具体走哪种实现方式,这里我们以临时实例为例,主要看看DistroConsistencyServiceImpl
DistroConsistencyServiceImpl.put 临时实例的注册方法
3.2.7、临时实例的添加
onPut方法:
- 会将任务放入
Notifier
内部的阻塞队列中,Notifier是个Runnable
(异步执行任务) - 最后会回到
Service.onChange
方法更新实例,内部调用updateIPs
方法,这里面需要注意更新后会触发一个服务变更事件(后面有用)
Service.updateIPs:
3.2.8、临时实例的集群同步
distroProtocol.sync()临时实例集群同步:
- 遍历集群中其他节点
- 定义一个
DistroDelayTask
异步任务放入一个ConcurrentHashMap
中,会有一个ScheduledExecutorService
线程池定时从这个map中取任务执行
线程池的定义在NacosDelayTaskExecuteEngine中:
上诉线程池执行的任务就是**NacosDelayTaskExecuteEngine.processTasks()**如下:
DistroDelayTaskProcessor.process:
任务的执行被放入到process
方法中,并被封装成DistroSyncChangeTask
异步任务,又被塞到一个不知名封装好的地方(是一个阻塞队列,同样有地方取出来执行,我们直接看这个任务的执行)
DistroSyncChangeTask.run
- syncData方法最终会到NamingProxy.syncData方法,执行HTTP请求,同步数据
- 如果失败了,则又会调用NacosDelayTaskExecuteEngine.addTask()方法重新将DistroDelayTask任务放进ConcurrentHashMap中,重复上述的processTasks方法
3.2、总结
- 客户端:启动则获取自身配置信息,发起http请求注册,临时实例同时会开启心跳机制(下面会说),服务端是单机的情况下请求失败会重试三次,服务端是单机的集群的情况下请求失败会轮询请求
- 服务端:
- 本地通过一个Map保存所有服务信息,注册的实质就是往map里面添加信息
- 会先创建空的服务,后更新服务中的实例信息
- 服务创建后会初始化服务,启动心跳检测
- 往服务中添加实例的时候会判断实例是永久实例还是临时实例,不同类型的实例有不同的处理方式
- 注册后同时会发布服务变更事件(后面说,先记着这个事件)
问题一:为什么客户端注册会先开启心跳后发起注册请求?
因为心跳是异步定时执行,就算后续的注册发生某意外注册失败,心跳机制还可以弥补注册(因为心跳也可以注册),如果是先发起注册后开启心跳,有可能注册发生某意外就直接终止了,心跳还没开启
问题二:服务端注册怎么保证线程安全?
服务器注册会先创建一个空的服务,后对该服务填充信息初始化,保存服务的map用ConcurrentHashMap修饰的,所以此过程是线程安全的,后续再对服务内实例更新的时候,采用synchronized对该服务做了加锁操作
问题三:服务端注册怎么保证性能?(临时实例)
前置操作时采用ConcurrentHashMap和synchronized锁服务,前者是最优的线程安全map,后者锁的是“服务”颗粒度一定程度的保证了性能,后续均采用了异步更新,如本地注册表更新采用了阻塞队列异步执行,临时实例集群同步过程中同样采用了阻塞队列异步执行机制,因为为阻塞的异步执行,所以保值性能的同时也保证了资源不会占用异常
四、Nacos源码之心跳机制
临时实例:实例发起心跳请求,服务端处理请求,并需要进行心跳检测
永久实例:服务端主动发起健康检测
4.1、临时实例
4.1.1、客户端
临时实例在注册的时候会开启心跳包,这个在前面有说(默认5s心跳)
1.开启入口
NacosNamingService.registerInstance():
buildBeatInfo
就是心跳信息的封装,我们主要看addBeatInfo
方法
2.心跳开启
BeatReactor.addBeatInfo():
把心跳任务BeatTask
丢到了延迟线程池里面执行,所以主要执行逻辑在BeatTask
中
3.心跳执行逻辑
BeatTask.run()
- 发送HTTP心跳请求 URL地址为:
/nacos/v1/ns/instance/beat
- 如果
当前实例在注册中心未找到就重新注册
- 不管结果如何添加心跳任务,继续定时发起心跳(继续将任务丢到线程池里面执行)
心跳请求如下,URL地址为:/nacos/v1/ns/instance/beat
,轻量级心跳包只需要请求头,普通心跳包需要带上请求体
4.1.2、服务端
1.心跳请求的处理
从上面请求URL:/nacos/v1/ns/instance/beat
,我们很容易能找到处理请求逻辑:
InstanceControllerregister.beat()
我这里省略了前面的校验逻辑,直接看主体逻辑:
- 从注册表中获取实例信息,若无则重新注册
- 从注册表中获取服务,若无则直接异常(实例都注册完了,服务还找不到是不合理的)
- 开启异步任务将临时实例状态 置为健康状态,然后返回
2.心跳检测处理
实例如果宕机或者其他什么请求无法发送心跳,那么服务端自然也要对这个实例进行处理,就在服务初始化的时候,会开启会实例的心跳检测任务,上面也有提到过
ServiceManager.putServiceAndInit()
Service.init()
这个内部呢,就会有个ClientBeatCheckTask
任务被放入了线程池,5s
执行一次
ClientBeatCheckTask.run()
而ClientBeatCheckTask任务主要做了两件事:
- 找到心跳超时的实例,改变其健康状态,并发布
serviceChange
事件(后面说),还有实例心跳超时事件 - 找到满足删除条件的实例,从注册表中删除该实例信息(HTTP请求调用API,异步删除)
- 默认
15s超时
,30s剔除
4.2、永久实例
4.2.1、入口
入口和上面临时实例入口差不多,但永久实例是在集群初始化的时候,而临时实例是在服务初始化的时候
Cluster.init如下:
- 检测任务就是HealthCheckTask,这是个异步任务
- 延迟任务第一次(2000ms+5000ms以内随机数)执行,后续在1000ms-5000ms内浮动
HealthCheckTask任务如下:
- process方法有多种实现意味着有多种检测方案(这里以TCP为例)
- 不管结果如何继续延迟执行,约等于是个定时器(因为每次延迟时间不同)
- HealthCheckTask实例化的时候同时初始化了TcpSuperSenseProcessor,该方法是一个Runnable,会执行TcpSuperSenseProcessor.run方法(以TCP为例)
可以看到主动检测有多种方法:
4.2.2、获取所有永久实例加入阻塞队列
TcpSuperSenseProcessor .process()
这里会遍历所有永久实例并将实例封装成Beat加入到阻塞队列中
4.2.3、从队列中获取实例并封装
上面把永久实例信息放到了阻塞队列中,那么就肯定有方法去取,那是哪里呢?还记得前面说过TcpSuperSenseProcessor本身也是个异步任务吗?
TcpSuperSenseProcessor.run方法如下:
- 会先从阻塞队列中取出实例信息并封装,然后尝试与实例建立socket连接
- 最后判断连接状态,连接上了就进去实例健康处理并断开连接
- 同时会有一个异步的延迟任务,去检测这段时间内是否连接上过,这段时间内没连接上过说明连接超时了
- 可以看到这个run方法是个死循环
TcpSuperSenseProcessor.processTask方法如下:
- 从阻塞队列中取实例信息,并封装成TaskProcessor异步任务
- 批量提交任务,就是执行TaskProcessor异步任务
4.2.4、与实例尝试建立连接
TaskProcessor.call()
- 主要就是尝试建立socket连接
- 开启一个超时检测的延迟任务TimeOutTask(500ms)
4.2.5、超时判断
TimeOutTask.run()
因为已经延迟执行了,就判断这段时间内是否连接上过,没有就代表超时,超时会进入finishCheck方法
4.2.6、正常处理
前面都执行完了,就到TcpSuperSenseProcessor.run里面最后的PostProcessor异步任务了,连接成功会进入finishCheck方法
PostProcessor.run()
4.2.7、最终判断
Beat.finishCheck
连接成功或超时连接都会进到这里处理,不管如何都会发布服务变更事件,只会改变实例状态不会剔除实例
4.3、总结
- 临时实例:
- 采用
客户端心跳检测
模式,心跳检测周期5秒
心跳间隔超过15秒(默认)则标记为不健康
心跳间隔超过30秒(默认)则从服务列表删除
- 采用
- 永久实例:
- 采用
服务端主动健康检测
方式 - 周期为
2000 + 5000毫秒
内的随机数 - 检测异常只会标记为不健康,不会删除
- 采用
五、Nacos源码之服务发现
实例是如何得知其他实例的信息呢?毕竟需要远程调用嘛
两种方式:1.客户端主动获取(定时更新)、2.服务端主动推送(长连接推送变更信息)
5.1、客户端主动获取
5.1.1、客户端
1.入口
NacosNamingService.getAllInstances
该方法就是获取所需的服务信息
2.第一次获取
HostReactor.getServiceInfo
- 先是故障转移机制判断是否去本地文件中读取信息,读到则返回
- 再去本地服务列表读取信息(本地缓存),没读到则创建一个空的服务,然后立刻去nacos中读取更新
- 读到了就返回,同时开启定时更新,定时向服务端同步信息 (正常1s,异常最多60s一次)
HostReactor.updateServiceNow
HostReactor.updateService
属性的serverProxy,这里面就是接口调用请求了
3.定时延迟任务
HostReactor.scheduleUpdateIfAbsent
这里全先判断定时任务是否已经在异步任务列表中了,不在才会添加一个UpdateTask任务延迟执行
UpdateTask.run
UpdateTask类就是一个异步执行类,里面会调用updateService方法更新服务信息,同时结束又会开启延迟,延迟的时间跟请求失败的次数有关,最多60s,正常是1s一次
无论是updateService方法、refreshOnly方法,还是刚开始的直接去nacos拉取信息的方法(getServiceInfoDirectlyFromServer)都会调用serverProxy.queryList方法,这个方法就是HTTP请求获取信息:
获取服务信息列表URL:/nacos/v1/ns/instance/list
NamingProxy.queryList
5.1.2、客户端服务端
InstanceController.list()
服务端这边处理请求就比较简单了,除去参数获取以及相关校验就剩服务列表的获取了
InstanceController.doSrvIpxt如下:
这里记住有个PushService
5.2、服务端主动推送
既然是主动推送那么就需要两个条件:1.建立长连接2.触发推送的事件
5.2.1、服务端
上面
InstanceController.doSrvIpxt
中的pushService.addClient
就是把客户端UDP、IP等信息封装成PushClient对象
存储在PushService类中
,方便以后服务变更后推送消息
PushService类实现ApplicationListener接口,监听ServiceChangeEvent(服务变更事件)
ServiceChangeEvent事件处理就在当前类下:
事件触发则是PushService.serviceChanged方法,这个方法之前我们就见过,在服务注册里面,心跳里面也有,服务变更就会调用这个方法,触发事件让服务端主动推送服务变更信息
5.2.2、客户端
客户端是在PushReceiver类里面,这个类是个Runnable会在HostReactor中被实例化
PushReceiver.run()
收到服务端的信息就会交给HostReactor.processServiceJson处理
HostReactor.processServiceJson就会更新本地缓存的信息,上述客户端主动拉取的时候也会调用这个方法更新
HostReactor.processServiceJson
中间一大段省略了哈,最重要的就是那几步:
- 更新本地缓存
- 发布实例变更事件
- 写入磁盘(故障转移机制)
5.3、总结
服务的发现有两种方式
客户端主动获取:
- 会先读取缓存,缓存内读取不到则会去服务端获取,同时开启一个定时任务定时更新
- 定时任务1s一次,异常时会延长时间最长60s
- 拉取URL:
/nacos/v1/ns/instance/list
服务端主动推送:
- 服务端和客户端在启动后会建立一个长连接
- 服务端服务变更后会发布服务变更事件ServiceChangeEvent,会通过长连接将变更后的信息发送给客户端
- 客户端更新的方式是hostReactor.processServiceJson方法,会写入缓存、发布实例变更事件、写入磁盘
创作不易,不妨点赞、收藏、关注支持一下,各位的支持就是我创作的最大动力❤️