Skip to content

Nacos 详解:注册中心 + 配置中心

Nacos(Dynamic Naming and Configuration Service)是阿里巴巴开源的一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。在 Spring Cloud Alibaba 生态中,Nacos 扮演着注册中心和配置中心的双重角色。

一、为什么需要注册中心 和 配置中心?

1.1 为什么需要注册中心?

  在微服务架构中,服务实例的数量和位置是动态变化的。今天可能 3 个实例,明天扩容到 10 个;今天在 192.168.1.10 上,明天可能因为容器重启迁移到了 192.168.1.99。如果服务之间直接通过硬编码的 IP + 端口来调用,每一次实例变化都意味着一次代码修改、一次重新部署——这在微服务集群中是完全不可接受的。

没有注册中心时:

order-service 需要调用 user-service

order-service 的 application.yml:
  user-service:
    urls:
      - http://192.168.1.10:8080
      - http://192.168.1.11:8080
      - http://192.168.1.12:8080    ← 硬编码 IP,实例变了就得改配置

问题:
  1. user-service 扩容到 5 个实例 → 改配置,重启 order-service
  2. 192.168.1.10 宕机 → order-service 仍然会尝试调用,直到超时
  3. 微服务数量增加 → 每对调用关系都要维护一堆 IP 列表
  4. 无法动态感知实例上下线

有了注册中心后:

order-service 调用 user-service 时,只需要知道服务名:

order-service:
  @FeignClient(name = "user-service")    ← 只写服务名,不写 IP
  UserServiceClient userServiceClient;

注册中心(Nacos)负责:
  ┌─────────────────────────────────────────────┐
  │  user-service 注册表                         │
  │  ┌──────────────────────────────────────┐   │
  │  │ 192.168.1.10:8080  healthy:true      │   │
  │  │ 192.168.1.11:8080  healthy:true      │   │
  │  │ 192.168.1.12:8080  healthy:true      │   │
  │  │ 192.168.1.13:8080  healthy:false     │   │  ← 不健康,不返回
  │  └──────────────────────────────────────┘   │
  └─────────────────────────────────────────────┘

  1. 扩容 → 新实例自动注册,order-service 自动感知
  2. 宕机 → 心跳超时,15s 标记不健康,30s 剔除
  3. 调用 → 从注册中心拿到健康实例列表,负载均衡选一个
  4. 无需硬编码任何 IP

注册中心的核心价值:

价值说明
服务发现服务消费者通过服务名即可找到提供者,无需关心 IP 和端口
动态感知实例上下线实时感知,自动更新调用列表
健康检查自动剔除不健康实例,保证调用成功率
负载均衡从健康实例列表中按策略选择,分摊流量
元数据管理实例可携带版本号、权重、机房等元数据,支持灰度路由
解耦调用方调用方只依赖服务名,不依赖实例的具体位置

1.2 为什么需要配置中心?

  在微服务架构中,一个系统往往由几十上百个服务组成,每个服务都有自己的配置文件(application.yml)。这些配置中包含数据库连接、Redis 地址、第三方 API Key、业务开关、限流阈值等各类信息。如果不做集中管理,配置的维护会成为一场噩梦。

没有配置中心时:

场景:数据库密码变更

  1. 修改 order-service 的 application.yml       ← 改第 1 个
  2. 修改 user-service 的 application.yml        ← 改第 2 个
  3. 修改 product-service 的 application.yml     ← 改第 3 个
  4. 修改 payment-service 的 application.yml     ← 改第 4 个
  ... 共 30 个服务 ...
  30. 修改 xxx-service 的 application.yml        ← 改第 30 个

  然后:
  ├── 重新打包 30 个服务
  ├── 重新部署 30 个服务
  └── 漏改了一个 → 生产事故

问题:
  1. 配置分散 → 修改一个配置要改几十个文件
  2. 修改需重启 → 哪怕只改一个开关,也要重启整个服务
  3. 无版本追溯 → 不知道谁在什么时候改了什么
  4. 配置泄露 → 密码明文写在配置文件中,所有人可见
  5. 环境差异 → dev / test / prod 三套配置,极易搞混

有了配置中心后:

场景:数据库密码变更

Nacos 配置中心:
  ┌──────────────────────────────────────────────┐
  │  Namespace: prod                              │
  │  ┌──────────────────────────────────────────┐ │
  │  │  Data ID: order-service.yml              │ │
  │  │  Data ID: user-service.yml               │ │
  │  │  Data ID: common-datasource.yml  ← 公共配置 │ │
  │  │    spring.datasource.url=jdbc:mysql://...  │ │
  │  │    spring.datasource.password=新密码        │ │  ← 只改这一处
  │  └──────────────────────────────────────────┘ │
  └──────────────────────────────────────────────┘

  1. 在 Nacos 控制台修改 common-datasource.yml 的密码
  2. 所有引用此配置的 30 个服务自动收到推送
  3. 配置刷新,无需重启
  4. 操作审计日志记录:谁、什么时间、改了什么

配置中心的核心价值:

价值说明
集中管理所有服务的配置统一存放在配置中心,一处修改,全局生效
动态刷新配置变更后实时推送到客户端,无需重启服务
环境隔离通过 Namespace 隔离 dev / test / prod,彻底杜绝环境混用
版本追溯每次修改都有历史记录,出问题可快速回滚到任意历史版本
权限管控敏感配置(密码、密钥)加密存储,操作需要权限审批
灰度发布配置可按 IP、标签灰度下发,先让部分实例验证,再全量推送
公共配置抽取多个服务共享的配置(如数据库连接、Redis 地址)抽取为公共配置,一处维护

