Nacos注册中心核心原理

不知你是否跟我一样,在使用Nacos时有以下几点疑问:

临时实例和永久实例是什么?有什么区别?
服务实例是如何注册到服务端的?
服务实例和服务端之间是如何保活的?
服务订阅是如何实现的?
集群间数据是如何同步的?CP还是AP?
Nacos的数据模型是什么样的?

本文就通过探讨上述问题来探秘Nacos服务注册中心核心的底层实现原理。

虽然Nacos最新版本已经到了2.x版本,但是为了照顾那些还在用1.x版本的同学,所以本文我会同时去讲1.x版本和2.x版本的实现

临时实例和永久实例

临时实例和永久实例在Nacos中是一个非常非常重要的概念

之所以说它重要,主要是因为我在读源码的时候发现,临时实例和永久实例在底层的许多实现机制是完全不同的

临时实例
临时实例在注册到注册中心之后仅仅只保存在服务端内部一个缓存中,不会持久化到磁盘
这个服务端内部的缓存在注册中心届一般被称为服务注册表
当服务实例出现异常或者下线之后,就会把这个服务实例从服务注册表中剔除

永久实例
永久服务实例不仅仅会存在服务注册表中,同时也会被持久化到磁盘文件中
当服务实例出现异常或者下线,Nacos只会将服务实例的健康状态设置为不健康,并不会对将其从服务注册表中剔除
所以这个服务实例的信息你还是可以从注册中心看到,只不过处于不健康状态
这是就是两者最最最基本的区别

这里你可能会有一个疑问?为什么Nacos要将服务实例分为临时实例和永久实例?

主要还是因为应用场景不同

临时实例就比较适合于业务服务,服务下线之后可以不需要在注册中心中查看到

永久实例就比较适合需要运维的服务,这种服务几乎是永久存在的,比如说MySQL、Redis等等,MySQL、Redis等服务实例可以通过SDK手动注册

对于这些服务,我们需要一直看到服务实例的状态,即使出现异常,也需要能够查看时实的状态

所以从这可以看出Nacos跟你印象中的注册中心不太一样,他不仅仅可以注册平时业务中的实例,还可以注册像MySQL、Redis这个服务实例的信息到注册中心
注意:在SpringCloud环境底下,一般其实都是业务服务,所以默认注册服务实例都是临时实例
Nacos注册中心核心原理_第1张图片
这里还有一个小细节

在1.x版本中,一个服务中可以既有临时实例也有永久实例,服务实例是永久还是临时是由服务实例本身决定的

但是2.x版本中,一个服务中的所有实例要么都是临时的要么都是永久的,是由服务决定的,而不是具体的服务实例

所以在2.x可以说是临时服务和永久服务
Nacos注册中心核心原理_第2张图片
为什么2.x把临时还是永久的属性由实例本身决定改成了由服务决定?

其实很简单,你想想,假设对一个MySQL服务来说,它的每个服务实例肯定都是永久的,不会出现一些是永久的,一些是临时的情况吧

所以临时还是永久的属性由服务本身决定其实就更加合理了

服务注册

作为一个服务注册中心,服务注册肯定是一个非常重要的功能

所谓的服务注册,就是通过注册中心提供的客户端SDK(或者是控制台)将服务本身的一些元信息,比如ip、端口等信息发送到注册中心服务端

服务端在接收到服务之后,会将服务的信息保存到前面提到的服务注册表中

1、1.x版本的实现
在Nacos在1.x版本的时候,服务注册是通过Http接口实现的

Nacos注册中心核心原理_第3张图片
代码如下
Nacos注册中心核心原理_第4张图片
整个逻辑比较简单,因为Nacos服务端本身就是用SpringBoot写的

但是在2.x版本的实现就比较复杂了
2、2.x版本的实现
2.1、通信协议的改变
2.x版本相比于1.x版本最主要的升级就是客户端和服务端通信协议的改变,由1.x版本的Http改成了2.x版本gRPC

gRPC是谷歌公司开发的一个高性能、开源和通用的RPC框架,Java版本的实现底层也是基于Netty来的

之所以改成了gRPC,主要是因为Http请求会频繁创建和销毁连接,白白浪费资源

所以在2.x版本之后,为了提升性能,就将通信协议改成了gRPC

根据官网显示,整体的效果还是很明显,相比于1.x版本,注册性能总体提升至少2倍

虽然通信方式改成了gRPC,但是2.x版本服务端依然保留了Http注册的接口,所以用1.x的Nacos SDK依然可以注册到2.x版本的服务端

2.2、具体的实现
Nacos客户端在启动的时候,会通过gRPC跟服务端建立长连接
在这里插入图片描述
这个连接会一直存在,之后客户端与服务端所有的通信都是基于这个长连接来的

当客户端发起注册的时候,就会通过这个长连接,将服务实例的信息发送给服务端

服务端拿到服务实例,跟1.x一样,也会存到服务注册表

