it编程 > 编程语言 > Asp.net

Nacos注册中心和配置中心的底层原理全面解读

18人参与 2025-06-06 Asp.net

临时实例和永久实例

临时实例

在注册到注册中心之后仅仅只保存在服务端内部一个缓存中,不会持久化到磁盘

这个服务端内部的缓存在注册中心届一般被称为服务注册表

当服务实例出现异常或者下线之后,就会把这个服务实例从服务注册表中剔除

永久服务

实例不仅仅会存在服务注册表中,同时也会被持久化到磁盘文件

当服务实例出现异常或者下线,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 请求的时间

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 连接未立即断开,但数据无法完整往返

nacos 主动检查机制(连接存活但不活跃),服务端会对 20s 没有发送数据的连接进行检查,出现异常时也会主动断开连接,剔除服务实例

健康检查(为解决永久实例的心跳机制)

心跳机制仅仅是临时实例用来保护的机制

而对于永久实例来说,一般来说无法主动上报心跳

就比如说 mysql 实例,肯定是不会主动上报心跳到 nacos 的,所以这就导致无法通过心跳机制来保活

所以针对永久实例的情况,nacos 通过一种叫健康检查的机制去判断服务实例是否活着

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

而所谓的健康检查就是服务端主动向服务实例发送请求,去探测服务实例是否活着

健康检查机制在 1.x 和 2.x 的实现机制是一样的

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

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

tcp

http

mysql

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

服务发现

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

nacos 提供了两种发现方式:

主动查询

服务订阅

在我们平时使用时,一般来说都是选择使用订阅的方式,这样一旦有服务实例数据的变动,客户端能够第一时间感知

并且 nacos 在整合 springcloud 的时候,默认就是使用订阅的方式

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

服务(主动)查询

1.x 整体就是发送 http 请求去查询服务实例,2.x 只不过是将 http 请求换成了 grpc 的请求

服务端对于查询的处理过程都是一样的,从服务注册表中查出符合查询条件的服务实例进行返回

服务订阅

不过对于服务订阅,两者的机制就稍微复杂一点

不论是 1.x 还是 2.x 都是通过 sdk 中的namingservice#subscribe方法来发起订阅的

当有服务实例数据变动的时,客户端就会回调eventlistener,就可以拿到最新的服务实例数据了

1.x服务发现订阅实现

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

调用namingservice#subscribe来发起订阅时,会先去服务端查询需要订阅服务的所有实例信息之后会将所有服务实例数据存到客户端的一个内部缓存中

会为这次订阅开启一个不定时执行的任务

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

这个任务会去从服务端查询订阅的服务实例信息,然后更新内部缓存

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

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妥协之后的产物)

nacos 的 ap 和 cp

nacos 其实目前是同时支持 ap 和 cp 的

具体使用 ap 还是 cp 得取决于 nacos 内部的具体功能,并不是有的文章说的可以通过一个配置自由切换。

就以服务注册举例来说,对于临时实例来说,nacos 会优先保证可用性,也就是 ap

对于永久实例,nacos 会优先保证数据的一致性,也就是 cp

nacos 的 ap 实现

对于 ap 来说,nacos 使用的是阿里自研的 distro 协议

在这个协议中,每个服务端节点是一个平等的状态,每个服务端节点正常情况下数据是一样的,每个服务端节点都可以接收来自客户端的读写请求

当某个节点刚启动时,他会向集群中的某个节点发送请求,拉取所有的服务实例数据到自己的服务注册表中

这样其它客户端就可以从这个服务节点中获取到服务实例数据了

当某个服务端节点接收到注册临时服务实例的请求,不仅仅会将这个服务实例存到自身的服务注册表,同时也会向其它所有服务节点发送请求,将这个服务数据同步到其它所有节点

所以此时从任意一个节点都是可以获取到所有的服务实例数据的。

即使数据同步的过程发生异常,服务实例也成功注册到一个 nacos 服务中,对外部而言,整个 nacos 集群是可用的,也就达到了 ap 的效果

同时为了满足 base 理论,nacos 也有下面两种机制保证最终节点间数据最终是一致的:

失败重试机制

定时对比机制

nacos 的 cp 实现

nacos 的 cp 实现是基于 raft 算法来实现

在 1.x 版本早期,nacos 是自己手动实现 raft 算法

在 2.x 版本,nacos 移除了手动实现 raft 算法,转而拥抱基于蚂蚁开源的 jraft 框架

在 raft 算法,每个节点主要有三个状态

集群启动时都是节点 follower,经过一段时间会转换成 candidate 状态,再经过一系列复杂的选择算法,选出一个 leader

当有写请求时,如果请求的节点不是 leader 节点时,会将请求转给 leader 节点,由 leader 节点处理写请求

比如,有个客户端连到的上图中的nacos服务2节点,之后向nacos服务2注册服务

nacos服务2接收到请求之后,会判断自己是不是 leader 节点,发现自己不是

此时nacos服务2就会向 leader 节点发送请求,leader 节点接收到请求之后,会处理服务注册的过程

为什么说 raft 是保证 cp 的呢?

主要是因为 raft 在处理写的时候有一个判断过程

小细节需要注意

nacos 在处理查询服务实例的请求直接时,并不会将请求转发给 leader 节点处理,而是直接查当前 nacos 服务实例的注册表

这其实就会引发一个问题

如果客户端查询的 follower 节点没有及时处理 leader 同步过来的写请求(过半响应的节点中不包括这个节点),此时在这个 follower 其实是查不到最新的数据的,这就会导致数据的不一致

所以说,虽然 raft 协议规定要求从 leader 节点查最新的数据,但是 nacos 至少在读服务实例数据时并没有遵守这个协议

当然对于其它的一些数据的读写请求有的还是遵守了这个协议。

数据模型

在 nacos 中,一个服务的确定是由三部分信息确定

在服务注册和订阅的时候,必须要指定上述三部分信息,如果不指定,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 配置中心是支持配置项自动刷新的,而其实现的原理是通过轮询+事件驱动+本地回调机制的方式来实现的,具体来说:

长轮询:服务器端接收到客户端的请求之后,如果没有数据更新,则连接保持一段时间,直到有数据或者超时才会返回。

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。

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持代码网。

(0)

您想发表意见!!点此发布评论

推荐阅读

C#封装HttpClient实现HTTP请求处理

06-06

C#实现Struct结构体与IntPtr转换的示例详解

06-05

C#提取文件时间戳实现实现与性能优化

06-08

C#使用BarcodeLib生成条形码的完整代码

06-04

C#代码实现解析WTGPS和BD数据

06-04

C#基于Whisper.net实现语音识别功能的示例详解

06-03

猜你喜欢

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论