1.3 注册中心 + 配置中心:Nacos 的二合一优势

  在 Nacos 出现之前,Spring Cloud 微服务体系通常需要同时部署 Eureka(注册中心)和 Spring Cloud Config + Bus(配置中心)两套组件。这意味着两套集群、两套运维、两套监控——架构复杂度和运维成本翻倍。

  Nacos 将注册中心和配置中心合二为一,一个服务同时扮演两个角色,从根本上简化了架构:

传统方案:两套组件                  Nacos 方案:一套搞定
                                  
  Eureka 集群(注册中心)            ┌─────────────────────────┐
  ┌──────┐ ┌──────┐ ┌──────┐       │      Nacos 集群          │
  │Node1 │ │Node2 │ │Node3 │       │  ┌──────────┬──────────┐ │
  └──────┘ └──────┘ └──────┘       │  │ 注册中心  │ 配置中心  │ │
                                   │  └──────────┴──────────┘ │
  Config Server 集群(配置中心)      │  ┌──────┐ ┌──────┐ ┌───┐ │
  ┌──────┐ ┌──────┐ ┌──────┐       │  │Node1 │ │Node2 │ │...│ │
  │Node1 │ │Node2 │ │Node3 │       │  └──────┘ └──────┘ └───┘ │
  └──────┘ └──────┘ └──────┘       └─────────────────────────┘

  运维成本:2 套集群                   运维成本:1 套集群
  学习成本:2 套 API                  学习成本:1 套 API
  一致性:各自独立                   一致性:同一套 Raft 协议
对比维度Eureka + ConfigNacos
组件数量2 个(Eureka + Config Server)1 个
运维复杂度高,两套集群分别维护低,一套集群统一管理
一致性协议AP(Eureka) + Git/DB(Config)AP + CP 可切换,统一 Raft 协议
动态刷新需 Spring Cloud Bus + MQ原生支持,gRPC 流式推送
配置回滚依赖 Git 历史原生支持,一键回滚
控制台无(Eureka 只有 Dashboard)统一控制台,注册 + 配置一站式管理
社区活跃度Eureka 已停维,Config 仍在维护阿里巴巴持续投入,社区活跃

二、Nacos 架构

Nacos 架构

三、Nacos 作为注册中心

3.1 核心概念

概念说明
Namespace命名空间,用于租户隔离。不同环境(dev/test/prod)使用不同 namespace
Group服务分组,同一个 namespace 内可按业务分组
Service服务,一个微服务的逻辑标识,如 order-service
Instance实例,一个服务的一个具体运行节点,含 IP + Port

3.2 注册流程

  Nacos 客户端的服务注册流程是这样的: Spring Boot 应用启动后,内嵌的 Tomcat 容器初始化完成并成功绑定端口,这时 Spring 容器会发布一个 WebServerInitializedEvent 事件,标志着 Web 服务已经准备就绪。Nacos 客户端的自动配置类里有一个监听器,专门监听这个事件,事件一触发就开始执行注册逻辑。

  监听器本身不处理具体的注册细节,而是把任务委托给 NacosServiceRegistry 服务注册器,调用它的 register 方法,同时把封装了服务注册信息的 NacosRegistration 对象传进去。NacosRegistration 里装着所有需要的注册参数,包括服务名、服务的 IP 地址、端口号、权重、健康状态、分组名、命名空间,还有自定义的元数据等等。

  服务注册器拿到这些信息后,会进一步调用 NamingService 命名服务的 registerInstance 方法,这是 Nacos 客户端的核心 API。NamingService 内部会先构建一个 Instance 实例对象,把 IP、端口、权重这些属性都设进去,然后把这些参数组装成 HTTP 请求需要的键值对格式。

  参数准备好之后,NamingService 就会调用底层的 HTTP 客户端,向 Nacos 服务端发送一个 POST 请求,请求地址是 /nacos/v1/ns/instance,把所有注册参数都带过去。这是整个注册流程中唯一一次跨网络的交互,之前的所有步骤都在同一个 JVM 进程内部完成。

  Nacos 服务端收到请求后,校验参数、创建服务、把实例加入内存注册表、持久化到磁盘,然后返回一个 200 OK 的响应,表示注册成功。HTTP 客户端把响应结果返回给 NamingService。

  注册成功之后,NamingService 还会调用 BeatReactor 心跳反应器的 addBeatInfo 方法,把当前服务的心跳信息添加进去。BeatReactor 内部会启动一个定时任务,每隔 5 秒就向 Nacos 服务端发送一次心跳请求,告诉服务端这个实例还活着。如果超过 15 秒没收到心跳,服务端就会把实例标记为不健康;超过 30 秒没收到心跳,就会直接把实例从服务列表里摘除。

  心跳任务启动后,NamingService 把注册成功的结果逐层返回,先返回给 NacosServiceRegistry,再返回给最开始的监听器,整个服务注册流程就完成了。 注册流程

3.3 服务发现流程

  Nacos 服务端收到客户端发来的注册请求后,首先会校验请求参数,检查服务名、IP 地址、端口号这些必填项是否都有、格式是否正确。校验通过后,就把这个服务实例的信息存到内存里的 Map 结构中,这样后续查询的时候速度快。存完内存,还会把数据写入磁盘做持久化,防止 Nacos 重启后数据丢失。接着通过 Distro 协议把这份数据同步给集群里的其他 Nacos 节点,保证整个集群的数据一致。然后再通过 UDP 协议,把服务变更的消息推送给所有订阅了这个服务的消费者,告诉它们服务列表有更新。最后,服务端给客户端返回一个 200 OK 的响应,表示注册成功。 服务发现流程