除了注册之外,当注册的是临时实例时,2.x还会将服务实例信息存储到客户端中的一个缓存中,供Redo操作

所谓的Redo操作,其实就是一个补偿机制,本质是个定时任务,默认每3s执行一次

这个定时任务作用是,当客户端与服务端重新建立连接时(因为一些异常原因导致连接断开)那么之前注册的服务实例肯定还要继续注册服务端(断开连接服务实例就会被剔除服务注册表)

所以这个Redo操作一个很重要的作用就是重连之后的重新注册的作用即除了注册之外,比如服务订阅之类的操作也需要Redo操作,当连接重新建立,之前客户端的操作都需要Redo一下

小总结:
1.x版本是通过Http协议来进行服务注册的

2.x由于客户端与服务端的通信改成了gRPC长连接,所以改成通过gRPC长连接来注册

2.x比1.x多个Redo操作,当注册的服务实例是临时实例是,出现网络异常,连接重新建立之后,客户端需要将服务注册、服务订阅之类的操作进行重做

这里你可能会有个疑问

既然2.x有Redo机制保证客户端与服务端通信正常之后重新注册,那么1.x有类似的这种Redo机制么?

当然也会有,接下往下看。

心跳机制

心跳机制,也可以被称为保活机制,它的作用就是服务实例告诉注册中心我这个服务实例还活着

Nacos注册中心核心原理_第5张图片
在Nacos服务端也会开启一个定时任务,默认也是5s执行一次,去检查这些服务实例最后一次心跳的时间,也就是客户端最后一次发送Http请求的时间

当最后一次心跳时间超过15s,但没有超过30s,会把这服务实例标记成不健康
当最后一次心跳超过30s,直接把服务从服务注册表中剔除

Nacos注册中心核心原理_第6张图片

这就是1.x版本的心跳机制,本质就是两个定时任务

其实1.x的这个心跳还有一个作用,就是跟上一节说的gRPC时Redo操作的作用是一样的
服务在处理心跳的时候,发现心跳携带这个服务实例的信息在注册表中没有,此时就会添加到服务注册表
所以心跳也有Redo的类似效果

2.x心跳实现
在2.x版本之后,由于通信协议改成了gRPC,客户端与服务端保持长连接,所以2.x版本之后它是利用这个gRPC长连接本身的心跳来保活

一旦这个连接断开,服务端就会认为这个连接注册的服务实例不可用,之后就会将这个服务实例从服务注册表中提出剔除

除了连接本身的心跳之外,Nacos还有服务端的一个主动检测机制

Nacos服务端也会启动一个定时任务,默认每隔3s执行一次

这个任务会去检查超过20s没有发送请求数据的连接

一旦发现有连接已经超过20s没发送请求,那么就会向这个连接对应的客户端发送一个请求

如果请求不通或者响应失败,此时服务端也会认为与客户端的这个连接异常,从而将这个客户端注册的服务实例从服务注册表中剔除

所以对于2.x版本,主要是两种机制来进行保活:

连接本身的心跳机制,断开就直接剔除服务实例
Nacos主动检查机制,服务端会对20s没有发送数据的连接进行检查,出现异常时也会主动断开连接,剔除服务实例

小总结
心跳机制仅仅针对临时实例而言
1.x心跳机制是通过客户端和服务端两个定时任务来完成的,客户端定时上报心跳信息,服务端定时检查心跳时间,超过15s标记不健康,超过30s直接剔除

1.x心跳机制还有类似2.x的Redo作用,服务端发现心跳的服务信息不存在会,会将服务信息添加到注册表,相当于重新注册了

2.x是基于gRPC长连接本身的心跳机制和服务端的定时检查机制来的,出现异常直接剔除

健康检查

前面说了,心跳机制仅仅是临时实例用来保护的机制
而对于永久实例来说,一般来说无法主动上报心跳
就比如说MySQL实例,肯定是不会主动上报心跳到Nacos的,所以这就导致无法通过心跳机制来保活
所以针对永久实例的情况,Nacos通过一种叫健康检查的机制去判断服务实例是否活着

健康检查跟心跳机制刚好相反,心跳机制是服务实例向服务端发送请求

而所谓的健康检查就是服务端主动向服务实例发送请求,去探测服务实例是否活着
Nacos注册中心核心原理_第7张图片
健康检查机制在1.x和2.x的实现机制是一样的

Nacos服务端在会去创建一个健康检查任务,这个任务每次执行时间间隔会在2000~7000毫秒之间

当任务触发的时候,会根据设置的健康检查的方式执行不同的逻辑,目前主要有以下三种方式:TCP,HTTP,MySQL

TCP的方式就是根据服务实例的ip和端口去判断是否能连接成功,如果连接成功,就认为健康,反之就任务不健康
HTTP的方式就是向服务实例的ip和端口发送一个Http请求,请求路径是需要设置的,如果能正常请求,说明实例健康,反之就不健康
MySQL的方式是一种特殊的检查方式,他可以执行下面这条Sql来判断数据库是不是主库
Nacos注册中心核心原理_第8张图片

