网站设计 网站开发 优化,设计网站推荐百度贴吧,泉州免费做网站,凡客诚品官方网站首页前言
本文来讲一讲nacos作为底层注册中心的实现原理。那么就有这几个问题#xff1f; 临时实例和永久实例是什么#xff1f;有什么区别#xff1f; 服务实例是如何注册到服务端的#xff1f; 服务实例和服务端之间是如何保活的#xff1f; 服务订阅是如何实现的#…前言
本文来讲一讲nacos作为底层注册中心的实现原理。那么就有这几个问题 临时实例和永久实例是什么有什么区别 服务实例是如何注册到服务端的 服务实例和服务端之间是如何保活的 服务订阅是如何实现的 集群间数据是如何同步的CP还是AP Nacos的数据模型是什么样的
本文就通过探讨上述问题来探秘Nacos服务注册中心核心的底层实现原理。
临时实例和永久实例
临时实例和永久实例在Nacos中是一个非常非常重要的概念
之所以说它重要主要是因为我在读源码的时候发现临时实例和永久实例在底层的许多实现机制是完全不同的
临时实例
临时实例在注册到注册中心之后仅仅只保存在服务端内部一个缓存中不会持久化到磁盘
这个服务端内部的缓存在注册中心届一般被称为服务注册表
当服务实例出现异常或者下线之后就会把这个服务实例从服务注册表中剔除
永久实例
永久服务实例不仅仅会存在服务注册表中同时也会被持久化到磁盘文件中
当服务实例出现异常或者下线Nacos只会将服务实例的健康状态设置为不健康并不会对将其从服务注册表中剔除
所以这个服务实例的信息你还是可以从注册中心看到只不过处于不健康状态
这是就是两者最最最基本的区别 当然除了上述最基本的区别之外两者还有很多其它的区别接下来本文还会提到 这里你可能会有一个疑问 为什么Nacos要将服务实例分为临时实例和永久实例? 主要还是因为应用场景不同
临时实例就比较适合于业务服务服务下线之后可以不需要在注册中心中查看到
永久实例就比较适合需要运维的服务这种服务几乎是永久存在的比如说MySQL、Redis等等 MySQL、Redis等服务实例可以通过SDK手动注册 对于这些服务我们需要一直看到服务实例的状态即使出现异常也需要能够查看时实的状态 所以从这可以看出Nacos跟你印象中的注册中心不太一样他不仅仅可以注册平时业务中的实例还可以注册像MySQL、Redis这个服务实例的信息到注册中心 在SpringCloud环境底下一般其实都是业务服务所以默认注册服务实例都是临时实例
当然如果你想改成永久实例可以通过下面这个配置项来完成
springcloud:nacos:discovery:#ephemeral单词是临时的意思设置成false就是永久实例了ephemeral: false这里还有一个小细节
在1.x版本中一个服务中可以既有临时实例也有永久实例服务实例是永久还是临时是由服务实例本身决定的
但是2.x版本中一个服务中的所有实例要么都是临时的要么都是永久的是由服务决定的而不是具体的服务实例
所以在2.x可以说是临时服务和永久服务 为什么2.x把临时还是永久的属性由实例本身决定改成了由服务决定? 其实很简单你想想假设对一个MySQL服务来说它的每个服务实例肯定都是永久的不会出现一些是永久的一些是临时的情况吧
所以临时还是永久的属性由服务本身决定其实就更加合理了
服务注册
作为一个服务注册中心服务注册肯定是一个非常重要的功能
所谓的服务注册就是通过注册中心提供的客户端SDK或者是控制台将服务本身的一些元信息比如ip、端口等信息发送到注册中心服务端
服务端在接收到服务之后会将服务的信息保存到前面提到的服务注册表中
1、1.x版本的实现
在Nacos在1.x版本的时候服务注册是通过Http接口实现的 代码如下 整个逻辑比较简单因为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服务端发送一个服务下线的请求
Nacos服务端在接收到请求之后会将这个服务实例从服务注册表中剔除
但是对于异常情况下比如出现网络问题可能导致这个注册的服务实例无法提供服务处于不可用状态也就是不健康
而此时在没有任何机制的情况下服务端是无法知道这个服务处于不可用状态
所以为了避免这种情况一些注册中心就比如Nacos、Eureka就会用心跳机制来判断这个服务实例是否能正常
在Nacos中心跳机制仅仅是针对临时实例来说的临时实例需要靠心跳机制来保活
心跳机制在1.x和2.x版本的实现也是不一样的
1.x心跳实现
在1.x中心跳机制实现是通过客户端和服务端各存在的一个定时任务来完成的
在服务注册时发现是临时实例客户端会开启一个5s执行一次的定时任务 这个定时任务会构建一个Http请求携带这个服务实例的信息然后发送到服务端 在Nacos服务端也会开启一个定时任务默认也是5s执行一次去检查这些服务实例最后一次心跳的时间也就是客户端最后一次发送Http请求的时间 当最后一次心跳时间超过15s但没有超过30s会把这服务实例标记成不健康 当最后一次心跳超过30s直接把服务从服务注册表中剔除 这就是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通过一种叫健康检查的机制去判断服务实例是否活着
健康检查跟心跳机制刚好相反心跳机制是服务实例向服务端发送请求
而所谓的健康检查就是服务端主动向服务实例发送请求去探测服务实例是否活着 健康检查机制在1.x和2.x的实现机制是一样的
Nacos服务端在会去创建一个健康检查任务这个任务每次执行时间间隔会在2000~7000毫秒之间
当任务触发的时候会根据设置的健康检查的方式执行不同的逻辑目前主要有以下三种方式 TCP HTTP MySQL
TCP的方式就是根据服务实例的ip和端口去判断是否能连接成功如果连接成功就认为健康反之就任务不健康
HTTP的方式就是向服务实例的ip和端口发送一个Http请求请求路径是需要设置的如果能正常请求说明实例健康反之就不健康
MySQL的方式是一种特殊的检查方式他可以执行下面这条Sql来判断数据库是不是主库 默认情况下都是通过TCP的方式来探测服务实例是否还活着
服务发现
所谓的服务发现就是指当有服务实例注册成功之后其它服务可以发现这些服务实例
Nacos提供了两种发现方式 主动查询 服务订阅
主动查询就是指客户端主动向服务端查询需要关注的服务实例也就是拉pull的模式
服务订阅就是指客户端向服务端发送一个订阅服务的请求当被订阅的服务有信息变动就会主动将服务实例的信息推送给订阅的客户端本质就是推push模式 在我们平时使用时一般来说都是选择使用订阅的方式这样一旦有服务实例数据的变动客户端能够第一时间感知
并且Nacos在整合SpringCloud的时候默认就是使用订阅的方式
对于这两种服务发现方式1.x和2.x版本实现也是不一样
服务查询其实两者实现都很简单
1.x整体就是发送Http请求去查询服务实例2.x只不过是将Http请求换成了gRPC的请求
服务端对于查询的处理过程都是一样的从服务注册表中查出符合查询条件的服务实例进行返回
不过对于服务订阅两者的机制就稍微复杂一点
在Nacos客户端不论是1.x还是2.x都是通过SDK中的NamingService#subscribe方法来发起订阅的 当有服务实例数据变动的时客户端就会回调EventListener就可以拿到最新的服务实例数据了
虽然1.x还是2.x都是同样的方法但是具体的实现逻辑是不一样的
1.x服务订阅实现
在1.x版本的时候服务订阅的处理逻辑大致会有以下三步
第一步客户端在启动的时候会去构建一个叫PushReceiver的类
这个类会去创建一个UDP Socket端口是随机的 其实通过名字就可以知道这个类的作用就是通过UDP的方式接收服务端推送的数据的
第二步调用NamingService#subscribe来发起订阅时会先去服务端查询需要订阅服务的所有实例信息
之后会将所有服务实例数据存到客户端的一个内部缓存中 并且在查询的时候会将这个UDP Socket的端口作为一个参数传到服务端
服务端接收到这个UDP端口后后续就通过这个端口给客户端推送服务实例数据
第三步会为这次订阅开启一个不定时执行的任务 之所以不定时是因为这个当执行异常的时候下次执行的时间间隔就会变长但是最多不超过60s正常是10s这个10s是查询服务实例是服务端返回的 这个任务会去从服务端查询订阅的服务实例信息然后更新内部缓存
这里你可能会有个疑问 既然有了服务变动推送的功能为什么还要定时去查询更新服务实例信息呢? 其实很简单那就是因为UDP通信不稳定导致的
虽然有Push但是由于UDP通信自身的不确定性有可能会导致客户端接收变动信息失败
所以这里就加了一个定时任务弥补这种可能性属于一个兜底的方案。
这就是1.x版本的服务订阅的实现 2.x服务订阅的实现
讲完1.x的版本实现接下来就讲一讲2.x版本的实现
由于2.x版本换成了gRPC长连接的方式所以2.x版本服务数据变更推送已经完全抛弃了1.x的UDP做法
当有服务实例变动的时候服务端直接通过这个长连接将服务信息发送给客户端
客户端拿到最新服务实例数据之后的处理方式就跟1.x是一样了
除了处理方式一样2.x也继承了1.x的其他的东西
比如客户端依然会有服务实例的缓存
定时对比机制也保留了只不过这个定时对比的机制默认是关闭状态
之所以默认关闭主要还是因为长连接还是比较稳定的原因
当客户端出现异常接收不到请求那么服务端会直接跟客户端断开连接
当恢复正常由于有Redo操作所以还是能拿到最新的实例信息的
所以2.x版本的服务订阅功能的实现大致如下图所示 这里还有个细节需要注意
在1.x版本的时候任何服务都是可以被订阅的
但是在2.x版本中只支持订阅临时服务对于永久服务已经不支持订阅了
小总结
服务查询1.x是通过Http请求2.x通过gRPC请求
服务订阅1.x是通过UDP来推送的2.x就基于gRPC长连接来实现的
1.x和2.x客户端都有服务实例的缓存也有定时对比机制只不过1.x会自动开启2.x提供了一个开个可以手动选择是否开启默认不开启
数据一致性
由于Nacos是支持集群模式的所以一定会涉及到分布式系统中不可避免的数据一致性问题
1、服务实例的责任机制
再说数据一致性问题之前先来讨论一下服务实例的责任机制
什么是服务实例的责任机制
比如上面提到的服务注册、心跳管理、监控检查机制当只有一个Nacos服务时那么自然而言这个服务会去检查所有的服务实例的心跳时间执行所有服务实例的健康检查任务 但是当出现Nacos服务出现集群时为了平衡各Nacos服务的压力Nacos会根据一定的规则让每个Nacos服务只管理一部分服务实例的 当然每个Nacos服务的注册表还是全部的服务实例数据 这个管理机制我给他起了一个名字就叫做责任机制因为我在1.x和2.x都提到了responsible这个单词
本质就是Nacos服务对哪些服务实例负有心跳监测健康检查的责任。
2、CAP定理和BASE理论
谈到数据一致性问题一定离不开两个著名分布式理论 CAP定理 BASE理论
CAP定理中三个字母分别代表这些含义 CConsistency单词的缩写代表一致性指分布式系统中各个节点的数据保持强一致也就是每个时刻都必须一样不一样整个系统就不能对外提供服务 AAvailability单词的缩写代表可用性指整个分布式系统保持对外可用即使从每个节点获取的数据可能都不一样只要能获取到就行 PPartition tolerance单词的缩写代表分区容错性。
所谓的CAP定理就是指在一个分布式系统中CAP这三个指标最多同时只能满足其中的两个不可能三个都同时满足 为什么三者不能同时满足
对于一个分布式系统网络分区是一定需要满足的
而所谓分区指的是系统中的服务部署在不同的网络区域中 比如同一套系统可能同时在北京和上海都有部署那么他们就处于不同的网络分区就可能出现无法互相访问的情况
当然你也可以把所有的服务都放在一个网络分区但是当网络出现故障时整个系统都无法对外提供服务那这还有什么意义呢
所以分布式系统一定需要满足分区容错性把系统部署在不同的区域网络中
此时只剩下了一致性和可用性它们为什么不能同时满足
其实答案很简单就因为可能出现网络分区导致的通信失败。
比如说现在出现了网络分区的问题上图中的A网络区域和B网络区域无法相互访问
此时假设往上图中的A网络区域发送请求将服务中的一个值 i 属性设置成 1 如果保证可用性此时由于A和B网络不通此时只有A中的服务修改成功B无法修改成功此时数据AB区域数据就不一致性也就没有保证数据一致性
如果保证一致性此时由于A和B网络不通所以此时A也不能修改成功必须修改失败否则就会导致AB数据不一致
虽然A没修改成功保证了数据一致性AB还是之前相同的数据但是此时整个系统已经没有写可用性了无法成功写数据了。
所以从上面分析可以看出在有分区容错性的前提下可用性和一致性是无法同时保证的。
虽然无法同时一致性和可用性但是能不能换种思路来思考一下这个问题
首先我们可以先保证系统的可用性也就是先让系统能够写数据将A区域服务中的i修改成1
之后当AB区域之间网络恢复之后将A区域的i值复制给B区域这样就能够保证AB区域间的数据最终是一致的了
这不就皆大欢喜了么
这种思路其实就是BASE理论的核心要点优先保证可用性数据最终达成一致性。
BASE理论主要是包括以下三点 基本可用Basically Available系统出现故障还是能够对外提供服务不至于直接无法用了 软状态Soft State允许各个节点的数据不一致 最终一致性Eventually Consistent虽然允许各个节点的数据不一致但是在一定时间之后各个节点的数据最终需要一致的
BASE理论其实就是妥协之后的产物。
3、Nacos的AP和CP
Nacos其实目前是同时支持AP和CP的
具体使用AP还是CP得取决于Nacos内部的具体功能并不是有的文章说的可以通过一个配置自由切换。
就以服务注册举例来说对于临时实例来说Nacos会优先保证可用性也就是AP
对于永久实例Nacos会优先保证数据的一致性也就是CP
接下来我们就来讲一讲Nacos的CP和AP的实现原理
3.1、Nacos的AP实现
对于AP来说Nacos使用的是阿里自研的Distro协议
在这个协议中每个服务端节点是一个平等的状态每个服务端节点正常情况下数据是一样的每个服务端节点都可以接收来自客户端的读写请求 当某个节点刚启动时他会向集群中的某个节点发送请求拉取所有的服务实例数据到自己的服务注册表中 这样其它客户端就可以从这个服务节点中获取到服务实例数据了
当某个服务端节点接收到注册临时服务实例的请求不仅仅会将这个服务实例存到自身的服务注册表同时也会向其它所有服务节点发送请求将这个服务数据同步到其它所有节点 所以此时从任意一个节点都是可以获取到所有的服务实例数据的。
即使数据同步的过程发生异常服务实例也成功注册到一个Nacos服务中对外部而言整个Nacos集群是可用的也就达到了AP的效果
同时为了满足BASE理论Nacos也有下面两种机制保证最终节点间数据最终是一致的 失败重试机制 定时对比机制
失败重试机制是指当数据同步给其它节点失败时会每隔3s重试一次直到成功
定时对比机制就是指每个Nacos服务节点会定时向所有的其它服务节点发送一些认证的请求
这个请求会告诉每个服务节点自己负责的服务实例的对应的版本号这个版本号随着服务实例的变动就会变动
如果其它服务节点的数据的版本号跟自己的对不上那就说明其它服务节点的数据不是最新的
此时这个Nacos服务节点就会将自己负责的服务实例数据发给不是最新数据的节点这样就保证了每个节点的数据是一样的了。
3.2、Nacos的CP实现
Nacos的CP实现是基于Raft算法来实现的
在1.x版本早期Nacos是自己手动实现Raft算法
在2.x版本Nacos移除了手动实现Raft算法转而拥抱基于蚂蚁开源的JRaft框架
在Raft算法每个节点主要有三个状态 Leader负责所有的读写请求一个集群只有一个 Follower从节点主要是负责复制Leader的数据保证数据的一致性 Candidate候选节点最终会变成Leader或者Follower
集群启动时都是节点Follower经过一段时间会转换成Candidate状态再经过一系列复杂的选择算法选出一个Leader 这个选举算法比较复杂完全值得另写一篇文章这里就不细说了。不过立个flag如果本篇文章点赞量超过28个我连夜爆肝再来一篇。 当有写请求时如果请求的节点不是Leader节点时会将请求转给Leader节点由Leader节点处理写请求
比如有个客户端连到的上图中的Nacos服务2节点之后向Nacos服务2注册服务
Nacos服务2接收到请求之后会判断自己是不是Leader节点发现自己不是
此时Nacos服务2就会向Leader节点发送请求Leader节点接收到请求之后会处理服务注册的过程
为什么说Raft是保证CP的呢
主要是因为Raft在处理写的时候有一个判断过程 首先Leader在处理写请求时不会直接数据应用到自己的系统而是先向所有的Follower发送请求让他们先处理这个请求 当超过半数的Follower成功处理了这个写请求之后Leader才会写数据并返回给客户端请求处理成功 如果超过一定时间未收到超过半数处理成功Follower的信号此时Leader认为这次写数据是失败的就不会处理写请求直接返回给客户端请求失败
所以一旦发生故障导致接收不到半数的Follower写成功的响应整个集群就直接写失败这就很符合CP的概念了。
不过这里还有一个小细节需要注意
Nacos在处理查询服务实例的请求直接时并不会将请求转发给Leader节点处理而是直接查当前Nacos服务实例的注册表
这其实就会引发一个问题
如果客户端查询的Follower节点没有及时处理Leader同步过来的写请求过半响应的节点中不包括这个节点此时在这个Follower其实是查不到最新的数据的这就会导致数据的不一致
所以说虽然Raft协议规定要求从Leader节点查最新的数据但是Nacos至少在读服务实例数据时并没有遵守这个协议
当然对于其它的一些数据的读写请求有的还是遵守了这个协议。 JRaft对于读请求其实是做了很多优化的其实从Follower节点通过一定的机制也是能够保证读到最新的数据 数据模型
在Nacos中一个服务的确定是由三部分信息确定 命名空间Namespace多租户隔离用的默认是public 分组Group这个其实可以用来做环境隔离服务注册时可以指定服务的分组比如是测试环境或者是开发环境默认是DEFAULT_GROUP 服务名ServiceName这个就不用多说了
通过上面三者就可以确定同一个服务了
在服务注册和订阅的时候必须要指定上述三部分信息如果不指定Nacos就会提供默认的信息
不过在Nacos中在服务里面其实还是有一个集群的概念 在服务注册的时候可以指定这个服务实例在哪个集体的集群中默认是在DEFAULT集群下
在SpringCloud环境底下可以通过如下配置去设置
springcloud:nacos:discovery:cluster-name: sanyoujavaCluster在服务订阅的时候可以指定订阅哪些集群下的服务实例
当然也可以不指定如果不指定话默认就是订阅这个服务下的所有集群的服务实例
我们日常使用中可以将部署在相同区域的服务划分为同一个集群比如杭州属于一个集群上海属于一个集群
这样服务调用的时候就可以优先使用同一个地区的服务了比跨区域调用速度更快。
总结
到这也是讲完了Nacos作为注册中心核心的实现原理
不知道你读完整篇文章是否有所收获
不过有一点可以确定的是能够看到这里的那一定都是真爱粉了
联系方式
关于文章中大家有任何疑问可以通过关注公众号《编程乐学》进行留言同时公众号还有更多有趣的项目以及关于学习编程的笔记资料大家可以看看欢迎大家进行留言。