看透 Nacos 3.x 注册中心:一次注册背后的 6 个关键设计
公众号名称:Fox爱分享
作者名称:Fox爱分享
发布时间:2026-07-02 17:30
阿里开源了 Nacos 3.2,AI 功能加了一堆,但很多人还没搞明白注册中心这个老本行。这篇文章,我们把源码翻出来,把一次服务注册走一遍,你就全懂了。
开篇:一个问题
你的 Spring Boot 服务启动,注解一加,服务就被 Nacos 发现了。
但你有没有想过——这个”被发现”,背后到底发生了什么?
-
实例信息存在哪儿?内存还是数据库?
-
多个 Nacos 节点,数据怎么同步?
-
服务挂了,Nacos 怎么知道?
-
同一个服务,为什么有时候 AP,有时候 CP?
把这 4 个问题答清楚,Nacos 注册中心你就吃透了。
核心架构:Client-Service 分离模型
Nacos 3.x 的 naming 模块做了一次重大重构,从 v1 的”以服务为中心”,升级为 v2 的”以客户端为中心”的架构。
核心抽象就两个:
Client(客户端) ←→ Service(服务)
持有实例发布信息 服务元数据/路由
持有订阅关系 实例列表索引
这个设计的好处在于:连接即状态。一个 gRPC 长连接断了,这个 Client 下面的所有临时实例自动摘除,不需要额外的心跳超时判断。

第一层:服务存在哪里?
先看 ServiceManager,它是整个注册中心的”服务目录”:
public class ServiceManager {
// 单例模式,全局唯一
privatestaticfinal ServiceManager INSTANCE = new ServiceManager();
// 核心存储:Service → Service(保证对象唯一性)
privatefinal ConcurrentHashMap singletonRepository;
// 按 namespace 做二级索引
privatefinal ConcurrentHashMap> namespaceSingletonMaps;
public Service getSingleton(Service service) {
// computeIfAbsent 保证线程安全地创建服务
Service result = singletonRepository.computeIfAbsent(service, key -> {
// 服务首次创建,发布元数据事件
NotifyCenter.publishEvent(new MetadataEvent.ServiceMetadataEvent(service, false));
return service;
});
// 同步更新 namespace 索引
namespaceSingletonMaps
.computeIfAbsent(result.getNamespace(), ns -> new ConcurrentHashSet<>())
.add(result);
return result;
}
}
几个设计要点值得注意:
① 初始容量 1024(1 << 10):经验值,减少扩容。大多数项目的服务数量在百到千级别,一次 resize 的代价比较高。
② computeIfAbsent 而不是 putIfAbsent:前者只有 key 不存在时才执行 lambda,避免重复创建对象;后者需要先创建对象再放入,存在无效创建的浪费。
③ 一发即忘的事件驱动:NotifyCenter.publishEvent 是异步的,服务创建和元数据处理解耦,不会因为下游慢而阻塞注册流程。
第二层:实例注册的完整链路
一次实例注册,从 HTTP/gRPC 入口到最终存储,经历这几层:
客户端 SDK
↓ gRPC InstanceRequest
InstanceRequestHandler(gRPC 处理器)
↓
InstanceOperatorClientImpl.registerInstance()
↓
EphemeralClientOperationServiceImpl.registerInstance()
↓
Client.addServiceInstance() ← 实例落地
↓
NotifyCenter.publishEvent() ← 触发 Distro 同步

核心实现在 EphemeralClientOperationServiceImpl:
@Override
public void registerInstance(Service service, Instance instance, String clientId)
throws NacosException {
// 1. 合法性校验(IP格式、端口范围、权重等)
NamingUtils.checkInstanceIsLegal(instance);
// 2. 获取/创建服务单例(保证全局唯一)
Service singleton = ServiceManager.getInstance().getSingleton(service);
// 3. 持久服务不能注册临时实例(类型保护)
if (!singleton.isEphemeral()) {
thrownew NacosRuntimeException(NacosException.INVALID_PARAM,
"Current service is persistent, can't register ephemeral instance.");
}
// 4. 找到对应的 Client(gRPC连接对应的客户端对象)
Client client = clientManager.getClient(clientId);
checkClientIsLegal(client, clientId);
// 5. 实例写入 Client 内存(这里才是真正的存储)
InstancePublishInfo instanceInfo = getPublishInfo(instance);
client.addServiceInstance(singleton, instanceInfo);
client.setLastUpdatedTime();
client.recalculateRevision(); // 更新数据版本号
// 6. 发布事件,触发:①Distro集群同步 ②订阅者推送
NotifyCenter.publishEvent(
new ClientOperationEvent.ClientRegisterServiceEvent(singleton, clientId));
NotifyCenter.publishEvent(
new MetadataEvent.InstanceMetadataEvent(singleton, instanceInfo.getMetadataId(), false));
}
有一个细节很重要:实例是存在 Client 上的,不是存在 Service 上的。
查询某个服务的实例列表时,是通过 ServiceStorage 反向索引找到所有注册了该服务的 Client,再聚合实例信息。这个设计让连接断开时的清理极其简单——删掉 Client 对象,所有实例自动消失。
第三层:AP vs CP,谁来决定?
这是最多人困惑的地方。
Nacos 同时支持两种一致性协议:
| AP(Distro) | CP(JRaft) | |
|---|---|---|
| 实例类型 | 临时实例(ephemeral=true) | 持久实例(ephemeral=false) |
| 一致性 | 最终一致 | 强一致 |
| 故障处理 | 节点宕机,其负责实例自动转移 | Leader 选举,少数服从多数 |
| 适用场景 | 微服务注册(默认) | 需要强一致的配置型服务 |

