临时实例和永久实例
临时实例
在注册到注册中心之后仅仅只保存在服务端内部一个缓存中,不会持久化到磁盘
这个服务端内部的缓存在注册中心届一般被称为服务注册表
当服务实例出现异常或者下线之后,就会把这个服务实例从服务注册表中剔除
永久服务
实例不仅仅会存在服务注册表中,同时也会被持久化到磁盘文件中
当服务实例出现异常或者下线,Nacos 只会将服务实例的健康状态设置为不健康,并不会对将其从服务注册表中剔除
所以这个服务实例的信息你还是可以从注册中心看到,只不过处于不健康状态
为什么 Nacos 要将服务实例分为临时实例和永久实例?
临时实例就比较适合于业务服务,服务下线之后可以不需要在注册中心中查看到
永久实例就比较适合需要运维的服务,这种服务几乎是永久存在的,比如说 MySQL、Redis 等等
-
当然如果你想改成永久实例,可以通过下面这个配置项来完成
spring cloud: nacos: discovery: #ephemeral单词是临时的意思,设置成false,就是永久实例了 ephemeral: false
1.x 版本和2.x版本的区别
在 1.x 版本中,一个服务中可以既有临时实例也有永久实例,服务实例是永久还是临时是由服务实例本身决定的
但是 2.x 版本中,一个服务中的所有实例要么都是临时的要么都是永久的,是由服务决定的,而不是具体的服务实例
服务注册
作为一个服务注册中心,服务注册肯定是一个非常重要的功能
所谓的服务注册,就是通过注册中心提供的客户端 SDK(或者是控制台)将服务本身的一些元信息,比如 ip、端口等信息发送到注册中心服务端
服务端在接收到服务之后,会将服务的信息保存到前面提到的服务注册表中
1.x 版本的实现
服务注册是通过 Http 接口实现的,Nacos 服务端本身就是用 SpringBoot 写的
代码如下
2.x 版本的实现
2.x 版本相比于 1.x 版本最主要的升级就是客户端和服务端通信协议的改变,由 1.x 版本的 Http 改成了 2.x 版本 gRPC
之所以改成了 gRPC,主要是因为 Http 请求会频繁创建和销毁连接,白白浪费资源
所以在 2.x 版本之后,为了提升性能,就将通信协议改成了 gRPC
根据官网显示,整体的效果还是很明显,相比于 1.x 版本,注册性能总体提升至少 2 倍
具体实现
Nacos 客户端在启动的时候,会通过 gRPC 跟服务端建立长连接
当客户端发起注册的时候,就会通过这个长连接,将服务实例的信息发送给服务端
服务端拿到服务实例,跟 1.x 一样,也会存到服务注册表
除了注册之外,当注册的是临时实例时,2.x 还会将服务实例信息存储到客户端中的一个缓存中,供 Redo 操作
所谓的 Redo 操作,其实就是一个补偿机制,本质是个定时任务,默认每 3s 执行一次
这个定时任务作用是,当客户端与服务端重新建立连接时(因为一些异常原因导致连接断开)
那么之前注册的服务实例肯定还要继续注册服务端(断开连接服务实例就会被剔除服务注册表)
所以这个 Redo 操作一个很重要的作用就是重连之后的重新注册的作用
除了注册之外,比如服务订阅之类的操作也需要 Redo 操作,当连接重新建立,之前客户端的操作都需要 Redo 一下
心跳机制(直接解决了临时实例的心跳机制)
心跳机制,也可以被称为保活机制,它的作用就是服务实例告诉注册中心我这个服务实例还活着
在正常情况下,服务关闭了,那么服务会主动向 Nacos 服务端发送一个服务下线的请求
Nacos 服务端在接收到请求之后,会将这个服务实例从服务注册表中剔除
但是对于异常情况下,比如出现网络问题,可能导致这个注册的服务实例无法提供服务,处于不可用状态,也就是不健康
而此时在没有任何机制的情况下,服务端是无法知道这个服务处于不可用状态
所以为了避免这种情况,一些注册中心,就比如 Nacos、Eureka,就会用心跳机制来判断这个服务实例是否能正常
在 Nacos 中,心跳机制仅仅是针对临时实例来说的,临时实例需要靠心跳机制来保活
1.x 心跳实现
在 1.x 中,心跳机制实现是通过客户端和服务端各存在的一个定时任务来完成的
在服务注册时,发现是临时实例,客户端会开启一个 5s 执行一次的定时任务
这个定时任务会构建一个 Http 请求,携带这个服务实例的信息,然后发送到服务端
在 Nacos 服务端也会开启一个定时任务,默认也是 5s 执行一次,去检查这些服务实例最后一次心跳的时间,也就是客户端最后一次发送 Http 请求的时间
-
当最后一次心跳时间超过 15s,但没有超过 30s,会把这服务实例标记成不健康
-
当最后一次心跳超过 30s,直接把服务从服务注册表中剔除
1.x 版本的心跳机制,本质就是两个定时任务
其实 1.x 的这个心跳还有一个作用,就是跟上一节说的 gRPC 时 Redo 操作的作用是一样的
服务在处理心跳的时候,发现心跳携带这个服务实例的信息在注册表中没有,此时就会添加到服务注册表
所以心跳也有 Redo 的类似效果
2.x 心跳实现(兼容1.x版本心跳机制,如果客户端使用的SDK是1.x的情况下)
在 2.x 版本之后,由于通信协议改成了 gRPC,客户端与服务端保持长连接,所以 2.x 版本之后它是利用这个 gRPC 长连接本身的心跳来保活
一旦这个连接断开,服务端就会认为这个连接注册的服务实例不可用,之后就会将这个服务实例从服务注册表中提出剔除
除了连接本身的心跳之外,Nacos 还有服务端的一个主动检测机制
Nacos 服务端也会启动一个定时任务,默认每隔 3s 执行一次
这个任务会去检查超过 20s 没有发送请求数据的连接
一旦发现有连接已经超过 20s 没发送请求,那么就会向这个连接对应的客户端发送一个请求
如果请求不通或者响应失败,此时服务端也会认为与客户端的这个连接异常,从而将这个客户端注册的服务实例从服务注册表中剔除
所以对于 2.x 版本,主要是两种机制来进行保活:
-
基于gRPC长连接双向活跃检测:gRPC 连接未立即断开,但数据无法完整往返
-
客户端进程卡死但心跳线程存活(假心跳场景) 例如:应用主线程死锁,但心跳线程仍能发送TCP包,此时服务实际不可用,但心跳正常。
-
网络隔离(Network Partition)(假心跳场景) 客户端与Nacos Server之间出现单向网络故障(客户端能发数据,但收不到响应)。
-
-
Nacos 主动检查机制(连接存活但不活跃),服务端会对 20s 没有发送数据的连接进行检查,出现异常时也会主动断开连接,剔除服务实例
健康检查(为解决永久实例的心跳机制)
心跳机制仅仅是临时实例用来保护的机制
而对于永久实例来说,一般来说无法主动上报心跳
就比如说 MySQL 实例,肯定是不会主动上报心跳到 Nacos 的,所以这就导致无法通过心跳机制来保活
所以针对永久实例的情况,Nacos 通过一种叫健康检查的机制去判断服务实例是否活着
健康检查跟心跳机制刚好相反,心跳机制是服务实例向服务端发送请求
而所谓的健康检查就是服务端主动向服务实例发送请求,去探测服务实例是否活着
健康检查机制在 1.x 和 2.x 的实现机制是一样的
Nacos 服务端在会去创建一个健康检查任务,这个任务每次执行时间间隔会在 2000~7000 毫秒之间
当任务触发的时候,会根据设置的健康检查的方式执行不同的逻辑,目前主要有以下三种方式:
-
TCP
-
根据服务实例的 ip 和端口去判断是否能连接成功,如果连接成功,就认为健康,反之就任务不健康
-
-
HTTP
-
向服务实例的 ip 和端口发送一个 Http 请求,请求路径是需要设置的,如果能正常请求,说明实例健康,反之就不健康
-
-
MySQL
-
一种特殊的检查方式,他可以执行下面这条 Sql 来判断数据库是不是主库
-
默认情况下,都是通过 TCP 的方式来探测服务实例是否还活着
服务发现
所谓的服务发现就是指当有服务实例注册成功之后,其它服务可以发现这些服务实例
Nacos 提供了两种发现方式:
-
主动查询
-
指客户端主动向服务端查询需要关注的服务实例,也就是拉(pull)的模式
-
-
服务订阅
-
指客户端向服务端发送一个订阅服务的请求,当被订阅的服务有信息变动就会主动将服务实例的信息推送给订阅的客户端,本质就是推(push)模式
-
在我们平时使用时,一般来说都是选择使用订阅的方式,这样一旦有服务实例数据的变动,客户端能够第一时间感知
并且 Nacos 在整合 SpringCloud 的时候,默认就是使用订阅的方式
对于这两种服务发现方式,1.x 和 2.x 版本实现也是不一样
服务(主动)查询
1.x 整体就是发送 Http 请求去查询服务实例,2.x 只不过是将 Http 请求换成了 gRPC 的请求
服务端对于查询的处理过程都是一样的,从服务注册表中查出符合查询条件的服务实例进行返回
服务订阅
不过对于服务订阅,两者的机制就稍微复杂一点
不论是 1.x 还是 2.x 都是通过 SDK 中的NamingService#subscribe
方法来发起订阅的
当有服务实例数据变动的时,客户端就会回调EventListener
,就可以拿到最新的服务实例数据了
1.x服务发现订阅实现
-
客户端在启动的时候,会去构建一个叫 PushReceiver 的类
-
这个类会去创建一个 UDP Socket,端口是随机的
-
作用:通过 UDP 的方式接收服务端推送的数据的
-
-
调用
NamingService#subscribe
来发起订阅时,会先去服务端查询需要订阅服务的所有实例信息之后会将所有服务实例数据存到客户端的一个内部缓存中-
并且在查询的时候,会将这个 UDP Socket 的端口作为一个参数传到服务端
-
服务端接收到这个 UDP 端口后,后续就通过这个端口给客户端推送服务实例数据
-
-
会为这次订阅开启一个不定时执行的任务
-
之所以不定时,是因为这个当执行异常的时候,下次执行的时间间隔就会变长,但是最多不超过 60s,正常是 10s,这个 10s 是查询服务实例是服务端返回的
-
这个任务会去从服务端查询订阅的服务实例信息,然后更新内部缓存
-
既然有了服务变动推送的功能,为什么还要定时去查询更新服务实例信息呢?
-
那就是因为 UDP 通信不稳定导致的
-
虽然有 Push,但是由于 UDP 通信自身的不确定性,有可能会导致客户端接收变动信息失败
-
所以这里就加了一个定时任务,弥补这种可能性,属于一个兜底的方案。
-
-
2.x服务发现订阅实现
由于 2.x 版本换成了 gRPC 长连接的方式,所以 2.x 版本服务数据变更推送已经完全抛弃了 1.x 的 UDP 做法
当有服务实例变动的时候,服务端直接通过这个长连接将服务信息发送给客户端
客户端拿到最新服务实例数据之后的处理方式就跟 1.x 是一样了
除了处理方式一样,2.x 也继承了 1.x 的其他的东西
比如客户端依然会有服务实例的缓存
定时对比机制也保留了,只不过这个定时对比的机制默认是关闭状态
之所以默认关闭,主要还是因为长连接还是比较稳定的原因
当客户端出现异常,接收不到请求,那么服务端会直接跟客户端断开连接
当恢复正常,由于有 Redo 操作,所以还是能拿到最新的实例信息的
细节
在 1.x 版本的时候,任何服务都是可以被订阅的
但是在 2.x 版本中,只支持订阅临时服务,对于永久服务,已经不支持订阅了
数据一致性
由于 Nacos 是支持集群模式的,所以一定会涉及到分布式系统中不可避免的数据一致性问题
服务实例的责任机制
什么是服务实例的责任机制?
比如上面提到的服务注册、心跳管理、监控检查机制,当只有一个 Nacos 服务时,那么自然而言这个服务会去检查所有的服务实例的心跳时间,执行所有服务实例的健康检查任务
但是当出现 Nacos 服务出现集群时,为了平衡各 Nacos 服务的压力,Nacos 会根据一定的规则让每个 Nacos 服务只管理一部分服务实例的
当然每个 Nacos 服务的注册表还是全部的服务实例数据
这个管理机制我给他起了一个名字,就叫做责任机制,因为我在 1.x 和 2.x 都提到了responsible这个单词
本质就是 Nacos 服务对哪些服务实例负有心跳监测,健康检查的责任。
BASE 理论(CAP妥协之后的产物)
-
基本可用(Basically Available):系统出现故障还是能够对外提供服务,不至于直接无法用了
-
软状态(Soft State):允许各个节点的数据短暂的不一致
-
最终一致性,(Eventually Consistent):虽然允许各个节点的数据不一致,但是在一定时间之后,各个节点的数据最终需要一致的
Nacos 的 AP 和 CP
Nacos 其实目前是同时支持 AP 和 CP 的
具体使用 AP 还是 CP 得取决于 Nacos 内部的具体功能,并不是有的文章说的可以通过一个配置自由切换。
就以服务注册举例来说,对于临时实例来说,Nacos 会优先保证可用性,也就是 AP
对于永久实例,Nacos 会优先保证数据的一致性,也就是 CP
Nacos 的 AP 实现
对于 AP 来说,Nacos 使用的是阿里自研的 Distro 协议
在这个协议中,每个服务端节点是一个平等的状态,每个服务端节点正常情况下数据是一样的,每个服务端节点都可以接收来自客户端的读写请求
当某个节点刚启动时,他会向集群中的某个节点发送请求,拉取所有的服务实例数据到自己的服务注册表中
这样其它客户端就可以从这个服务节点中获取到服务实例数据了
当某个服务端节点接收到注册临时服务实例的请求,不仅仅会将这个服务实例存到自身的服务注册表,同时也会向其它所有服务节点发送请求,将这个服务数据同步到其它所有节点
所以此时从任意一个节点都是可以获取到所有的服务实例数据的。
即使数据同步的过程发生异常,服务实例也成功注册到一个 Nacos 服务中,对外部而言,整个 Nacos 集群是可用的,也就达到了 AP 的效果
同时为了满足 BASE 理论,Nacos 也有下面两种机制保证最终节点间数据最终是一致的:
-
失败重试机制
-
数据同步给其它节点失败时,会每隔 3s 重试一次,直到成功
-
-
定时对比机制
-
每个 Nacos 服务节点会定时向所有的其它服务节点发送一些认证的请求
-
这个请求会告诉每个服务节点自己负责的服务实例的对应的版本号,这个版本号随着服务实例的变动就会变动
-
如果其它服务节点的数据的版本号跟自己的对不上,那就说明其它服务节点的数据不是最新的
-
此时这个 Nacos 服务节点就会将自己负责的服务实例数据发给不是最新数据的节点,这样就保证了每个节点的数据是一样的了。
-
Nacos 的 CP 实现
Nacos 的 CP 实现是基于 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 认为这次写数据是失败的,就不会处理写请求,直接返回给客户端请求失败
小细节需要注意
Nacos 在处理查询服务实例的请求直接时,并不会将请求转发给 Leader 节点处理,而是直接查当前 Nacos 服务实例的注册表
这其实就会引发一个问题
如果客户端查询的 Follower 节点没有及时处理 Leader 同步过来的写请求(过半响应的节点中不包括这个节点),此时在这个 Follower 其实是查不到最新的数据的,这就会导致数据的不一致
所以说,虽然 Raft 协议规定要求从 Leader 节点查最新的数据,但是 Nacos 至少在读服务实例数据时并没有遵守这个协议
当然对于其它的一些数据的读写请求有的还是遵守了这个协议。
数据模型
在 Nacos 中,一个服务的确定是由三部分信息确定
-
命名空间(Namespace):多租户隔离用的,默认是
public
-
分组(Group):这个其实可以用来做环境隔离,服务注册时可以指定服务的分组,比如是测试环境或者是开发环境,默认是
DEFAULT_GROUP
-
服务名(ServiceName):这个就不用多说了
在服务注册和订阅的时候,必须要指定上述三部分信息,如果不指定,Nacos 就会提供默认的信息
不过,在 Nacos 中,在服务里面其实还是有一个集群的概念
在服务注册的时候,可以指定这个服务实例在哪个集体的集群中,默认是在DEFAULT
集群下
在 SpringCloud 环境底下可以通过如下配置去设置
spring
cloud:
nacos:
discovery:
cluster-name: sanyoujavaCluster
配置中心
Spring Boot 应用
↓
Spring Cloud Nacos Config
↓
ConfigService(客户端核心类)
↓
LongPollingRunnable(长轮询任务)
↓
HTTP 请求 /nacos/v1/cs/configs/listener
↓
Nacos Server 检测配置变化
↓
返回新配置 → 客户端触发 Listener → 事件驱动更新 Bean
Nacos 配置中心是支持配置项自动刷新的,而其实现的原理是通过长轮询+事件驱动+本地回调机制的方式来实现的,具体来说:
-
客户端向 Nacos 服务器发送一个带有监听器(Listener)的请求,以获取某个特定配置的值。
-
Nacos 服务器接收到请求后,会检查该配置是否发生了变化。如果没有变化,则该请求将被阻塞,直到超时或配置发生变化。
-
当配置发生变化时,Nacos 服务器会立即响应,并将新的配置值返回给客户端。
-
客户端接收到新的配置值后,可以根据需要更新自身的配置。
长轮询:服务器端接收到客户端的请求之后,如果没有数据更新,则连接保持一段时间,直到有数据或者超时才会返回。
gRPC 长连接是 Nacos 2.x 的推荐通信方式,性能更优,但为保证兼容性、适配多语言客户端和轻量场景,Nacos 仍然保留了 HTTP 长轮询机制。两者可以共存,动态选择,适配更广泛的实际业务场景。
机制 | 说明 |
长轮询 | 保证实时监听变化,服务端主动推送 |
客户端本地缓存 | 保证容错、降级能力 |
Bean 自动刷新 | 与 Spring 深度集成,支持注解级别的动态刷新 |
异步监听线程 | 保证主线程业务不被阻塞 |
如何集成 Nacos Config 实现配置项动态刷新?
使用 @NacosValue
注解注入配置
import com.alibaba.nacos.api.config.annotation.NacosValue;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
@Component
public class NacosValueExample {
@NacosValue(value = "${my.config.value}", autoRefreshed = true)
private String configValue;
@PostConstruct
public void init() {
System.out.println("读取到配置:" + configValue);
}
public String getConfigValue() {
return configValue;
}
}
使用 @NacosConfigListener
手动监听配置变更
import com.alibaba.nacos.api.config.annotation.NacosConfigListener;
import org.springframework.stereotype.Component;
@Component
public class NacosListenerExample {
@NacosConfigListener(dataId = "my-config.yaml", groupId = "DEFAULT_GROUP")
public void onChange(String newConfig) {
System.out.println("配置发生变化,新内容为:" + newConfig);
// 你可以在这里解析 YAML 并手动更新 Bean 或缓存
}
}
使用 @ConfigurationProperties
+ @RefreshScope
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;
@Component
@RefreshScope
@ConfigurationProperties(prefix = "my.config")
public class ConfigPropertiesExample {
private String value;
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
使用 bootstrap.yml
配置动态读取 Nacos 的配置
# bootstrap.yml
spring:
application:
name: nacos-demo # 用作默认的 dataId:nacos-demo.yaml
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848
file-extension: yaml
group: DEFAULT_GROUP
namespace: public
refresh-enabled: true # 开启全局自动刷新
extension-configs:
- data-id: my-config.yaml
group: DEFAULT_GROUP
refresh: true # 支持动态刷新
配置项作用
server-addrNacos 服务地址
file-extension默认 dataId 后缀,比如 nacos-demo.yaml
refresh-enabled全局开启动态刷新
extension-configs可以加载多个额外配置文件
refresh: true为该配置开启动态刷新
Spring Boot 启动阶段读取 bootstrap.yml
,初始化 Spring Cloud Nacos。
从配置中心读取 dataId
对应的配置(如 my-config.yaml
),并注入到环境中。
如果设置了 refresh: true
,则在配置变更时,Nacos 会通过监听机制自动刷新对应的 Bean。
-
如果你用的是
@RefreshScope
或@NacosValue(autoRefreshed = true)
,对应字段会自动更新。 -
也可以通过
/actuator/refresh
手动刷新(Spring Cloud 原生方式)。