不同版本Nacos原理之临时/永久实例,注册服务,心跳保活,服务发现,责任机制

1 Nacos原理

1.1 临时实例和永久实例

临时实例永久实例Nacos中是一个非常非常重要的概念,之所以说它重要,主要是因为在读源码的时候发现,临时实例永久实例在底层的许多实现机制是完全不同的

1.1.1 临时实例

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

1.1.2 永久实例

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

这是就是两者最最最基本的区别

1.1.3 应用场景

临时实例就比较适合于业务服务,服务下线之后可以不需要在注册中心中查看到
永久实例就比较适合需要运维的服务,这种服务几乎是永久存在的,比如说MySQL、Redis等等
MySQL、Redis等服务实例可以通过SDK手动注册
对于这些服务,我们需要一直看到服务实例的状态,即使出现异常,也需要能够查看时实的状态
所以从这可以看出Nacos跟印象中的注册中心不太一样,不仅仅可以注册平时业务中的实例,还可以注册像MySQL、Redis这个服务实例的信息到注册中心

SpringCloud环境底下,一般其实都是业务服务,所以默认注册服务实例都是临时实例
当然如果你想改成永久实例,可以通过下面这个配置项来完成

spring
  cloud:
    nacos:
      discovery:
        #ephemeral单词是临时的意思,设置成false,就是永久实例了
        ephemeral: false

注意:在1.x版本中,一个服务中可以既有临时实例也有永久实例,服务实例是永久还是临时是由服务实例本身决定的,但是2.x版本中,一个服务中的所有实例要么都是临时的要么都是永久的,是由服务决定的,而不是具体的服务实例,所以在2.x可以说是临时服务和永久服务
在这里插入图片描述

那么为什么2.x把临时还是永久的属性由实例本身决定改成了由服务决定

其实很简单,假设对一个MySQL服务来说,它的每个服务实例肯定都是永久的,不会出现一些是永久的,一些是临时的情况
所以临时还是永久的属性由服务本身决定其实就更加合理了

1.2 服务注册

作为一个服务注册中心,服务注册肯定是一个非常重要的功能
所谓的服务注册,就是通过注册中心提供的客户端 SDK(或者是控制台)将服务本身的一些元信息,比如ip、端口等信息发送到注册中心服务端
服务端在接收到服务之后,会将服务的信息保存到前面提到的服务注册表中

1.2.1 1.x版本的实现

Nacos1.x版本的时候,服务注册是通过Http接口实现的
图片

代码如下

public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {
	NAMING_LOGGER.info("[REGISTER-SERVICE]{} registering service {} with instance: {}", namespaceId, serviceName,instance);
	final Map<String,String> params = new HashMap<>( initialCapacity:16);
	params.put(CommonParams.NAMESPACE_ID,namespaceld);
	params.put(CommonParams.SERVICE_NAME,serviceName);
	params.put(CommonParams.GROUP_NAME, groupName);
	params.put(CommonParams.CLUSTER_NAME,instance.getClusterName());
	params.put("ip",instance.getIp());
	params .put("port",String.value0f(instance.getPort()));
	params.put("weight",String.value0f(instance.getWeight()));
	params.put("enable",String.value0f(instance.isEnabled()));
	params.put("healthy",String.value0f(instance.isHealthy()));
	params .put("ephemeral",String.value0f(instance.isEphemeral()));
	params.put("metadata",JacksonUtils.toJson(instance.getMetadata()));

	reqApi(UtilAndComs.nacosUrlInstance, params, HttpMethod.POST);
}

整个逻辑比较简单,因为Nacos服务端本身就是用SpringBoot写的

但是在2.x版本的实现就比较复杂了

1.2.2 2.x版本的实现

1.2.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.xNacos SDK依然可以注册到2.x版本的服务端

1.2.2.2 具体的实现

Nacos客户端在启动的时候,会通过gRPC跟服务端建立长连接
在这里插入图片描述
这个连接会一直存在,之后客户端与服务端所有的通信都是基于这个长连接来的
当客户端发起注册的时候,就会通过这个长连接,将服务实例的信息发送给服务端
服务端拿到服务实例,跟1.x一样,也会存到服务注册表
除了注册之外,当注册的是临时实例时,2.x还会将服务实例信息存储到客户端中的一个缓存中,供Redo操作