3.4 注册与发现完整流程图和时序图

注册与发现完整流程 注册与发现时序图

注册与发现时序图

3.5 消费者服务发现与注册表一致性

  消费者(服务调用方)如何拉取注册表、如何保证本地缓存的注册表与 Nacos 服务端一致,是服务发现中最核心的问题。Nacos 通过 拉取 + 推送 + 版本比对 三重机制,确保消费者拿到的实例列表始终是最新的。

3.5.1 消费者拉取机制

  消费者获取服务实例列表有两种触发方式:主动拉取(Pull)被动接收推送(Push)。Nacos 将两者结合,形成了一套高效的同步机制。

(1)首次全量拉取

  消费者启动后,第一次调用某个服务时,会向 Nacos 服务端发送一个 GET 请求,拉取该服务的完整实例列表。请求路径为 /nacos/v1/ns/instance/list,参数包括服务名、分组名、命名空间等。Nacos 服务端收到请求后,从内存注册表中查出该服务下所有实例,一次性返回给消费者。消费者将这份实例列表存入本地缓存,后续调用直接从缓存中获取,无需每次都访问 Nacos 服务端。

消费者启动 → 首次调用服务A

    ├── GET /nacos/v1/ns/instance/list?serviceName=service-A

    ├── Nacos 返回全量实例列表
    │     [{ip:192.168.1.10, port:8080, healthy:true, weight:1.0},
    │      {ip:192.168.1.11, port:8080, healthy:true, weight:0.8},
    │      {ip:192.168.1.12, port:8080, healthy:false, weight:1.0}]

    ├── 存入本地缓存 ConcurrentHashMap

    └── 后续调用直接从本地缓存获取,不再请求 Nacos

(2)定时拉取(兜底轮询)

  即使有推送机制,消费者仍然会每隔一段时间主动向 Nacos 拉取一次全量注册表,作为兜底保障。Nacos 1.x 默认间隔为 10 秒,Nacos 2.x 中这个间隔更长(默认约 30 秒),因为 gRPC 长连接推送已经足够可靠,定时拉取只作为极端情况下的容错手段。

java
// Nacos 客户端内部逻辑(简化)
// NacosNamingService.java
private void updateServiceNow(String serviceName, String groupName) {
    // 1. 向 Nacos 服务端发送 GET 请求,拉取完整实例列表
    List<Instance> instances = serverProxy.queryList(serviceName, groupName);

    // 2. 更新本地缓存
    Map<String, List<Instance>> localCache = hostMap.get(serviceName);
    localCache.put(groupName, instances);

    // 3. 定时任务调度,间隔默认 30s(2.x)
    executor.schedule(() -> updateServiceNow(serviceName, groupName), 30, TimeUnit.SECONDS);
}

(3)UDP 推送(Nacos 1.x)

  Nacos 1.x 中,服务端在注册表发生变更时,通过 UDP 协议向所有订阅了该服务的消费者推送一条变更通知。注意,推送的不是完整的实例列表,而是一个轻量级的 "服务变更通知",只包含服务名和一个版本号。消费者收到通知后,比对版本号,如果发现版本号比自己本地缓存的新,再主动发起 HTTP 请求拉取最新数据。

服务端实例变更(注册/下线/健康状态变化)

    ├── UDP 推送 "service-A 变了,version=12345"

    ├── 消费者收到 UDP 通知
    │     ├── 比对:本地 version=12344 < 服务端 version=12345
    │     └── 触发主动拉取最新实例列表

    └── 消费者收到 UDP 通知
          ├── 比对:本地 version=12345 == 服务端 version=12345
          └── 忽略,无需拉取

(4)gRPC 双向流推送(Nacos 2.x)

  Nacos 2.x 将通信协议升级为 gRPC 长连接,推送机制从 UDP 改为 gRPC 双向流(Bidirectional Streaming)。客户端和服务端之间维持一条长连接,服务端通过这条连接主动推送变更事件,无需客户端轮询。相比 UDP,gRPC 推送的可靠性更高(UDP 不保证送达),延迟更低,且支持双向通信。

长连接 vs 短连接

  要理解 Nacos 2.x 的推送机制,首先需要搞清楚长连接和短连接的本质区别。

特性短连接(HTTP 1.x)长连接(gRPC / HTTP/2)
连接生命周期一次请求-响应后即关闭建立后持久保持,可复用
TCP 握手开销每次请求都需三次握手一次握手,后续复用
服务端推送不支持,只能客户端主动拉原生支持,服务端可主动推
适用场景低频请求、简单 API 调用高频推送、实时通信、服务发现
Nacos 1.x 使用注册/发现/配置拉取用 HTTP心跳用 UDP(不可靠推送)
Nacos 2.x 使用不再使用全部通信统一为 gRPC 长连接

  短连接模式下,客户端每次请求都要经历 TCP 三次握手 → 发送请求 → 接收响应 → 四次挥手,整个过程至少需要 1.5 个 RTT(Round-Trip Time)。在微服务集群中,上百个服务实例频繁与 Nacos 通信,每次心跳(5s 间隔)都是一次新的短连接,连接建立和销毁的开销非常可观。长连接则将 TCP 握手压缩到一次,后续所有请求都复用这条连接,省去了反复握手和挥手的开销。

