微服务

注册中心的原理:Nacos、Eureka 与 CP/AP 之争

服务之间真正发起调用的那条链路上,注册中心其实完全不参与。理解它「只做地址管理、不做请求转发」的定位,才能想明白推拉结合怎么配合、AP 与 CP 该怎么选,以及它整个集群挂掉之后业务为什么还能继续跑。

2026-02-11 约 11 分钟 4519 字 微服务 Nacos 服务发现 高可用

一、一个反直觉的事实

假设一个电商系统拆了二十个微服务,每秒钟有上万个跨服务调用。这里先算一笔账:这些调用里,有多少次会经过注册中心?

答案是 零次。

注册中心保存着所有服务实例的地址清单,但真正的业务请求一笔都不走它。消费者从注册中心拿到地址之后就把它放到一边,后面成千上万次调用全是点对点直连。

这个事实会带来一个很反直觉的推论:

注册中心是微服务体系里唯一一个「整个集群全挂掉、核心链路还能继续跑一段时间」的关键组件。 但它也因此成为整个体系里最危险的单点——它一旦挂得久了,新服务上不来、故障机器下不去,系统会缓慢地烂掉。

要把这件事讲清楚,得先把它的职责边界划出来。

二、它到底解决什么问题

微服务拆开之后,服务 A 要调服务 B,得知道 B 的 IP 和端口。最原始的做法是写在配置文件里:

# ❌ 硬编码:每一次扩缩容、每一次机器漂移都要改所有调用方
order:
  url: http://192.168.1.11:8080

线上环境里这套做法撑不过一周。B 扩容到三个实例,调用方得配三个地址并自己做负载均衡;B 有一台机器宕机漂移,B 自己不知道,调用方更不知道,流量还会继续往那台死机器上打。

注册中心就是来收拾这个局面的:地址从静态配置变成动态名录。提供者上线自己登记,下线自己注销,消费者只认「服务名」,不认 IP。

三、三大角色与完整流程

角色 英文 职责
服务提供者 Provider 启动后上报自己的服务名、IP、端口、健康状态
服务消费者 Consumer 拉取实例列表并缓存,后续直连调用
注册中心 Registry 保存全量实例清单,负责健康检测和变更通知

一次完整的生命周期分四步:

1. 服务注册。 提供者启动后主动把元数据上报给注册中心,注册中心把它存进内存里的实例列表。注意是「主动上报」,不是注册中心去扫机器——服务不知道自己什么时候该上线。

2. 服务发现。 消费者启动时按服务名拉取实例列表,写进本地缓存。此后所有调用都基于本地缓存做负载均衡,不再问注册中心。

3. 健康检测。 提供者周期性发心跳。注册中心长时间收不到心跳就判定实例不健康,把它从列表里剔除。消费者本地缓存同步刷新。

4. 变更通知。 实例上下线、机器宕机时,注册中心把变更推给消费者,消费者刷新本地缓存。

这四步里最容易被忽略的是第 2 步和第 4 步的关系:「发现」是一次性的快照,「通知」才是持续的同步。两者的实现方式不同,代价也不同。

四、推还是拉:客户端缓存才是核心设计

如果只允许一种同步方式,注册中心会各选一半:

方式 机制 优点 缺点
纯推送 注册中心主动把变更发给所有消费者 实时性最好 实例多、变更频繁时推送风暴,注册中心要维护所有消费者连接
纯拉取 消费者定时轮询全量列表 实现简单,注册中心压力可控 实时性差,轮询间隔就是故障感知延迟;全量拉取带宽浪费
推拉结合 消费者长轮询挂起 + 注册中心有变更才响应 实时性和压力都兼顾 实现复杂一些

生产环境基本都是第三种。Nacos 1.x 的做法是消费者发起一个长轮询请求,注册中心把这个请求挂起,最多等 29.5 秒:

  • 期间有变更 → 立刻返回变更数据(体感上等同于推送)
  • 期间没变更 → 到点返回一个空响应,消费者重新发起下一轮

