注册中心的原理:Nacos、Eureka 与 CP/AP 之争
服务之间真正发起调用的那条链路上,注册中心其实完全不参与。理解它「只做地址管理、不做请求转发」的定位,才能想明白推拉结合怎么配合、AP 与 CP 该怎么选,以及它整个集群挂掉之后业务为什么还能继续跑。
一、一个反直觉的事实
假设一个电商系统拆了二十个微服务,每秒钟有上万个跨服务调用。这里先算一笔账:这些调用里,有多少次会经过注册中心?
答案是 零次。
注册中心保存着所有服务实例的地址清单,但真正的业务请求一笔都不走它。消费者从注册中心拿到地址之后就把它放到一边,后面成千上万次调用全是点对点直连。
这个事实会带来一个很反直觉的推论:
注册中心是微服务体系里唯一一个「整个集群全挂掉、核心链路还能继续跑一段时间」的关键组件。 但它也因此成为整个体系里最危险的单点——它一旦挂得久了,新服务上不来、故障机器下不去,系统会缓慢地烂掉。
要把这件事讲清楚,得先把它的职责边界划出来。
二、它到底解决什么问题
微服务拆开之后,服务 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_* 四个变量后自动开启。