所谓的Redo操作,其实就是一个补偿机制,本质是个定时任务,默认每3s执行一次
这个定时任务作用是,当客户端与服务端重新建立连接时(因为一些异常原因导致连接断开),那么之前注册的服务实例肯定还要继续注册服务端(断开连接服务实例就会被剔除服务注册表)
所以这个Redo操作一个很重要的作用就是重连之后的重新注册的作用
除了注册之外,比如服务订阅之类的操作也需要Redo操作,当连接重新建立,之前客户端的操作都需要Redo一下

1.2.3 服务注册总结

总结:

  • 1.x:是通过Http协议来进行服务注册的
  • 2.x:由于客户端与服务端的通信改成了gRPC长连接,所以改成通过gRPC长连接来注册
  • 2.x1.x多个Redo操作,当注册的服务实例是临时实例是,出现网络异常,连接重新建立之后,客户端需要将服务注册、服务订阅之类的操作进行重做

这里你可能会有个疑问
既然2.x有Redo机制保证客户端与服务端通信正常之后重新注册,那么1.x有类似的这种Redo机制么?
当然也会有,接下往下看

1.3 心跳机制

心跳机制,也可以被称为保活机制,它的作用就是服务实例告诉注册中心这个服务实例还活着
在正常情况下,服务关闭了,那么服务会主动向Nacos服务端发送一个服务下线的请求,Nacos服务端在接收到请求之后,会将这个服务实例从服务注册表中剔除
但是对于异常情况下,比如出现网络问题,可能导致这个注册的服务实例无法提供服务,处于不可用状态,也就是不健康,而此时在没有任何机制的情况下,服务端是无法知道这个服务处于不可用状态

所以为了避免这种情况,一些注册中心,就比如Nacos、Eureka,就会用心跳机制来判断这个服务实例是否能正常

Nacos中,心跳机制仅仅是针对临时实例来说的,临时实例需要靠心跳机制来保活
心跳机制1.x2.x版本的实现也是不一样的

1.3.1 1.x心跳实现

1.x中,心跳机制实现是通过客户端和服务端各存在的一个定时任务来完成的
在服务注册时,发现是临时实例,客户端会开启一个5s执行一次的定时任务
在这里插入图片描述

这个定时任务会构建一个Http请求,携带这个服务实例的信息,然后发送到服务端
在这里插入图片描述

Nacos服务端也会开启一个定时任务,默认也是5s执行一次,去检查这些服务实例最后一次心跳的时间,也就是客户端最后一次发送Http请求的时间

  • 当最后一次心跳时间超过15s,但没有超过30s,会把这服务实例标记成不健康
  • 当最后一次心跳超过30s,直接把服务从服务注册表中剔除
    在这里插入图片描述
    这就是1.x版本的心跳机制,本质就是两个定时任务,其实1.x的这个心跳还有一个作用,就是跟上面说的gRPCRedo操作的作用是一样的
    服务在处理心跳的时候,发现心跳携带这个服务实例的信息在注册表中没有,此时就会添加到服务注册表
    所以心跳也有Redo的类似效果

1.3.2 2.x心跳实现

2.x版本之后,由于通信协议改成了gRPC,客户端与服务端保持长连接,所以2.x版本之后它是利用这个gRPC长连接本身的心跳来保活,一旦这个连接断开,服务端就会认为这个连接注册的服务实例不可用,之后就会将这个服务实例从服务注册表中剔除

除了连接本身的心跳之外,Nacos还有服务端的一个主动检测机制
Nacos服务端也会启动一个定时任务,默认每隔3s执行一次,这个任务会去检查超过20s没有发送请求数据的连接,一旦发现有连接已经超过20s没发送请求,那么就会向这个连接对应的客户端发送一个请求,如果请求不通或者响应失败,此时服务端也会认为与客户端的这个连接异常,从而将这个客户端注册的服务实例从服务注册表中剔除

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

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

1.3.3 心跳机制总结

