一、面试官为什么要问这道题?
「用过 Nacos,说说服务注册的原理」,几乎是大厂后端面试的必考题。它表面上只考察一个注册流程,实际上能顺藤摸瓜问出一整套分布式系统基本功:你有没有读过源码、知不知道 AP 和 CP 的区别、心跳是怎么做的、集群之间数据怎么同步、消费者怎么感知服务上下线、挂了会不会影响业务。
因此,这道题的高分回答从来不是一句「服务提供者把 IP 和端口注册上去,消费者拉下来就能调用了」,而是要把注册链路、数据模型、一致性协议、健康检查、服务发现、推送机制、集群同步这些点串成一条有逻辑的线。
建议把握三条主线:临时实例的 AP 链路(Distro 协议)、持久实例的 CP 链路(Raft / JRaft 协议)、注册之后的信息如何被消费者及时感知(拉取 + 推送 + 长连接)。
全文提纲:
Nacos 服务注册到底发生了什么
为什么需要服务注册中心
Nacos 的核心概念
Nacos 的整体架构
服务注册的完整流程
客户端源码分析
服务端源码分析
临时实例的核心:Distro 协议与 AP 链路
持久实例的核心:Raft / JRaft 协议与 CP 链路
心跳与健康检查
服务发现:消费者怎么拿到最新实例列表
集群同步:一个节点收到注册后如何扩散到全集群
容灾、重试与高可用设计
面试现场:高频追问与高质量回答
总结
二、先说结论:Nacos 服务注册到底发生了什么?
如果用一句话概括:服务提供者启动后,把「服务名 + IP + 端口 + 健康状态 + 元数据」封装成一个实例对象,通过注册请求发给 Nacos Server;Nacos Server 先把实例写进自己内存里的注册表(ServiceManager 维护的 Service 和 Cluster、Instance 三层模型),再根据实例是临时还是持久,走不同的持久化和一致性处理,最后把变更同步给集群其他节点,并通知订阅了该服务的消费者。
几个容易混淆的概念必须先澄清:
注册(Register)不等于持久化到磁盘:默认的临时实例只存在内存里,靠客户端心跳维持存活;持久实例才会被服务端主动健康检查并保留。
注册成功不等于立即强一致:临时实例走 AP,允许短暂不一致;持久实例走 CP,要求多数派确认。
服务注册和服务发现是一体两面:注册端写实例,发现端订阅服务名并拿到实例列表。
Nacos 2.x 已经全面转向 gRPC:注册、心跳、订阅不再是单纯的 HTTP 短连接,而是基于 gRPC 长连接 + 双向流。
三、先从为什么需要服务注册中心讲起
3.1 单体时代的调用方式
在单体架构里,配置里写死数据库地址、第三方系统地址就够了:
yaml
pay: base-url: http://10.0.8.21:8080 user: base-url: http://10.0.8.22:8081
一旦系统微服务化,几十上百个服务、每个服务多实例、实例还经常扩缩容和发布,写死 IP 的维护成本会爆炸。
3.2 微服务带来的三个新问题
服务地址不固定:容器化之后,Pod 或者容器的 IP 每次重启都可能变。
实例数量动态变化:弹性伸缩、灰度发布、故障摘除都会让实例列表不断变化。
调用方需要及时感知:服务下线后,调用方如果还拿旧地址,就会出现连接失败。
3.3 注册中心要解决的四个核心问题
服务的统一登记:服务提供者启动时把自身信息上报到一个中心节点。
服务的动态发现:消费者根据服务名查到当前可用的实例列表。
健康状态管理:通过心跳或主动探测,把不健康的实例剔除。
变更实时通知:实例上下线后,尽快告诉所有关心的订阅方。
四、Nacos 的核心概念:先把名词对齐
4.1 服务(Service)
服务由namespace+group+serviceName三元组唯一确定。
4.2 实例(Instance)
核心字段:
ip:实例 IP;port:实例端口;clusterName:所属集群;healthy:是否健康;weight:权重;enabled:是否启用;ephemeral:是否临时实例;metadata:扩展元数据。
4.3 命名空间(Namespace)
用于做租户级隔离,生产、测试、灰度环境可以用不同命名空间隔开。Nacos 自带的public是默认命名空间。
4.4 分组(Group)
在同一个命名空间内做更细粒度的服务分组,默认是DEFAULT_GROUP。
4.5 集群(Cluster)
同一个服务下的实例可以划分到不同集群,比如同机房优先调用、跨机房容灾。
4.6 临时实例与持久实例
| 对比项 | 临时实例(ephemeral=true) | 持久实例(ephemeral=false) |
|---|---|---|
| 健康检查方式 | 客户端主动上报心跳 | 服务端主动探测(TCP/HTTP/MySQL 等) |
| 数据存储 | 默认只存内存 | 持久化,服务端重启可恢复 |
| 一致性协议 | Distro(AP) | Raft / JRaft(CP) |
| 典型场景 | Spring Cloud 微服务、Dubbo 临时节点 | 数据库、中间件等需要主动探测的基础设施 |
五、Nacos 的整体架构
三块:
Nacos Server:注册中心服务端,负责存储实例、健康检查、推送变更。
Nacos Client SDK:业务应用集成的客户端,负责注册实例、发心跳、订阅服务。
Nacos Console:控制台。
Nacos 2.x 引入了 gRPC 长连接,把大量请求从短连接切换成长连接上的请求响应流。
六、服务注册的完整流程:从客户端到服务端
6.1 一句话时序
应用启动,初始化
NamingService;调用
registerInstance;客户端把实例信息组装成注册请求;
通过 gRPC(2.x)或 HTTP(1.x)发给服务端;
服务端
InstanceOperatorClientImpl处理请求;写入内存注册表
ServiceManager中的Service -> Cluster -> Instance;因为默认是临时实例,走 Distro 协议,触发集群同步;
客户端侧
BeatReactor启动定时心跳;服务端收到心跳后刷新实例的
lastBeat时间;消费者订阅的服务发生变更时,服务端通过推送或长连接通知消费者更新本地缓存。
6.2 入门示例:客户端怎么注册一个实例
java
Properties properties = new Properties(); properties.put(PropertyKeyConst.SERVER_ADDR, "127.0.0.1:8848"); properties.put(PropertyKeyConst.NAMESPACE, "public"); NamingService naming = NacosFactory.createNamingService(properties); Instance instance = new Instance(); instance.setIp("192.168.1.10"); instance.setPort(8080); instance.setClusterName("DEFAULT"); instance.setWeight(1.0); instance.setHealthy(true); instance.setEphemeral(true); naming.registerInstance("order-service", "DEFAULT_GROUP", instance);七、客户端源码分析:注册请求是怎么发出的
7.1 NacosNamingService 的构造
java
public class NacosNamingService implements NamingService { private NamingClientProxy clientProxy; private BeatReactor beatReactor; private HostReactor hostReactor; ... }7.2 registerInstance 的调用链
java
@Override public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException { if (instance.isEphemeral()) { BeatInfo beatInfo = buildBeatInfo(serviceName, instance); beatReactor.addBeatInfo(groupName, serviceName, beatInfo); } clientProxy.registerService(serviceName, groupName, instance); }临时实例在注册之前,会先把心跳信息加到 BeatReactor。
7.3 代理层选择 gRPC 还是 HTTP
java
InstanceRequest request = new InstanceRequest(); request.setNamespace(namespace); request.setServiceName(serviceName); request.setGroupName(groupName); request.setType(InstanceRequest.REGISTER); request.setInstance(instance); NamingGrpcClientProxy.this.redoService(request); requestToServer(request);
7.4 心跳是怎么发出来的
java
public void addBeatInfo(String groupName, String serviceName, BeatInfo beatInfo) { ... ScheduledFuture<?> future = executorService.scheduleWithFixedDelay( new BeatTask(beatInfo), 0, beatInterval, TimeUnit.MILLISECONDS); ... }默认心跳间隔 5 秒,超过 15 秒不健康,超过 30 秒摘除。
八、服务端源码分析:注册表是怎么组织的
8.1 请求入口
java
public void registerInstance(Service service, Instance instance) throws NacosException { // 1. 解析并校验参数 // 2. 更新内存注册表 // 3. 根据临时/持久实例走不同的一致性协议 // 4. 触发健康检查或心跳计时 // 5. 通知订阅者 }8.2 核心注册表:ServiceManager
java
public class ServiceManager { private final Map<String, Map<String, Service>> serviceMap = new ConcurrentHashMap<>(); ... }外层 key 是namespace,内层 key 由group + "@@" + serviceName拼接而成。
8.3 Service 内部:Cluster 与 Instance 的两级存储
java
public class Service { private String namespaceId; private String groupName; private String name; private Map<String, Cluster> clusterMap = new ConcurrentHashMap<>(); ... } public class Cluster { private String serviceName; private String name; private Set<Instance> persistentInstances = new ConcurrentSkipListSet<>(); private Set<Instance> ephemeralInstances = new ConcurrentSkipListSet<>(); ... }两个设计点:一是 Service 下按clusterName分桶;二是临时实例和持久实例从存储层就被分开放置。
8.4 注册表的并发安全与读写路径
Nacos 借助ConcurrentHashMap和对象级细粒度锁来保证线程安全。写入时先根据 namespace 找到内层 Map,再根据group + "@@" + serviceName拿到 Service;如果服务不存在,会通过双重检查的方式创建新的 Service 并放入注册表。读取时从内存注册表组装结果,单次查询耗时能做到毫秒级。
九、临时实例的核心:Distro 协议与 AP 链路
9.1 为什么临时实例默认不落盘
临时实例生命周期短,扩缩容、滚动发布、容器重启都会让实例频繁上下线。
健康信息本身就是易失信息,历史心跳没有持久化价值。
业务对临时实例的诉求是可用性优先,CAP 中更偏向 AP。
9.2 Distro 协议想解决什么问题
Distro 是 Nacos 自研的轻量级一致性协议,核心思想是去中心化、按服务名做路由、异步复制。它不要求强一致性,只保证最终一致。
9.3 注册写入与异步同步流程
请求进入
InstanceOperatorClientImpl,识别出是临时实例;先把实例写入本地
ServiceManager的Service -> Cluster -> ephemeralInstances;本地写入成功后立即返回客户端,不等待其他节点确认;
同时把这次变更封装成 Distro 数据同步任务,异步发送给集群中的其他节点;
其他节点收到后更新自己的注册表,并向各自长连接上的消费者推送变更。
9.4 Distro 的一致性为什么是最终一致
异步复制天然存在失败和延迟的可能。Nacos 通过定时任务和失败重试来收敛这种不一致:如果同步任务失败,会进入待重发队列继续尝试;同时各节点还会周期性做数据校验,把差异补齐。
十、持久实例的核心:Raft / JRaft 协议与 CP 链路
10.1 持久实例的注册流程
如果实例被标记为ephemeral=false,就是持久实例。持久实例注册时,Nacos 会把它交给 Raft 协议处理。请求不能只在单节点成功,而要写入 Raft 日志,经过集群多数派确认后,才能认为注册成功并返回客户端。
10.2 为什么 Nacos 选择 Raft
Raft 的三个核心机制:
Leader 选举:集群中任何时刻尽量只有一个 Leader 负责处理写请求。
日志复制:Leader 把写操作组织成日志条目,复制给 Follower。
安全性:只有包含最新已提交日志的节点才能成为 Leader。
Nacos 的底层默认使用 JRaft。
10.3 写入日志、多数派确认与状态机应用
客户端把写请求发给 Leader;
Leader 把「注册实例」这个操作封装成日志条目,追加到本地日志;
Leader 将日志复制给所有 Follower;
超过半数节点确认收到并持久化日志后,Leader 提交该条目;
各节点把日志应用到状态机,写入各自的
ServiceManager内存注册表和持久化存储;Leader 向客户端返回成功。
10.4 临时实例与持久实例链路对比
| 维度 | 临时实例 | 持久实例 |
|---|---|---|
| 一致性协议 | Distro,AP | Raft / JRaft,CP |
| 写请求延迟 | 低,本地写成功即返回 | 相对高,需要多数派确认 |
| 健康检查 | 客户端主动心跳 | 服务端主动探活 |
| 持久化 | 默认不落盘 | 通过 Raft 日志和快照持久化 |
| 适用场景 | 业务微服务、弹性伸缩频繁 | 基础设施、需要主动探测和恢复 |
十一、心跳与健康检查:实例是怎么被摘除的
11.1 临时实例:客户端心跳兜底存活
客户端注册成功后,BeatReactor会以固定间隔向服务端发送心跳。服务端收到心跳后,刷新实例的lastBeat时间,并重新计算健康状态。
11.2 5 秒、15 秒、30 秒分别是什么
5 秒:客户端心跳发送间隔,默认为 5000 毫秒。
15 秒:服务端超过 15 秒没收到心跳,就把临时实例标记为不健康。
30 秒:服务端超过 30 秒还没收到心跳,就把临时实例从注册表中摘除。
11.3 持久实例:服务端主动探活
持久实例通常不具备可靠的心跳上报能力,因此换由服务端主动健康检查。Nacos 支持多种探测方式:TCP 连接检测、HTTP 接口返回码检测、MySQL 探活等。
11.4 心跳失败之后的客户端重注册
如果客户端心跳持续失败,或者服务端返回「实例不存在」,客户端的redoService逻辑会触发重新注册。
十二、服务发现:消费者怎么拿到最新实例列表
12.1 服务订阅模型
消费者调用subscribe后,Nacos 客户端会先拉取一次全量实例列表放入本地缓存,然后以长连接或轮询的方式跟踪变化。真正发起的远程调用,读的是客户端本地缓存。
12.2 1.x 时代的 HTTP + UDP 推送
实例发生变更时,服务端通过 UDP 向订阅者发送轻量级通知,客户端收到通知后主动发起一次 HTTP 查询拉取最新列表。
12.3 2.x 时代的 gRPC 双向流推送
Nacos 2.x 用 gRPC 长连接取代了「HTTP 短连接 + UDP 推送」的组合。服务端维护与每个客户端的双向流,实例变更后可以直接通过连接推送。
12.4 客户端本地缓存与服务端变更队列
客户端侧由HostReactor维护服务名到实例列表的本地缓存。服务端则通过PushService或连接管理器管理变更的及时下发。即使推送短暂失败,客户端仍会定期到服务端查询,用拉取作为推送的兜底。
十三、集群同步:一个节点收到注册后如何扩散到全集群
13.1 Nacos 集群部署形态
生产环境中的 Nacos 通常是三节点或更多节点的集群。客户端配置的是多个 server 地址,启动后会选择其中一个建立连接。如果当前节点不可用,客户端会自动切换到其他节点。
13.2 临时实例的 Distro 同步细节
每个节点都可以接收临时实例的注册请求。Distro 同步按服务名把数据分散到不同节点负责管理,同时把变更异步复制给其他节点。
13.3 持久实例的 Raft 复制
持久实例和配置数据则统一交给 JRaft 复制。集群中存在 Leader 和 Follower 之分,写请求统一由 Leader 处理并复制到多数派节点。
13.4 集群故障与一致性取舍
临时实例在部分节点不可用时仍能继续写入,代价是数据可能短暂不一致;持久实例在多数派节点存活时才能写入,牺牲部分可用性换取强一致。
十四、容灾、重试与高可用设计
14.1 客户端失败重试:redoService 与 redoTask
注册、心跳、订阅等请求失败后,会进入redoService或定时重做任务队列,由后台任务按一定策略重试。
14.2 客户端多地址与连接切换
客户端初始化时会保存多个 Nacos server 地址。当前节点宕机或连接断开时,客户端会通过连接管理器的重连机制切换到其他节点。
14.3 防雪崩:本地缓存与推空保护
客户端优先使用本地缓存。即使注册中心完全不可用,消费者仍然可以用最后一次拉取到的实例列表继续发起调用。同时,服务端在异常情况下也应避免把空列表随意推给客户端。
14.4 注册中心宕机会不会影响业务调用
已经建立订阅并且本地有缓存的调用方,短期内仍能继续工作。受影响的主要是新实例无法注册、已下线实例无法及时感知、新消费者无法拿到初次列表。因此注册中心更像服务的「控制平面」,真正运行中的调用依赖的是本地缓存和已建立的连接。
十五、面试现场:高频追问与高质量回答
15.1 临时实例和持久实例怎么选
业务微服务通常用临时实例,因为扩缩容频繁、客户端天然有心跳能力;数据库、中间件等基础设施通常用持久实例,因为需要服务端主动探活并持久化。选择的关键是看「谁来保证健康」和「是否需要强一致持久化」。
15.2 AP 和 CP 在 Nacos 里如何体现
临时实例走 Distro,写请求本地成功即返回,是 AP;持久实例走 Raft/JRaft,写请求需要多数派确认,是 CP。Nacos 允许两类实例共存,是因为不同业务对一致性和可用性的要求不同。
15.3 服务注册后消费者怎么感知
1.x 靠 UDP 通知 + HTTP 拉取;2.x 靠 gRPC 长连接双向流推送。客户端收到变更后更新本地缓存,业务调用读的是本地缓存。
15.4 心跳为什么是 5 秒、15 秒、30 秒
5 秒是客户端发送间隔,15 秒是服务端标记不健康的阈值,30 秒是摘除阈值。这套参数在「快速发现故障」和「避免误判」之间取得平衡。
15.5 注册中心挂了会不会影响业务
已订阅且有本地缓存的调用方短期内不受影响,新注册和新订阅会受影响。因此 Nacos 的高可用设计重点是客户端本地缓存和连接重试。
15.6 Nacos 2.x 相比 1.x 有什么变化
1.x 主要走 HTTP 短连接 + UDP 推送;2.x 全面转向 gRPC 长连接 + 双向流,注册、心跳、订阅、推送复用同一条连接,性能和可靠性都有提升。
十六、总结
Nacos 服务注册的核心可以压缩成三条主线:
临时实例的 AP 链路:Distro 协议,本地写成功即返回,异步复制到其他节点,靠客户端心跳维持存活。
持久实例的 CP 链路:Raft / JRaft 协议,写请求需要多数派确认,靠服务端主动探活。
服务发现链路:客户端订阅 + 本地缓存 + 服务端推送,1.x 用 UDP + HTTP,2.x 用 gRPC 双向流。
把这套体系讲清楚,再结合源码细节和集群同步机制,就能在面试中从容应对 Nacos 服务注册相关的各种追问。