Nacos源码解析
-
jar包指定配置文件
–spring.config.location=/ect/server/application.yml
优先级, --spring.config.location大于jar包中的配置
-
spring boot 的项目
-
通信都是使用Rest API通信
-
注册的单位是实例, 最小单元是实例
-
单集群的nacos 的功能
- 服务发现
- 心跳检测
- 服务注册
-
实例:
isemphere: true /false- 临时实例 :AP架构(注册中心)
- 永久实例 :CP架构(配置中心)
-
服务注册的请求方式:
- 1.x版本使用http / https请求
- 2.x版本及以后: grpc请求
-
AutoConfigreAfter ( {xxxx.class ,xxx.class}) 在在这写配置加载之后再加载 顺序
-
实例注册请求发送时机:
Spring 监听器ApplicationListener 监听WenInit 初始化完成后 发送请求;
-
spring boot web 容器
- tomcat : 默认
- undertow
- jetty
-
nacos注册表结构
- Nacos 注册表结构的核心是一个双层
Map结构,具体如下:-
最外层 Map:
- Key:
nameSpace(命名空间) - Value: 一个
Map
- Key:
-
第二层 Map: ConcurrentHashMap
- Key:
ServiceName(组::服务名称) - Value:
Service对象,该对象包含了一些关键属性。
- Key:
-
Service 对象的属性:
clusterMap: 一个Map,用于管理不同的集群- Key: 集群名称 (
clusterName) - Value: 一个
Set,包含集群内的Instance对象集合
- Key: 集群名称 (
-
Instance 对象:
- 表示具体的服务实例,包含实例的 IP 地址、端口、权重、健康状态等信息。
-
1 | +----------------------------------------------------------+ |
一 Nacos 的核心功能
通信机制
-
- 在
1.x版本中, Nacos 的服务端和客户端之间的通信是通过Restful风格的请求通信的 - 在
2.x版本之后, 改用了grpc的方式
- 在
服务注册
naocs 服务客户端,在启动时, 会根据配置文件中的nacos 的服务端配置信息, 向服务端发送一个请求, 将客户端的元数据发送给服务端, 比如: ip \ port \ 服务名 等信息.
服务端接收到数据后, 会将客户端的元数据存储在一个双层map中
服务注册的时间: 在客户端启动时, nacos客户端使用了spring的监听器ApplicaitonListener,监听WebServerInitializedEvent 是事件, 当web服务器启动后, 发送一个请求给服务端注册服务.
发送的请求地址 : Http: nacosip:nacos端口号/nacos/v1/ns/instance 发送一个Post请求, 带上请求头和请求参数 : 元数据 \ Ip \ 端口 \ 服务名 \ namspace 等
服务心跳检测
在客服端服务注册成功之后, Nacos 的客户端会维护一个定时任务 , 来持续的发送请求给服务端 ,告诉服务端自己还活着 ,默认是每5秒发送一次, 防止服务在注册中心被剔除
服务的健康检查
Nacos的服务端也会维护一个定时任务, 通过判断当前时间和实例最后一次的检测时间, 如果超过了15秒, 就将该实例的健康状态改为false ,默认是true, 如果超过了30 秒, 则直接将该服务从nacos中剔除
服务发现
服务端在注册到nacos后, 在调用服务注册中心的服务时, 会发送请求给服务端, 拉取服务端中所注册的服务清单,并且缓存在客户端本地,同时会开启一个定时任务,定时从服务端拉取最新服务清单
所以在第一次调用时, 速度会相对慢有点 , 后续主要通过查找缓存中的服务列表来调用, 靠定时任务来定时更新缓存中的服务列表
Nacos服务注册表结构
-
双Map结构:
HashMap<String,CurrentHashMap<String,Service>>-
第一层是一个HashMap ,Key 是
nameSpace, Value 是一个CurrentHashMap<Service,> -
第二层的CurrentHashMap的 Key 是
GroupName::ServiceName, Value 是 Service 对象Group本身没有单独的一个层级, 通常是直接携带在ServiceName后面,在查找时就可以判断属于哪个组了
-
Service对象: Service 对象中又有一个
custerMap集群map -
custerMap的 Key 是custerName , Value 是一个Cluster 对象
-
在Cluster 对象中, 存在一个set集合, 里面存放的元素就是实例Instance对象 , Instance 对象中的属性存放着客户端传的元数据
-
1 | +----------------------------------------------------------+ |
二 OpenFeign + Nacos 调用流程
- Nacos客户端在启动时,根据服务注册机制将服务注册到Nacos注册中心.
- nacos客户端会通过心跳检测机制定时在Nacos的服务端更新当前实例的健康状态, 防止被Nacos服务端剔除, Nacos服务端则根据当前时间和实例最后一次的报告时间做对比, 如果超过了15秒,则将该实例的健康状态标记为false, 如果超过了30 秒, 则直接将该服务剔除掉
- nacos客户端通过服务发现机制, 拉取Nacos服务端中的服务列表, 然后根据Ribbon的负载均衡机制, 选择一个服务的ip和端口号进调用
三 Nacos服务端具体处理注册流程
1. 功能 :
Nacos在进行服务注册时, 主要的工作就是将实例添加到注册表中对应服务的对应Cluster下的set中, 里面存放着当前服务下所有集群下的所有服务
2. **主要实现: **
思想: Nacos在进行处理时 , 采用同步加异步思想,

2.1 在同步中做的事
-
解析客户端注册服务时发送的请求中的数据
-
如果该服务是第一次注册, 首先会先创建一个空的Service对象
1
2createEmptyService(namespaceId, serviceName, instance.isEphemeral());
Service service = getService(namespaceId, serviceName); -
构建一个双层Map结构, 将Service放进去
-
初始化该服务
在初始化该服务时,
-
使用定时任务的线程池开启了一个定时任务, 对实例做心跳检测,
心跳检测: 通过判断实例的最后一次的报告时间 和当前时间的差值, 如果大于15秒 则将该实例的健康状态改为false. 如果不大于 ,则不变为true ,
如果大于 30 秒, 则将该实例从服务中移除 , 如果不大于就不移除
-
-
做完这些后, 将该实例对象 存放在一个临时的Map中.
-
最后再将实例对象从临时map取出, 添加到阻塞队列中, task.add(Pair.with)
该阻塞队列为LinedBlockingQueue阻塞队列, 默认大小为1024 * 1024
2.2 异步线程做的事(notifiyer)
- 异步线程notiyer 从阻塞队列出Pair对象, 使用
Task.take()获取, 并将该实例存放到Cluster对象中 , - 更新Cluster对象中的数据的操作
- 进行各种实例的CRUD比对
- 得到最终要注册的实例对象
- 最后将该实例存放在在注册表中的该service下的Cluster 对象中set集合中
这样, 实例就相当于存放在了注册表中. 并且这个线程是在服务启动时一种存在的, Nacos使用的@PostConstrct注解和@Service 来让该线程在服务启动时就运行 .
对于这种需要一直执行的任务, 既需要让这个任务一直执行下去, 又要防止 这个任务大量的消耗CPU资源
我在Nacos中就学到了一种很好的解决办法, 它首先通过在该任务中加入while (true) 循环, 来确保该线程可以一直执行下去, 并且,为了防止该线程一直循环消耗资源, 所以Nacos在这里它使用的是一个阻塞队列, 当该任务 获取不实例对象时,会进入阻塞状态, 只有当新的实例对象被添加进来, 该线程才会继续执行. 这样就很好的解决了消耗CPU资源的问题. 然后,while 循环其实并不能百分百保证该线程能一直循环下去, 当该任务在执行的过程中发生了异常, 该任务也会停止 , 因此, 在这个任务的while循环中,还通过try-catch包围, 将异常直接处理, 这样, 就算发生了异常, 该任务也不会停止了.
注册表实列的多线程读取的问题和解决办法
❔问题
在对注册表中的实例的操作, 在Nacos中主要就两种,存和取, 这样就有一个问题, 如果有多个线程对该set集合进行操作, 一个线程读数据, 另外一个线程来存数据,
- 当写线程还没有写进去, 读线程就读完了, 就会造成
数据不一致 - 在存的过程中,如果其他线程同时取, 那么读取到的数据就可能是错误的一个数据,
读写冲突问题
💡解决办法
对于这种多线程同时存储的问题, 一般的解决办法就是加锁 , 但是加锁的弊端太大了, 虽然加锁能很好的解决读写冲突问题 和 数据不一致的问题. 但是,在Nacos源码中, 它并没有去使用加锁, 而是采用了写时复制的解决办法. 因为在Nacos的官网中也介绍了,Nacos 的TPS能达到13000 ,如果使用锁, 那么对于并发的性能就会很差, TPS很难达到13000, 采用写时复制, 虽然还是存在数据一致性问题, 但是确保了最终一致性, 而且还拥有很好的并发性能.
写时复制:
原理:
就是在一个容器中存在两种操作, 写和读时, 当只有读时 , 所有的读线程都是操作的同一个容器,
但是,如果是写操作, 它不会去操作原容器, 而是先将原容器中复制一份, 操作那个复制出的新容器, 当写操作完成后, 再将旧的容器替换成新的容器.
这样,就能很好的解决并发读写问题
但是:
虽然写时复制虽然读与读, 读与写之间没有冲突, 但是写于写之间还是会存在并发的冲突, 如果存在并发的环境, 还是得通过加锁来解决. 不过,在Nacos的服务注册中并不存在并发的写, 因为该任务只有一个线程, 并且是通过拿取阻塞队列中的数据来一个个写的.谁先存谁就先写
❓阻塞队列对堆积吗?
一般情况下不会堆积, Nacos本身处理这个逻辑就很快, 但是也可能发生人为的攻击 ,
程序员通过编写脚本, 一次性发送大量的注册请求, 除此之外,不会出现堆积问题
