当前位置:网站首页>聊聊 Nacos
聊聊 Nacos
2022-08-02 23:54:00 【Young丶】
Nacos架构
- Provider APP:服务提供者
- Consumer APP:服务消费者
- Name Server:通过VIP(Virtual IP)或DNS的方式实现Nacos高可用集群的服务路由
- Nacos Server:Nacos服务提供者,里面包含的Open API是功能访问入口,Conig Service、Naming Service 是Nacos提供的配置服务、命名服务模块。Consitency Protocol是一致性协议,用来实现Nacos集群节点的数据同步,这里使用的是Raft算法(Etcd、Redis哨兵选举)
- Nacos Console:控制台
一、注册中心的原理
- 服务实例在启动时注册到服务注册表,并在关闭时注销
- 服务消费者查询服务注册表,获得可用实例
- 服务注册中心需要调用服务实例的健康检查API来验证它是否能够处理请求
二、SpringCloud完成注册的时机
在Spring-Cloud-Common包中有一个类org.springframework.cloud. client.serviceregistry .ServiceRegistry
,它是Spring Cloud提供的服务注册的标准。集成到Spring Cloud中实现服务注册的组件,都会实现该接口。
该接口有一个实现类是NacoServiceRegistry
。
SpringCloud集成Nacos的实现过程:
在spring-clou-commons包的META-INF/spring.factories中包含自动装配的配置信息如下:
其中AutoServiceRegistrationAutoConfiguration就是服务注册相关的配置类:
在AutoServiceRegistrationAutoConfiguration配置类中,可以看到注入了一个AutoServiceRegistration实例,该类的关系图如下所示。
可以看出, AbstractAutoServiceRegistration抽象类实现了该接口,并且最重要的是NacosAutoServiceRegistration继承了AbstractAutoServiceRegistration。
看到EventListener我们就应该知道,Nacos是通过Spring的事件机制继承到SpringCloud中去的。
AbstractAutoServiceRegistration实现了onApplicationEvent抽象方法,并且监听WebServerInitializedEvent事件(当Webserver初始化完成之后) , 调用this.bind ( event )方法。
最终会调用NacosServiceREgistry.register()方法进行服务注册。
三、NacosServiceRegistry的实现
在NacosServiceRegistry.registry方法中,调用了Nacos Client SDK中的namingService.registerInstance完成服务的注册。
跟踪NacosNamingService的registerInstance()方法:
通过
beatReactor.addBeatInfo()
创建心跳信息实现健康检测, Nacos Server必须要确保注册的服务实例是健康的,而心跳检测就是服务健康检测的手段。serverProxy.registerService()
实现服务注册
心跳机制:
从上述代码看,所谓心跳机制就是客户端通过schedule定时向服务端发送一个数据包 ,然后启动-个线程不断检测服务端的回应,如果在设定时间内没有收到服务端的回应,则认为服务器出现了故障。Nacos服务端会根据客户端的心跳包不断更新服务的状态。
注册原理:
Nacos提供了SDK和Open API两种形式来实现服务注册。
Open API:
curl -X POST 'http://127.0.0.1:8848/nacos/v1/ns/instance?serviceName=nacos.naming.serviceName&ip=20.18.7.11&port=8080'
SDK:
这两种形式本质都一样,底层都是基于HTTP协议完成请求的。所以注册服务就是发送一个HTTP请求:
对于nacos服务端,对外提供的服务接口请求地址为nacos/v1/ns/instance
,实现代码咋nacos-naming模块下的InstanceController类中:
- 从请求参数汇总获得serviceName(服务名)和namespaceId(命名空间Id)
- 调用registerInstance注册实例
- 创建一个控服务(在Nacos控制台“服务列表”中展示的服务信息),实际上是初始化一个serviceMap,它是一个ConcurrentHashMap集合
- getService,从serviceMap中根据namespaceId和serviceName得到一个服务对象
- 调用addInstance添加服务实例
- 根据namespaceId、serviceName从缓存中获取Service实例
- 如果Service实例为空,则创建并保存到缓存中
- 通过putService()方法将服务缓存到内存
- service.init()建立心跳机制
- consistencyService.listen实现数据一致性监听
service.init ( ) 方法的如下图所示,它主要通过定时任务不断检测当前服务下所有实例最后发送心跳包的时间。如果超时,则设置healthy为false表示服务不健康,并且发送服务变更事件。在这里请大家思考一一个问题,服务实例的最后心跳包更新时间是谁来触发的?实际上前面有讲到, Nacos客户端注册服务的同时也建立了心跳机制。
putService方法,它的功能是将Service保存到serviceMap中:
继续调用addInstance方法把当前注册的服务实例保存到Service中:
总结:
- Nacos客户端通过Open API的形式发送服务注册请求
- Nacos服务端收到请求后,做以下三件事:
- 构建一个Service对象保存到ConcurrentHashMap集合中
- 使用定时任务对当前服务下的所有实例建立心跳检测机制
- 基于数据一致性协议服务数据进行同步
服务提供者地址查询
Open API:
SDK:
InstanceController中的list方法:
- 解析请求参数
- 通过doSrvIPXT返回服务列表数据
- 根据namespaceId、serviceName获得Service实例
- 从Service实例中基于srvIPs得到所有服务提供者实例
- 遍历组装JSON字符串并返回
Nacos服务地址动态感知原理
可以通过subscribe方法来实现监听,其中serviceName表示服务名、EventListener表示监听到的事件:
具体调用方式如下:
或者调用selectInstance方法,如果将subscribe属性设置为true,会自动注册监听:
Nacos客户端中有一个HostReactor类,它的功能是实现服务的动态更新,基本原理是:
客户端发起时间订阅后,在HostReactor中有一个UpdateTask线程,每10s发送一次Pull请求,获得服务端最新的地址列表
对于服务端,它和服务提供者的实例之间维持了心跳检测,一旦服务提供者出现异常,则会发送一个Push消息给Nacos客户端,也就是服务端消费者
服务消费者收到请求之后,使用HostReactor中提供的processServiceJSON解析消息,并更新本地服务地址列表
四、集群环境:分布式的前提
如果是 Nacos 集群环境,客户端会随机选择一个 Nacos 节点发起注册。
搭建好一套Nacos 集群环境
为了讲解客户端是如何注册到 Nacos 集群环境的底层原理,我在本地搭建了一个 Nacos 集群环境,有 3 个 Nacos 服务,它们的 IP 相同,端口号不同。
192.168.10.197:8848
192.168.10.197:8858
192.168.10.197:8868
然后服务 A 和服务 B 都是配置了 Nacos 集群的 IP 和 端口号的,配置如下所示
spring.cloud.nacos.discovery.server-addr
=192.168.10.197:8848,192.168.10.197:8858,192.168.10.197:8868
整体的结构如下图所示,服务 A 和 服务 B 都往 Nacos 集群进行注册。
但是里面有一个问题:服务 A 注册时,是向所有 Nacos 节点发起注册呢?还是只向其中一个节点发起注册?如果只向一个节点注册,要向哪个节点注册呢?
答案:在 Client 发起注册之前,会有一个后台线程随机拿到 Nacos 集群服务列表中的一个地址。
Nacos 为什么会这样设计?
- 这其实就是一个负载均衡的思想在里面,每个节点都均匀的分摊请求。
- 保证高可用,当某个节点宕机后,重新拿到其他的 Nacos 节点来建立连接。
接下来我们看下服务 A 是怎么随机拿到一个 Nacos 节点的。
随机节点
我们来看下客户端是如何随机选择一个节点的,流程图如下:
那么如何找到这些代码逻辑呢?思路是怎么样的?
我们之前讲过,RpcClient 会发起 request 请求,用的是和 Nacos 建立 currentConnection
连接来发起调用,代码如下:
// 发起调用
response = this.currentConnection.request(request, timeoutMills);
这个 currentConnection
是客户端和 Nacos 集群中的某个节点建立的连接,我们找下它在哪里赋值的。代码如下:
// 拿到 Nacos 节点信息
serverInfo = recommendServer.get() == null ? nextRpcServer() : recommendServer.get();
// 连接 Nacos 节点
connectToServer = connectToServer(serverInfo);
// 赋值 currentConnection
this.currentConnection = connectToServer;
而连接的信息是通过参数 serverInfo 传进去的,所以我们再看下 serverInfo 在哪里赋值的。
这个 nextRpcServer() 方法里面会拿到一个随机的 Nacos 地址:
// 一个 int 随机数,范围 [0 ~ Nacos 个数)
currentIndex.set(new Random().nextInt(serverList.size()));
// index 自增 1
int index = currentIndex.incrementAndGet() % getServerList().size();
// 返回 Nacos 地址
return getServerList().get(index);
小结:客户端生成一个随机数,然后通过这个随机数从 Nacos 服务列表中拿到一个 Nacos 服务地址返回给客户端,然后客户端通过这个地址和 Nacos 服务建立连接。Nacos 服务列表中的节点都是平等的,随机拿到的任何一个节点都是可以用来发起调用的。
五、路由转发
5.1 发起和转发请求的流程
为了演示发起注册的流程,我在这里模拟了一个注册请求。
用的是 curl 命令,对 Nacos 节点(127.0.0.1:8848)发起注册请求:
curl -X POST 'http://127.0.0.1:8848/nacos/v1/ns/instance?serviceName=nacos.naming.serviceName&ip=20.18.7.11&port=8080'
请求 URL:/nacos/v1/ns/instance
请求参数:
- serviceName=nacos.naming.serviceName
- ip=20.18.7.11
- port=8080’
之前我们讲到,Nacos 的有多个节点可以分别处理请求,当节点发现这个请求不是属于自己的,就会进行转发。
如下图所示:
服务 A 随机选择一个 Nacos 节点(图中为 Nacos1)发起注册请求,请求参数中包含了实例信息,Nacos 1 根据实例信息 hash + 取模拿到正确的节点,如果不属于自己,则将请求转发给其他节点(图中为 Nacos2)
步骤如下:
- ① Nacos 节点从客户端发起的 request 中拿到客户端的实例信息生成 distroTag,如 IP + port 或 service name。
- ② Nacos 根据 distroTag 生成 hash 值。
- ③ 用 hash 值对 Nacos 节点数进行
取余
,拿到余数,比如 0、1、2、3。 - ④ 根据余数从 Nacos 节点列表中拿到指定的节点地址。
5.2 路由转发源码分析
入口文件是 DistroFilter.java:
naming/src/main/java/com/alibaba/nacos/naming/web/DistroFilter.java
请求会先到 DistroFilter 类的 doFilter() 方法,拿到正确的节点地址后,将请求转发出去。
获取需要转发节点地址的代码如下:
// 找到 Nacos 集群中的目标节点
final String targetServer = distroMapper.mapSrv(distroTag);
// mapSrv 方法会先 hash,然后再取模,responsibleTag的值类似这样:"20.18.7.11:8080"
int index = distroHash(responsibleTag) % servers.size();
// distroHash 方法里面会对 客户端的 ip+port 字符串或者服务名字符串 进行 hash
Math.abs(responsibleTag.hashCode() % Integer.MAX_VALUE);
不论是自己处理注册请求还是转发给其他节点来处理,都会把实例信息存储起来,那么是如何进行存储的?
六、处理请求
Nacos 目前有两个版本,v1 和 v2,如果是 v1,则是 instanceController 来处理注册请求,否则用 instanceControllerV2。本篇我们只讲解 v1 版本是怎么处理请求的。
先上流程图:
添加实例信息的流程
测试用的发起注册的命令:
curl -X POST 'http://127.0.0.1:8858/nacos/v1/ns/instance?serviceName=nacos.naming.serviceName&ip=20.18.7.11&port=8080'
核心代码就是这个:
服务端注册实例的方法
首先有一个 synchronized 锁,然后执行 put 操作将临时的实例信息存放起来,所以重点看下 这个 consistencyService.put() 方法做了什么事情。
先看下源码:
onPut(key, value);
// 开启 1s 的延迟任务,将数据同步给其他 Nacos 节点
distroProtocol.sync(new DistroKey(key,KeyBuilder.INSTANCE_LIST_KEY_PREFIX),DataOperation.CHANGE,
DistroConfig.getInstance().getSyncDelayMillis());
这里面做了三件事情:
- ① 将实例信息存放到内存缓存 ConcurrentHashMap 里面。
- ② 添加一个任务到 BlockingQueue 队列里面,这个任务就是将最新的实例列表通过 UDP 的方式推送给所有客户端(服务实例),这样客户端就拿到了最新的服务实例列表。没想到吧,计算机网络的知识终于用上了~
- ③ 开启 1s 的延迟任务,将数据通过给其他 Nacos 节点。
注意:针对第二点和第三点,属于 Distro 一致性协议的一部分,里面的内容还比较多,我们放到下一讲专门来讲。
一条注册请求的核心流程:
参考
blog.csdn.net/cold___play/article/details/108032204
边栏推荐
- nmap: Bad CPU type in executable
- Day117.尚医通:生成挂号订单模块
- D experimental new anomaly
- 有奖提问|《新程序员》专访“Apache之父”Brian Behlendorf
- 十年架构五年生活-03作为技术组长的困扰
- 新公链时代的跨链安全性解决方案
- Carefully organize 16 MySQL usage specifications to reduce problems by 80% and recommend sharing with the team
- Rasa 3.x study series - Rasa - Issues 4792 socket debug logs clog up debug feed study notes
- js基础知识整理之 —— 字符串
- vue3的keepAlive缓存组件
猜你喜欢
NLP commonly used Backbone model cheat sheet (1)
Day117. Shangyitong: Generate registered order module
js基础知识整理之 —— 判断语句和三元运算符
科捷智能冲刺科创板:年营收12.8亿 顺丰与日日顺是股东
Test | ali internship 90 days in life: from the perspective of interns, talk about personal growth
年近30 ,4月无情被辞,想给划水的兄弟提个醒...
Day117.尚医通:生成挂号订单模块
我为什么又能面试一次就拿到offer
[NCTF2019]SQLi-1||SQL注入
谷歌 Chrome 浏览器 104 正式版发布:加快网页加载,蓝牙 API 改进
随机推荐
十年架构五年生活-03作为技术组长的困扰
vue3的keepAlive缓存组件
2022山东国际青少年眼睛健康产业展会,视力健康展,眼视光展
智能合约安全-可重入攻击(SW107-Reentrancy)
DB2数据库-获取表结构异常:[jcc][t4][1065][12306][4.26.14]CharConvertionException ERRORCODE=-4220,SQLSTATE=null
MySQL的多表查询(1)
Auto.js 特殊定位控件方法 不能在ui线程执行阻塞操作,请使用setTimeout代替
Database auditing - an essential part of network security
Mock工具之Moco使用教程
js基础知识整理之 —— Math
解决错误:Optional int parameter ‘pageSize‘ is present but cannot be translated into a null value due to
令人心动的AI综述(1)
厌倦了安装数据库?改用 Docker
Jmeter二次开发实现rsa加密
2022 China Eye Expo, Shandong Eye Health Exhibition, Vision Correction Instrument Exhibition, Eye Care Products Exhibition
定了!8月起,网易将为本号粉丝提供数据分析培训,费用全免!
Moco of Mock tools use tutorial
线性DP
@GetMapping、@PostMapping、@PutMapping、@DeleteMapping的区别
IDEA多线程调试