gRPC 长连接的底层基础:HTTP/2

  gRPC 构建在 HTTP/2 协议之上,长连接能力正是源自 HTTP/2 的几个核心特性:

  • 多路复用(Multiplexing):一条 TCP 连接上可以同时并发多个 Stream(流),每个 Stream 独立传输消息,互不阻塞。Nacos 客户端可以在一条连接上同时处理服务发现、配置订阅、心跳上报等多个逻辑通道,无需为每个功能单独建连接。
  • 双向流(Bidirectional Streaming):HTTP/2 的 Stream 是双向的,客户端和服务端可以在同一个 Stream 上同时发送和接收数据。Nacos 正是利用这个特性,让服务端能主动向客户端推送变更事件,而不需要客户端先发起请求。
  • 帧(Frame):HTTP/2 的最小传输单元是二进制帧,而非 HTTP/1.x 的文本行。二进制帧解析效率高,头部压缩(HPACK)进一步减少了传输体积。
HTTP/1.x 短连接模式:
  客户端                         Nacos 服务端
    │──── TCP 三次握手 ────────────→│
    │──── GET /nacos/v1/ns/instance ──→│
    │←──── 200 OK + 实例列表 ────────│
    │──── TCP 四次挥手 ────────────→│
    │                               │
    │  ... 5s 后 ...                │
    │                               │
    │──── TCP 三次握手 ────────────→│  ← 每次心跳都要重新握手!
    │──── 心跳请求 ────────────────→│
    │←──── 200 OK ─────────────────│
    │──── TCP 四次挥手 ────────────→│

HTTP/2 长连接模式(gRPC):
  客户端                         Nacos 服务端
    │──── TCP 三次握手 ────────────→│  ← 只握一次
    │══════ gRPC 长连接建立 ═══════│
    │                               │
    │  Stream 1: 订阅 service-A     │
    │  Stream 2: 心跳上报           │  ← 多路复用,一条连接干多件事
    │  Stream 3: 配置订阅           │
    │                               │
    │←── Stream 1: 推送变更事件 ────│  ← 服务端主动推,无需客户端拉
    │←── Stream 3: 推送配置变更 ────│
    │                               │
    │  ... 连接持续保持,直到显式关闭 ...  │