这样既没有高频轮询的开销,又不会漏掉变更。Nacos 2.x 之后改成了 gRPC 长连接,思路一致但连接维护成本更低。

Eureka 则是纯拉取路线:客户端每 30 秒拉一次增量注册表,注册中心只维护一个最近三分钟变更队列,客户端按版本号取增量。实时性差一点,但注册中心的压力非常小、几乎不需要维护连接状态,这也是 Eureka 在高实例规模下反而稳定的原因。

为什么必须有本地缓存

「推拉结合」能成立,前提是消费者本地有一份缓存。

这份缓存的价值不只是省一次网络往返。它把注册中心的可用性和业务链路的可用性解耦了:注册中心抖动几秒、重启一次、甚至整个集群挂掉,只要消费者本地缓存还在,调用就能照常进行。如果没有本地缓存、每次调用都实时查注册中心,那它就是一个纯纯的单点。

五、心跳检测:怎么判定一个实例「死了」

心跳这一环里藏着分布式系统里最经典的取舍。参数配得太激进,一次网络抖动就把健康机器踢下线;配得太保守,流量会持续打到已经死掉的机器上。

主流组件的默认值:

组件 心跳间隔 判定不健康 剔除实例
Nacos(临时实例) 5 秒 15 秒 30 秒
Eureka 30 秒 — 90 秒
Zookeeper 会话超时时间内任意 — 会话超时(默认秒级到十几秒)

不同组件对「心跳丢失」的态度差别很大。

Nacos 走的是「不健康 ≠ 剔除」的两段式。15 秒收不到心跳先标记为不健康,实例还在列表里但不再被负载均衡选中;30 秒才真正剔除。这个中间态给了网络抖动一个缓冲窗口。

Zookeeper 走的是会话机制,靠临时节点(ephemeral node)实现。客户端会话断了,节点自动消失。这个机制很优雅,但代价是 ZK 在选举期间(比如 Leader 挂掉触发重新选主)所有写操作都会阻塞,注册和发现会短暂不可用。

Eureka 则干脆加了个「自我保护」开关:

eureka:
  server:
    enable-self-preservation: true      # 默认开启
    renewal-percent-threshold: 0.85     # 15 分钟内续约成功率低于 85% 触发

它的逻辑是:如果 15 分钟内心跳续约的成功率低于 85%,Eureka 认为是自己出了问题(网络分区),而不是那么多实例同时死了,于是停止剔除任何实例。

这个设计曾经被批评为「宁可留着死实例也不肯剔除」,但换个角度看它非常合理:注册中心最常见的失效场景不是「大量机器同时宕机」,而是「注册中心自己网络不通」。前者概率极低,后者一旦发生,激进剔除会把整个集群的所有实例全部清空——那才是真正的灾难。宁可让调用方偶尔打到死机器上重试,也不能让列表变成空的。

六、AP 与 CP:到底在争什么

CAP 说的是:网络分区(P)发生时,一致性和可用性只能保一个。注册中心这个场景里,P 是既定事实——多机房、多节点、网络必然会断。所以真正的选择只有 A 和 C。

AP 模式 CP 模式
代表组件 Nacos(临时实例)、Eureka Zookeeper、Consul、Nacos(持久化实例)
分区时的行为 各节点继续提供读写,接受数据暂时不一致 少数派节点拒绝服务,等选主完成
代价 可能读到过期的实例列表 选主期间注册和发现整体不可用
一致性协议 无,靠最终一致 Raft / ZAB / Paxos
适合 业务服务、在线交易这类「不能停」的系统 配置管理、分布式锁、Master 选举

关键点是这两种模式的失败表现完全不同。

AP 模式失败时长这样:某台机器已经下线了,但消费者在几秒内还能拿到它的地址,于是请求打过去、连接失败、重试下一个实例。表现为少量请求多了一次重试,业务整体不中断。