默认情况下,都是通过TCP的方式来探测服务实例是否还活着

服务发现

所谓的服务发现就是指当有服务实例注册成功之后,其它服务可以发现这些服务实例

Nacos提供了两种发现方式:主动查询和服务订阅
主动查询就是指客户端主动向服务端查询需要关注的服务实例,也就是拉(pull)的模式

服务订阅就是指客户端向服务端发送一个订阅服务的请求,当被订阅的服务有信息变动就会主动将服务实例的信息推送给订阅的客户端,本质就是推(push)模式
Nacos注册中心核心原理_第9张图片
在我们平时使用时,一般来说都是选择使用订阅的方式,这样一旦有服务实例数据的变动,客户端能够第一时间感知
并且Nacos在整合SpringCloud的时候,默认就是使用订阅的方式

对于这两种服务发现方式,1.x和2.x版本实现也是不一样

服务查询其实两者实现都很简单
1.x整体就是发送Http请求去查询服务实例,2.x只不过是将Http请求换成了gRPC的请求
服务端对于查询的处理过程都是一样的,从服务注册表中查出符合查询条件的服务实例进行返回。

不过对于服务订阅,两者的机制就稍微复杂一点
在Nacos客户端,不论是1.x还是2.x都是通过SDK中的NamingService#subscribe方法来发起订阅的
Nacos注册中心核心原理_第10张图片
当有服务实例数据变动的时,客户端就会回调EventListener,就可以拿到最新的服务实例数据了
虽然1.x还是2.x都是同样的方法,但是具体的实现逻辑是不一样的

1.x服务订阅实现

在1.x版本的时候,服务订阅的处理逻辑大致会有以下三步:

第一步,客户端在启动的时候,会去构建一个叫PushReceiver的类

这个类会去创建一个UDP Socket,端口是随机的
Nacos注册中心核心原理_第11张图片
其实通过名字就可以知道这个类的作用,就是通过UDP的方式接收服务端推送的数据的

第二步,调用NamingService#subscribe来发起订阅时,会先去服务端查询需要订阅服务的所有实例信息

之后会将所有服务实例数据存到客户端的一个内部缓存中
Nacos注册中心核心原理_第12张图片
并且在查询的时候,会将这个UDP Socket的端口作为一个参数传到服务端

服务端接收到这个UDP端口后,后续就通过这个端口给客户端推送服务实例数据

第三步,会为这次订阅开启一个不定时执行的任务
之所以不定时,是因为这个当执行异常的时候,下次执行的时间间隔就会变长,但是最多不超过60s,正常是10s,这个10s是查询服务实例是服务端返回的

既然有了服务变动推送的功能,为什么还要定时去查询更新服务实例信息呢?
其实很简单,那就是因为UDP通信不稳定导致的

虽然有Push,但是由于UDP通信自身的不确定性,有可能会导致客户端接收变动信息失败

所以这里就加了一个定时任务,弥补这种可能性,属于一个兜底的方案。
这就是1.x版本的服务订阅的实现

Nacos注册中心核心原理_第13张图片

2.x服务订阅的实现

讲完1.x的版本实现,接下来就讲一讲2.x版本的实现

由于2.x版本换成了gRPC长连接的方式,所以2.x版本服务数据变更推送已经完全抛弃了1.x的UDP做法

当有服务实例变动的时候,服务端直接通过这个长连接将服务信息发送给客户端

客户端拿到最新服务实例数据之后的处理方式就跟1.x是一样了

除了处理方式一样,2.x也继承了1.x的其他的东西

比如客户端依然会有服务实例的缓存

定时对比机制也保留了,只不过这个定时对比的机制默认是关闭状态

之所以默认关闭,主要还是因为长连接还是比较稳定的原因

当客户端出现异常,接收不到请求,那么服务端会直接跟客户端断开连接

当恢复正常,由于有Redo操作,所以还是能拿到最新的实例信息的

所以2.x版本的服务订阅功能的实现大致如下图所示

Nacos注册中心核心原理_第14张图片
这里还有个细节需要注意

在1.x版本的时候,任何服务都是可以被订阅的

但是在2.x版本中,只支持订阅临时服务,对于永久服务,已经不支持订阅了

小总结
服务查询1.x是通过Http请求;2.x通过gRPC请求

服务订阅1.x是通过UDP来推送的;2.x就基于gRPC长连接来实现的

1.x和2.x客户端都有服务实例的缓存,也有定时对比机制,只不过1.x会自动开启;2.x提供了一个开关,可以手动选择是否开启,默认不开启

参考文章:
https://nacos.io/zh-cn/docs/quick-start.html
https://mp.weixin.qq.com/s/NYH6jgDOp1MwnAUyUHM5yg

你可能感兴趣的:(面试,分布式,spring,微服务)