总结:

  • 心跳机制仅仅针对临时实例而言
  • 1.x 心跳机制是通过客户端和服务端两个定时任务来完成的,客户端定时上报心跳信息,服务端定时检查心跳时间,超过15s标记不健康,超过30s直接剔除
  • 1.x心跳机制还有类似2.xRedo作用,服务端发现心跳的服务信息不存在会,会将服务信息添加到注册表,相当于重新注册了
  • 2.x是基于gRPC长连接本身的心跳机制和服务端的定时检查机制来的,出现异常直接剔除

1.4 健康检查

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

健康检查心跳机制刚好相反,心跳机制服务实例服务端发送请求,而所谓的健康检查就是服务端主动向服务实例发送请求,去探测服务实例是否活着

健康检查机制在1.x和2.x的实现机制是一样的
Nacos服务端在会去创建一个健康检查任务,这个任务每次执行时间间隔会在2000~7000毫秒之间
当任务触发的时候,会根据设置的健康检查的方式执行不同的逻辑,目前主要有三种方式:TCPHTTPMySQL

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

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

1.5 服务发现

1.5.1 简介

服务发现就是指当有服务实例注册成功之后,其它服务可以发现这些服务实例
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.5.2 1.x服务订阅实现

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

  • 第一步,客户端在启动的时候,会去构建一个叫PushReceiver的类,这个类会去创建一个UDP Socket,端口是随机的
    在这里插入图片描述
    其实通过名字就可以知道这个类的作用,就是通过UDP的方式接收服务端推送的数据的
  • 第二步,调用NamingService#subscribe来发起订阅时,会先去服务端查询需要订阅服务的所有实例信息,之后会将所有服务实例数据存到客户端的一个内部缓存中
    在这里插入图片描述
    并且在查询的时候,会将这个UDP Socket 的端口作为一个参数传到服务端,服务端接收到这个UDP端口后,后续就通过这个端口给客户端推送服务实例数据
  • 第三步,会为这次订阅开启一个不定时执行的任务,这个任务会去从服务端查询订阅的服务实例信息,然后更新内部缓存
    之所以不定时,是因为这个当执行异常的时候,下次执行的时间间隔就会变长,但是最多不超过60s,正常是10s,这个10s是查询服务实例是服务端返回的

那么既然有了服务变动推送的功能,为什么还要定时去查询更新服务实例信息呢?

其实很简单,那就是因为UDP通信不稳定导致的
虽然有Push,但是由于UDP通信自身的不确定性,有可能会导致客户端接收变动信息失败
所以这里就加了一个定时任务,弥补这种可能性,属于一个兜底的方案。

1.x版本的服务订阅的实现
在这里插入图片描述

1.5.3 2.x服务订阅的实现

讲完1.x的版本实现,接下来就讲一讲2.x版本的实现
由于2.x版本换成了gRPC长连接的方式,所以2.x版本服务数据变更推送已经完全抛弃了1.xUDP做法,当有服务实例变动的时候,服务端直接通过这个长连接将服务信息发送给客户端
客户端拿到最新服务实例数据之后的处理方式就跟1.x是一样了

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

  • 客户端依然会有服务实例的缓存
  • 定时对比机制也保留了,只不过这个定时对比的机制默认是关闭状态
    之所以默认关闭,主要还是因为长连接还是比较稳定的原因

当客户端出现异常,接收不到请求,那么服务端会直接跟客户端断开连接,当恢复正常,由于有Redo操作,所以还是能拿到最新的实例信息的
所以2.x版本的服务订阅功能的实现大致如下图所示
在这里插入图片描述
注意
1.x版本的时候,任何服务都是可以被订阅的
但在2.x版本中,只支持订阅临时服务,对于永久服务,已经不支持订阅了

1.5.4 服务发现总结

总结:

  • 服务查询1.x是通过Http请求;2.x通过gRPC请求
  • 服务订阅1.x是通过UDP来推送的;2.x就基于gRPC长连接来实现的
  • 1.x2.x客户端都有服务实例的缓存,也有定时对比机制,只不过1.x会自动开启;2.x提供了一个开关,可以手动选择是否开启,默认不开启