临时实例走 Distro,这是注册中心的默认模式。
DistroClientDataProcessor 同时实现了两个接口:
public class DistroClientDataProcessor extends SmartSubscriber
implements DistroDataStorage, // 提供数据快照,用于节点间全量同步
DistroDataProcessor { // 处理从其他节点收到的增量数据
public static final String TYPE = "Nacos:Naming:v2:ClientData";
// 监听 Client 操作事件,触发集群同步
// ClientRegisterServiceEvent → 向其他节点同步新增实例
// ClientDeregisterServiceEvent → 向其他节点同步删除实例
}
Distro 的同步逻辑:
-
写请求路由:每个节点负责一部分 Client(按 hash 分片),自己的 Client 自己处理
-
增量同步:有变更时,异步推送 DistroData 到其他节点
-
全量同步:新节点加入,拉取全量快照,这就是
isFinishInitial标志位的作用
第四层:健康检查,两种姿势
姿势一:连接健康即实例健康(临时实例)
这是 Nacos 3.x 最优雅的设计。gRPC 长连接本身就是心跳载体:
-
连接活着 → Client 活着 → 实例活着
-
连接断开 → 触发
ClientDisconnectEvent→ Client 下所有实例自动注销
不需要额外的心跳线程,连接即心跳。
姿势二:主动心跳检测(兼容旧版 HTTP 注册)
ClientBeatCheckTaskV2 处理 HTTP 方式注册的旧版实例,通过定时任务检查心跳超时:
@Override
public void doHealthCheck() {
Collection services = client.getAllPublishedService();
for (Service each : services) {
HealthCheckInstancePublishInfo instance =
(HealthCheckInstancePublishInfo) client.getInstancePublishInfo(each);
// 经过拦截器链,最终判断心跳是否超时
interceptorChain.doInterceptor(
new InstanceBeatCheckTask(client, each, instance));
}
}

拦截器链(InstanceBeatCheckTaskInterceptorChain)是个责任链模式,可以插入自定义逻辑,比如自定义健康检查策略。
第五层:订阅推送,事件驱动
消费者订阅服务后,注册中心怎么把变更推过去?
答案是事件驱动 + gRPC 服务端推送:
实例变更
↓ NotifyCenter.publishEvent(ClientOperationEvent)
NamingSubscriberServiceV2Impl(订阅者通知服务)
↓ 找出订阅了该服务的所有 Client
↓ 遍历,组装 ServiceChangedEvent
gRPC 连接推送
↓
消费者客户端更新本地缓存
整条链路全异步,发布者不感知消费者的存在,系统解耦做得非常干净。
第六层:Namespace + Group,两级隔离
Namespace(命名空间)
└── Group(服务分组)
└── ServiceName(服务名)
└── Instance(实例)
-
Namespace:最高隔离级别,不同 namespace 的服务互相不可见,适合环境隔离(dev/test/prod)
-
Group:同 namespace 内的逻辑分组,适合同环境下的业务隔离
ServiceManager 按 namespace 维护二级索引,查询时先过滤 namespace,再匹配服务名,这也是为什么用 namespaceSingletonMaps 做二级缓存的原因。
总结:注册中心的 6 个核心设计
| 设计点 | 方案 | 优势 |
|---|---|---|
| 服务存储 | ConcurrentHashMap 单例仓库 | 线程安全,对象唯一 |
| 实例挂载 | 实例挂在 Client 上 | 连接断开即自动清理 |
| 一致性选择 | AP(Distro) / CP(JRaft) 按实例类型分流 | 灵活适配不同场景 |
| 健康检查 | gRPC连接 + 心跳检测双轨 | 向下兼容,覆盖全场景 |
| 变更推送 | 事件驱动 + 服务端推送 | 实时性高,解耦彻底 |
| 多租户 | Namespace + Group 两级隔离 | 灵活的环境/业务隔离 |
最后说一句
很多人觉得注册中心”就那样”,其实里面的细节非常多。
Nacos 3.x 的 V2 架构比 V1 精妙在哪里?就在于那句”连接即状态”。把 gRPC 长连接的生命周期和实例的生命周期绑定在一起,系统复杂度直线下降,可靠性反而上去了。
这种”用约束换简单”的设计思想,才是最值得学的东西。
如果你觉得这篇文章有收获,欢迎转发给更多做分布式的朋友。下篇我们聊 Nacos 3.x 的 Distro 协议数据同步细节——那里才是真正硬核的部分。
关注「Fox爱分享」,持续输出分布式/云原生/AI工程硬核内容。
内容效果不满意?点此反馈