CP 模式失败时长这样:ZK 集群 Leader 挂掉,触发重新选主,这个窗口内(通常是秒级,网络状况差时可能到十几秒)所有服务注册和发现请求全部阻塞。表现为新实例起不来、老实例也拿不到地址,整个集群处于「谁也找不到谁」的状态。

对在线业务来说,前者显然比后者好接受得多。这就是为什么主流的互联网业务系统几乎都选 AP 模式的注册中心。

Nacos 的混合方案

Nacos 的有意思之处在于它不做二选一,而是按实例类型区分:

spring:
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.1.10:8848
        ephemeral: true       # 临时实例(默认):AP,靠心跳,挂了自动剔除
        # ephemeral: false    # 持久化实例:CP,走 Raft 协议,不靠心跳
  • ephemeral: true(默认):实例信息只存内存,靠客户端心跳维持,心跳断了就剔除。走 AP 路径。
  • ephemeral: false:实例信息写进 Raft 协议管理的存储,注册后永久存在,不做心跳剔除,只有主动注销才下线。走 CP 路径。

这个设计的实用价值在于:无状态业务服务用临时实例(AP),数据库、消息队列这类基础设施代理用持久化实例(CP)。比如一个注册进 Nacos 的 MySQL 代理节点,你绝对不希望它因为一次网络抖动就被剔除出列表;而一个无状态订单服务实例,网络抖动了剔除掉反而更安全。

七、注册中心挂了,服务还能调用吗

这是关于注册中心最值得想清楚的一个问题。答案是能,但分情况。

消费者状态 注册中心挂掉后的表现
已经启动、本地缓存有效 ✅ 正常调用,只是感知不到新实例上下线
已经启动、自身重启过 ⚠️ 缓存丢失,拉不到列表,可能启动失败
新启动的实例 ❌ 无法注册,调用方发现不了它
故障实例 ❌ 不会被剔除,流量会持续打过去,直到调用方自己的重试/熔断生效

所以准确的说法不是「注册中心挂了没事」,而是:

存量调用不受影响,增量变化全部冻结。

系统会在一个「时间胶囊」里继续运行——用着挂掉那一刻的实例列表。业务能跑,但扩容不起作用、故障机器下不去、新服务上不来。挂十分钟可以接受,挂半天就是事故。

这也解释了注册中心的高可用为什么必须认真做:

# Nacos 集群:至少三个节点,用独立数据库(MySQL)而不是内嵌 Derby
spring:
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.1.10:8848,192.168.1.11:8848,192.168.1.12:8848

节点数的选择理由很直接:两个节点时挂一个就失去了多数派,三个节点挂一个还能提供服务。生产环境不推荐双节点,也不推荐用 Nacos 内嵌的 Derby——那样每个节点各存一份数据,集群里会出现「同一份实例列表有三个版本」的诡异状态。

八、小结

  • 注册中心只做地址管理,不做请求转发;真实调用一笔都不经过它,请求转发是网关的职责
  • 三大角色:提供者注册、消费者发现、注册中心保存清单并做健康检测
  • 核心设计是推拉结合 + 消费者本地缓存;本地缓存把注册中心的可用性和业务链路的可用性解耦开
  • 心跳参数决定故障感知速度,但激进剔除比延迟剔除更危险,Eureka 的自我保护就是踩过坑之后的产物
  • AP 与 CP 的差别体现在故障时的表现形态:AP 是多一次重试,CP 是整体不可用;在线业务基本都选 AP
  • 注册中心挂掉后存量调用正常、增量变化冻结;高可用必须靠三节点集群 + 外部数据库兜住

把「消费者本地缓存」这一层想明白,注册中心的所有设计取舍都会变得顺理成章——它从来不是一个请求必经的组件,而是一个把「地址变化」这件事缓慢、可靠地传播出去的组件。

留下点什么

评论基于 GitHub Discussions(giscus)。在项目根目录配置.env.local填入 NEXT_PUBLIC_GISCUS_* 四个变量后自动开启。