连接建立流程

  Nacos 客户端启动后,建立 gRPC 长连接的过程分为以下步骤:

  1. 地址解析:客户端从配置中拿到 Nacos 服务端地址列表(如 nacos://192.168.1.1:9848),注意 gRPC 端口是 9848(偏移 1000),不是 HTTP 端口 8848。Nacos 2.x 启动时会同时监听 8848(HTTP)和 9848(gRPC)两个端口,偏移量固定为 1000。

  2. 连接重试与选主:客户端遍历地址列表,逐个尝试建立 gRPC 连接。如果第一个地址连不上,自动切换到下一个。连接成功后会记录当前连接的 Nacos 节点信息。

  3. TLS 握手(可选):如果配置了 ssl:// 前缀或启用了 TLS,gRPC 会先进行 TLS 握手,建立加密通道。

  4. gRPC Channel 创建:底层 Netty 创建 ManagedChannel,内部封装了 TCP 连接池、消息编解码、Stream 管理等能力。

  5. 连接就绪回调:连接建立成功后,触发 onReady() 回调,客户端开始发送订阅请求和心跳。

java
// Nacos 2.x 客户端建立 gRPC 连接的内部逻辑(简化)
// GrpcClient.java

// 1. 创建 gRPC Channel,设置 keepalive 参数
ManagedChannel channel = NettyChannelBuilder.forAddress(serverIp, serverPort)
    .keepAliveTime(10, TimeUnit.SECONDS)       // 每 10s 发一次 keepalive ping
    .keepAliveTimeout(3, TimeUnit.SECONDS)      // ping 超时 3s 判定连接断开
    .keepAliveWithoutCalls(true)                // 即使没有活跃 Stream 也发 ping
    .build();

// 2. 创建 Stub(客户端存根)
RequestGrpc.RequestFutureStub stub = RequestGrpc.newFutureStub(channel);

// 3. 建立双向流,用于接收服务端推送
StreamObserver<Payload> responseObserver = new StreamObserver<>() {
    @Override
    public void onNext(Payload payload) {
        // 收到服务端推送的变更事件
        handleServerPush(payload);
    }
    @Override
    public void onError(Throwable t) {
        // 连接异常,触发重连
        reconnect();
    }
    @Override
    public void onCompleted() {
        // 服务端正常关闭连接
    }
};
StreamObserver<Payload> requestObserver = stub.requestBiStream(responseObserver);

五层保活机制 — 如何保证长连接不断

  长连接的核心挑战在于:网络环境不可靠,连接随时可能因 NAT 超时、防火墙切断、网络抖动、服务端重启等原因断开。Nacos 2.x 设计了五层保活机制,从上到下逐层兜底,确保连接始终可用。

第一层:gRPC Keepalive Ping(应用层心跳)
    │     每隔 10s 发送一次 HTTP/2 PING 帧
    │     超时 3s 未收到 ACK → 判定连接断开

第二层:客户端健康检查(连接状态自检)
    │     gRPC 连接状态变化时触发 onStateChanged 回调
    │     TRANSIENT_FAILURE → 立即重连

第三层:指数退避重连(断线自动重连)
    │     首次重连等待 1s,失败后翻倍(2s → 4s → 8s → ...)
    │     最大等待 60s,避免频繁重连打爆服务端

第四层:服务端空闲检测(防僵尸连接)
    │     服务端超过 20s 未收到任何数据 → 主动关闭连接
    │     客户端收到连接关闭事件 → 触发重连

第五层:定时全量拉取(终极兜底)
          即使推送彻底失效,客户端每 30s 仍会主动拉取一次
          保证注册表数据不会长时间过期

第一层:gRPC Keepalive Ping

  这是 gRPC 协议层面的心跳机制,不依赖任何业务数据,由 gRPC 框架自动完成。客户端每隔一定时间向服务端发送一个 HTTP/2 PING 帧,服务端收到后立即回复一个 PING ACK 帧。如果客户端在超时时间内未收到 ACK,就认为连接已断开。

客户端                               Nacos 服务端
    │                                      │
    │──── HTTP/2 PING ────────────────────→│  ← 每隔 10s
    │←──── HTTP/2 PING ACK ───────────────│  ← 即时回复
    │                                      │
    │  ... 正常通信 ...                      │
    │                                      │
    │──── HTTP/2 PING ────────────────────→│
    │           ╳ 网络中断,收不到 ACK        │
    │──── HTTP/2 PING ────────────────────→│  ← 3s 超时未收到 ACK
    │           ╳                           │
    │  判定连接断开,触发重连                  │

  Keepalive 的核心参数配置:

参数默认值含义
keepAliveTime10s每隔多久发送一次 PING
keepAliveTimeout3s等待 ACK 的超时时间
keepAliveWithoutCallstrue即使没有活跃 Stream 也发 PING(关键!)
permitKeepAliveTime5s(服务端)服务端允许客户端的最小 PING 间隔,防止恶意客户端用高频 PING 消耗资源

关键keepAliveWithoutCalls=true 必须开启。如果没有活跃的 Stream(比如服务发现订阅完成后,暂时没有变更推送),默认情况下 gRPC 不会发送 PING,防火墙或 NAT 设备可能在 30~60s 后切断空闲连接。开启后,即使没有业务数据,PING 也持续发送,保持连接活跃。

第二层:客户端健康检查

  gRPC Channel 维护了一个连接状态机,状态变化时通知客户端。Nacos 客户端监听这些状态变化,及时做出反应。

gRPC 连接状态机:

  CONNECTING ──→ READY ──→ IDLE
       ↑           │
       │           ↓
       └── TRANSIENT_FAILURE ──→ SHUTDOWN
状态含义客户端行为
CONNECTING正在建立连接等待,不做额外处理
READY连接就绪,可以正常通信发起订阅、启动心跳
TRANSIENT_FAILURE连接临时失败(网络抖动等)立即触发重连
IDLE连接空闲(无活跃 Stream)正常状态,不做处理
SHUTDOWN连接已关闭清理资源,触发重连

第三层:指数退避重连

  连接断开后,客户端不会立即无限重试,而是采用指数退避(Exponential Backoff)策略:

重连时间线:

断连时刻

  ├── 等待 1s  → 第 1 次重连 ── 失败
  ├── 等待 2s  → 第 2 次重连 ── 失败
  ├── 等待 4s  → 第 3 次重连 ── 失败
  ├── 等待 8s  → 第 4 次重连 ── 失败
  ├── 等待 16s → 第 5 次重连 ── 失败
  ├── 等待 32s → 第 6 次重连 ── 失败
  ├── 等待 60s → 第 7 次重连 ── 成功!

  └── 最大等待 60s,不再翻倍,稳定间隔重试

  首次重连间隔 1 秒,每次失败后翻倍,最大 60 秒。这样既能在网络短暂抖动时快速恢复,又能避免在 Nacos 集群整体宕机时,大量客户端同时高频重连造成 DDoS 效应。

第四层:服务端空闲检测

  连接保活是双向的,服务端也需要清理僵尸连接。Nacos 服务端对每个 gRPC 连接设置了一个空闲超时时间(默认 20s)。如果超过 20s 没有收到客户端的任何数据(包括 PING),服务端就认为这个连接已失效,主动关闭。

  这个机制与客户端的 keepAliveTime=10s 配合使用:客户端每 10s 发一次 PING,服务端每 20s 检查一次,只要客户端正常,PING 间隔(10s)小于空闲超时(20s),连接就不会被服务端误杀。

服务端空闲检测逻辑:

服务端维护每个连接的 lastReadTime(最后一次收到数据的时间)

定时任务(每 1s 检查一次):
  遍历所有连接
    if (当前时间 - lastReadTime > 20s) {
      关闭该连接;
      发送 GOAWAY 帧通知客户端;
    }

第五层:定时全量拉取

  这是最底层的兜底机制。即使前面四层全部失效——gRPC 连接彻底断开、重连持续失败、推送链路完全不可用——客户端仍然每 30s 主动向 Nacos 发起一次 HTTP 全量拉取。这确保注册表数据不会长时间过期,服务调用不会因为注册中心故障而完全中断。

完整保活流程总结:

客户端启动

    ├── 建立 gRPC 长连接(TCP 握手 + HTTP/2 升级)

    ├── 订阅服务 + 启动心跳

    ├── 保活循环:
    │     ├── 每 10s:发送 PING → 收到 ACK → 连接正常
    │     ├── 每 30s:全量拉取注册表(兜底)
    │     └── 连接状态变化 → 触发重连

    ├── 连接断开:
    │     ├── 立即重连(指数退避:1s → 2s → 4s → ... → 60s)
    │     ├── 重连成功 → 恢复订阅 + 心跳
    │     └── 重连期间 → 使用本地缓存 + 磁盘快照

    └── 终极兜底:
          即使推送彻底失效,定时拉取保证 30s 内数据一致

Nacos 2.x gRPC 推送流程:

消费者 ←─── gRPC 长连接(Keepalive 10s/3s)───→ Nacos 服务端
    │                                                │
    │  订阅 service-A(建立双向 Stream)                │
    │───────────────────────────────────────────────→│
    │                                                │
    │  返回全量实例列表                                │
    │←───────────────────────────────────────────────│
    │                                                │
    │  每 10s: PING ────────────────────────────────→│
    │←──────────────────────────────── PING ACK ─────│
    │                                                │
    │                                   实例变更(新实例注册)
    │  推送变更事件(同一 Stream 上)                    │
    │  {type:INSTANCE_ADD,                           │
    │   service:service-A,                           │
    │   instance:{ip,port,...}}                      │
    │←───────────────────────────────────────────────│
    │                                                │
    │  更新本地缓存 + 磁盘快照                          │

3.5.2 注册表一致性保证

  Nacos 通过以下机制,确保消费者本地缓存的注册表与 Nacos 服务端的数据保持一致。

(1)版本号(Checksum)机制

  每个服务在 Nacos 服务端都有一个 checksum(数据摘要),每次实例列表发生变化时,checksum 都会重新计算。消费者在每次拉取实例列表时,会同时拿到 checksum,将它存入本地缓存。

Nacos 服务端维护:
  service-A → instances: [{...}, {...}, {...}]
  service-A → checksum:  "a1b2c3d4..."   ← 每次实例变更时重新计算

消费者本地缓存:
  service-A → instances: [{...}, {...}, {...}]
  service-A → checksum:  "a1b2c3d4..."

  消费者收到推送通知后,不需要立即拉取完整实例列表,而是先向 Nacos 服务端发送一个轻量级请求,只查询当前服务的 checksum。如果 checksum 与本地缓存一致,说明注册表没有变化,无需拉取;如果不一致,再发起全量拉取。这个机制大幅减少了网络传输量,尤其在实例数量多、变更频繁的场景下效果显著。

消费者收到推送通知

    ├── GET /nacos/v1/ns/instance/list?serviceName=service-A&checksum=a1b2c3d4...

    ├── Nacos 比对 checksum
    │     ├── 一致 → 返回 304 Not Modified → 消费者不更新,省带宽
    │     └── 不一致 → 返回 200 + 全量实例列表 + 新 checksum

    └── 消费者更新本地缓存

(2)缓存分层与容错

  消费者的本地缓存分为两层:内存缓存磁盘快照。服务启动时,先尝试从磁盘快照中恢复上次的实例列表,避免冷启动时对 Nacos 造成瞬时压力。运行期间,实例列表变更后同步更新内存缓存和磁盘快照。

消费者本地缓存结构:

┌─────────────────────────────────┐
│  内存缓存(ConcurrentHashMap)     │
│  service-A → [{...}, {...}]      │
│  service-B → [{...}]             │
│  ← 调用时直接读取,毫秒级            │
└─────────────────────────────────┘
              ↕ 同步更新
┌─────────────────────────────────┐
│  磁盘快照(本地文件)                 │
│  ~/nacos/naming/service-A        │
│  ← 重启时恢复,容灾兜底              │
└─────────────────────────────────┘

  如果 Nacos 服务端完全宕机,消费者仍然可以使用本地缓存中的实例列表进行服务调用。虽然无法感知实例的上下线变化,但已有的实例可以继续工作,保证服务不中断。这就是 "注册中心挂了,服务调用不受影响" 的底层原理。

(3)保护阈值(Protection Threshold)

  保护阈值是一个 0~1 之间的浮点数,表示健康实例占比的最低容忍值。当健康实例占比低于这个阈值时,Nacos 不再剔除不健康实例,而是将所有实例(包括不健康的)都返回给消费者。这是为了防止因网络抖动导致大量实例被误判为不健康,进而引发雪崩。

场景:10 个实例,保护阈值 0.8

正常情况:
  健康实例 9 个 → 占比 90% > 80% → 正常返回健康实例

网络抖动:
  健康实例 5 个 → 占比 50% < 80% → 触发保护
  → 将所有 10 个实例都返回给消费者
  → 消费者负载均衡时可能调到不健康实例,但不会全部不可用

  消费者拿到实例列表后,自身的负载均衡器(Ribbon/LoadBalancer)也会根据实例的健康状态做过滤,所以即使 Nacos 把不健康实例也返回了,消费者端仍然可以做一层兜底过滤。

(4)心跳与实例剔除联动

  消费者拿到的实例列表是否准确,取决于 Nacos 服务端对实例状态的判断是否准确。Nacos 通过心跳机制判断实例是否存活:

时间节点事件
实例启动注册到 Nacos,状态为 healthy:true
每隔 5s实例发送心跳
超过 15s 无心跳实例标记为 healthy:false
超过 30s 无心跳实例从注册表中剔除

  实例状态变更后,Nacos 通过 gRPC 推送(2.x)或 UDP 通知(1.x)告知所有订阅者,消费者收到通知后更新本地缓存。从实例宕机到消费者感知,整个流程的延迟通常在 10 秒以内(15s 心跳超时 + 推送延迟)。

(5)一致性保证总结

机制解决的问题时效性
首次全量拉取消费者冷启动时获取初始注册表即时
gRPC 推送(2.x)实例变更时实时通知消费者秒级
checksum 比对避免重复拉取未变更的注册表,节省带宽即时
定时拉取兜底容错,防止推送丢失30s 间隔
磁盘快照Nacos 宕机时,消费者从本地恢复注册表启动时恢复
保护阈值防止网络抖动导致大量误剔除实时生效

3.6 心跳与健康检查

Nacos 的健康检查机制比 Eureka 更丰富:

  • 临时实例(AP):客户端主动上报心跳,Nacos 被动接收
  • 持久化实例(CP):Nacos 主动探测实例的健康状态(TCP / HTTP / MySQL)
  • 保护阈值:当健康实例占比低于该阈值时,Nacos 会保护所有实例(包括不健康的),防止大面积误删

3.7 AP 模式 vs CP 模式

四、Nacos 作为配置中心

4.1 配置模型

Namespace(命名空间)
  └── Group(分组)
        └── Data ID(配置集 ID)
              └── 配置内容(properties / yaml / json / text)

Data ID 命名规范${prefix}-${spring.profile.active}.${file-extension}

例如:order-service-dev.yaml 对应 order-service 在 dev 环境下的配置。

4.2 配置拉取流程

服务启动

    ├── 1. 拼接 Data ID: order-service-dev.yaml

    ├── 2. 向 Nacos Server 请求配置
    │      GET /nacos/v1/cs/configs?dataId=order-service-dev.yaml&group=DEFAULT_GROUP

    ├── 3. Nacos 返回配置内容

    ├── 4. 服务将配置加载到 Spring Environment
    │      (优先级高于本地 application.yml)

    └── 5. 通过长轮询监听配置变更
           配置变更后,发布 RefreshScopeRefreshedEvent
           标注了 @RefreshScope 的 Bean 会被重新初始化

4.3 配置动态刷新

java
@RestController
@RefreshScope   // 标记此 Bean 支持动态刷新
public class OrderController {

    @Value("${order.timeout:30}")
    private int timeout;

    @Value("${order.max-items:10}")
    private int maxItems;

    @GetMapping("/config")
    public Map<String, Object> getConfig() {
        return Map.of("timeout", timeout, "maxItems", maxItems);
    }
}

配置变更后,无需重启服务,调用 /config 接口即可看到最新值。

4.4 配置灰度和回滚

  • 灰度发布:部分实例使用新配置,验证无误后全量推送
  • 历史版本:Nacos 保存配置变更的 30 天历史,支持一键回滚
  • 监听查询:查看当前有哪些服务订阅了某项配置

五、生产环境最佳实践

5.1 高可用部署

                    ┌─────────────────┐
                    │   Nginx / VIP    │  ← 负载均衡
                    └────────┬────────┘

          ┌──────────────────┼──────────────────┐
          │                  │                  │
    ┌─────▼─────┐     ┌─────▼─────┐     ┌─────▼─────┐
    │  Nacos-1   │     │  Nacos-2   │     │  Nacos-3   │
    │ 192.168.1.1│     │ 192.168.1.2│     │ 192.168.1.3│
    └─────┬─────┘     └─────┬─────┘     └─────┬─────┘
          │                  │                  │
          └──────────────────┼──────────────────┘

                    ┌────────▼────────┐
                    │   MySQL 主从     │  ← 持久化存储
                    └─────────────────┘

关键配置:

  • 至少 3 个 Nacos 节点(Raft 协议要求奇数节点)
  • 前端挂 Nginx 做负载均衡,或使用 VIP
  • MySQL 使用主从架构,避免单点故障

5.2 命名空间规划

Nacos Namespace
├── dev (开发环境)
│   ├── order-service
│   ├── user-service
│   └── payment-service
├── test (测试环境)
│   ├── order-service
│   └── ...
└── prod (生产环境)
    ├── order-service
    └── ...

5.3 常见问题

Q:服务下线后,消费者多久能感知?

临时实例:心跳超时 15s 标记不健康,30s 剔除。消费者通过长轮询感知变更,通常在 10s 内生效。

Q:Nacos 挂了,服务还能调用吗?

能。消费者本地缓存了服务实例列表,即使 Nacos 完全宕机,已缓存的实例仍然可用(但新增实例或下线实例无法感知)。

Q:Nacos 1.x vs 2.x 的区别?

2.x 版本将通信协议从 HTTP 短连接改为 gRPC 长连接,大幅提升了推送效率和稳定性,建议新项目直接使用 2.x。

六、Prometheus + Grafana 监控

  生产环境中,Nacos 自身的健康状态和运行指标必须纳入统一监控体系。Prometheus 负责指标采集和存储,Grafana 负责可视化展示和告警。

6.1 Nacos 暴露 Metrics

  Nacos 2.x 基于 Micrometer 暴露指标,原生支持 Prometheus 拉取。

Nacos 服务端开启 Metrics:

Nacos 2.x 默认已经暴露了 /nacos/actuator/prometheus 端点。确保 application.properties 中以下配置生效:

properties
# Nacos 2.x 默认配置,通常无需修改
management.endpoints.web.exposure.include=*
management.metrics.export.prometheus.enabled=true

启动后访问 http://nacos-host:8848/nacos/actuator/prometheus 即可看到 Prometheus 格式的指标数据。

Spring Boot 客户端暴露 Metrics:

微服务接入 Nacos 后,自身也会产生与 Nacos 交互相关的指标,需要一并暴露:

xml
<!-- actuator + prometheus -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
yaml
# application.yml
management:
  endpoints:
    web:
      exposure:
        include: health,info,prometheus
  metrics:
    tags:
      application: ${spring.application.name}   # 打上应用名标签

6.2 Prometheus 采集配置

yaml
# prometheus.yml
scrape_configs:
  # 抓取 Nacos 服务端指标
  - job_name: 'nacos-server'
    metrics_path: '/nacos/actuator/prometheus'
    static_configs:
      - targets:
          - 'nacos-1:8848'
          - 'nacos-2:8848'
          - 'nacos-3:8848'
    relabel_configs:
      - target_label: cluster
        replacement: 'nacos-cluster'

  # 抓取各微服务客户端指标
  - job_name: 'spring-boot-apps'
    metrics_path: '/actuator/prometheus'
    static_configs:
      - targets:
          - 'order-service:8080'
          - 'user-service:8080'
          - 'payment-service:8080'

6.3 关键监控指标

6.3.1 Nacos 服务端指标

指标名含义关注点
nacos_monitor系统运行指标CPU、内存、连接数
nacos_naming_service_count注册服务总数服务数量的变化趋势
nacos_naming_instance_count实例总数健康/不健康实例占比
nacos_config_count配置总数配置数量变化
nacos_http_request_totalHTTP 请求总数QPS、错误率
nacos_grpc_server_connectionsgRPC 连接数客户端连接数,异常波动说明有服务下线
jvm_memory_used_bytesJVM 内存使用是否接近 OOM 阈值
jvm_gc_pause_secondsGC 停顿时间GC 频率和耗时

6.3.2 客户端(微服务)指标

指标名含义关注点
nacos_timer_secondsNacos 客户端操作耗时注册/发现/配置拉取的延迟
nacos_client_request_total客户端请求总数与 Nacos 服务端的通信频率
http_client_requests_secondsHTTP 调用耗时服务间调用延迟
resilience4j_circuitbreaker_state熔断器状态哪些服务正在被熔断

6.4 Grafana 仪表盘

6.4.1 导入官方 Dashboard

Nacos 社区提供了现成的 Grafana Dashboard:

  • Nacos 官方 Dashboard 编号13221(Grafana 官网市场)
  • 导入方式:Grafana → Create → Import → 输入 13221 → 选择 Prometheus 数据源

Dashboard 包含以下面板:

面板分组内容
系统概览CPU 使用率、内存使用率、JVM 堆内存、GC 次数
服务注册注册服务总数、注册实例数、健康实例数、不健康实例数
配置管理配置总数、配置变更次数、长轮询 QPS
请求统计HTTP 请求 QPS、gRPC 连接数、请求延迟分布、错误率
客户端监控各微服务与 Nacos 通信延迟、心跳成功率

6.4.2 自定义 PromQL 查询示例

promql
# Nacos 服务注册实例数
nacos_naming_instance_count

# 健康实例占比
nacos_naming_instance_count{status="UP"} / nacos_naming_instance_count

# Nacos 服务端 QPS
rate(nacos_http_request_total[1m])

# Nacos 服务端请求错误率
sum(rate(nacos_http_request_total{status=~"5.."}[1m])) 
  / 
sum(rate(nacos_http_request_total[1m]))

# 各微服务与 Nacos 通信延迟(P99)
histogram_quantile(0.99, rate(nacos_timer_seconds_bucket[1m]))

6.5 Prometheus 告警规则

yaml
# nacos-alerts.yml
groups:
  - name: nacos_alerts
    rules:
      # Nacos 服务端宕机
      - alert: NacosInstanceDown
        expr: up{job="nacos-server"} == 0
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "Nacos 节点 {{ $labels.instance }} 已宕机"

      # 不健康实例占比过高
      - alert: NacosHighUnhealthyInstances
        expr: |
          (nacos_naming_instance_count - nacos_naming_instance_count{status="UP"}) 
          / nacos_naming_instance_count > 0.3
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "不健康实例占比超过 30%"

      # Nacos 配置中心请求延迟过高
      - alert: NacosHighLatency
        expr: |
          histogram_quantile(0.99, rate(nacos_http_request_seconds_bucket{uri=~"/nacos/v1/cs/.*"}[5m])) > 0.5
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Nacos 配置中心 P99 延迟超过 500ms"

      # Nacos JVM 内存使用率过高
      - alert: NacosHighMemory
        expr: |
          (jvm_memory_used_bytes{area="heap"} / jvm_memory_max_bytes{area="heap"}) > 0.85
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Nacos 堆内存使用率超过 85%"

6.6 监控体系全景

┌──────────────┐   ┌──────────────┐   ┌──────────────┐
│ Order Service│   │ User Service │   │Payment Service│
│  :8080/act   │   │  :8081/act   │   │  :8082/act   │
└──────┬───────┘   └──────┬───────┘   └──────┬───────┘
       │    metrics       │    metrics       │    metrics
       │                  │                  │
       │    ┌─────────────┼──────────────────┘
       │    │             │
       ▼    ▼             ▼
  ┌─────────────────────────────────┐
  │         Prometheus              │
  │   scrape_interval: 15s          │
  └───────────────┬─────────────────┘
                  │  query

  ┌─────────────────────────────────┐
  │          Grafana                │
  │  Dashboard + Alerting           │
  │  → 企微/钉钉/邮件 通知           │
  └─────────────────────────────────┘

                  │ metrics
  ┌───────────────┴─────────────────┐
  │         Nacos Cluster           │
  │  :8848/nacos/actuator/prometheus│
  └─────────────────────────────────┘

  实际部署时,Prometheus 和 Grafana 通常独立部署在监控网段,通过 Consul / Nacos 的服务发现或静态配置来发现目标。Grafana 中除了 Nacos 面板,还应该整合链路追踪(SkyWalking / Jaeger)和日志(ELK / Loki),形成「指标 + 链路 + 日志」三支柱可观测体系。