1.6 数据一致性

由于Nacos是支持集群模式的,所以一定会涉及到分布式系统中不可避免的数据一致性问题

1.6.1 服务实例的责任机制

再说数据一致性问题之前,先来讨论一下服务实例的责任机制

什么是服务实例的责任机制?
上面提到的服务注册心跳管理监控检查机制,当只有一个Nacos服务时,那么自然而言这个服务会去检查所有的服务实例的心跳时间,执行所有服务实例的健康检查任务
在这里插入图片描述
但是当出现Nacos服务出现集群时,为了平衡各Nacos服务的压力,Nacos会根据一定的规则让每个Nacos服务只管理一部分服务实例的,当然每个Nacos服务的注册表还是全部的服务实例数据
在这里插入图片描述
这个管理机制我给他起了一个名字,就叫做责任机制,因为在1.x和2.x都提到了responsible这个单词
本质就是Nacos服务对哪些服务实例负有心跳监测,健康检查的责任。

1.6.2 CAP定理和BASE理论

点击了解 CAP和BASE理论

1.6.3 Nacos的AP和CP

Nacos其实目前是同时支持APCP
具体使用AP还是CP得取决于Nacos内部的具体功能,并不是有的文章说的可以通过一个配置自由切换。

  • 对于临时实例Nacos会优先保证可用性,也就是AP
  • 对于永久实例Nacos会优先保证数据的一致性,也就是CP

接下来我们就来讲一讲Nacos的CP和AP的实现原理

1.6.3.1 Nacos的AP实现

对于AP来说,Nacos使用的是阿里自研的Distro协议
在这个协议中,每个服务端节点是一个平等的状态,每个服务端节点正常情况下数据是一样的,每个服务端节点都可以接收来自客户端的读写请求
在这里插入图片描述
当某个节点刚启动时,他会向集群中的某个节点发送请求,拉取所有的服务实例数据到自己的服务注册表中
在这里插入图片描述

这样其它客户端就可以从这个服务节点中获取到服务实例数据了
当某个服务端节点接收到注册临时服务实例的请求,不仅仅会将这个服务实例存到自身的服务注册表,同时也会向其它所有服务节点发送请求,将这个服务数据同步到其它所有节点
所以此时从任意一个节点都是可以获取到所有的服务实例数据的。

即使数据同步的过程发生异常,服务实例也成功注册到一个Nacos服务中,对外部而言,整个Nacos集群是可用的,也就达到了AP的效果
同时为了满足BASE理论,Nacos也有下面两种机制保证最终节点间数据最终是一致的:

  • 失败重试机制:指当数据同步给其它节点失败时,会每隔3s重试一次,直到成功
  • 定时对比机制:指每个Nacos服务节点会定时向所有的其它服务节点发送一些认证的请求,这个请求会告诉每个服务节点自己负责的服务实例的对应的版本号,这个版本号随着服务实例的变动就会变动
    如果其它服务节点的数据的版本号跟自己的对不上,那就说明其它服务节点的数据不是最新的
    此时这个Nacos服务节点就会将自己负责的服务实例数据发给不是最新数据的节点,这样就保证了每个节点的数据是一样的了。

1.6.3.2 Nacos的CP实现

NacosCP实现是基于Raft算法来实现的
1.x版本早期,Nacos是自己手动实现Raft算法
2.x版本,Nacos移除了手动实现Raft算法,转而拥抱基于蚂蚁开源的JRaft框架

Raft算法,每个节点主要有三个状态:

  • Leader:负责所有的读写请求,一个集群只有一个
  • Follower:从节点,主要是负责复制Leader的数据,保证数据的一致性
  • Candidate:候选节点,最终会变成Leader或者Follower

集群启动时都是节点Follower,经过一段时间会转换成Candidate状态,再经过一系列复杂的选择算法,选出一个Leader
在这里插入图片描述
当有写请求时,如果请求的节点不是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节点通过一定的机制也是能够保证读到最新的数据

posted @ 2024-01-19 14:36  上善若泪  阅读(303)  评论(0编辑  收藏  举报