Nacos 第 7 篇:源码拆解:服务发现模块关键实现
注册表使用双层 Map 结构,按 namespace -> (group+service) -> Cluster -> Instance 高效存储。一致性哈希环决定 Distro 写责任,通过虚拟节点和 TreeMap 实现,扩缩容时触发数据迁移。请求处理采用 Filter 责任链,实现鉴权、流量修正、Distro 转发等可插拔逻辑。推送通道 gRPC 双向流提供可靠低延迟,UDP 作为 1.x
系列导读
前六篇我们从架构、心跳、配置推送、Distro、Raft,再到集群部署,把 Nacos 的理论体系全部铺开了。但真正让你从“会用”跳到“通透”的,是打开源码,看这些设计是如何一行行落地的。
从本篇开始,我们将连续两篇深入源码,今天聚焦 服务发现模块(Naming):注册表用什么结构?一致性哈希环如何指挥 Distro 同步?gRPC 推送通道怎样建立?责任链如何优雅处理请求?NamingService 注册全流程是怎样的?打开你的 IDE(或 GitHub),我们开始挖地三尺。
一、服务注册表:双层 ConcurrentHashMap 的内存结构
Nacos 的服务注册表全在内存中(持久化另有逻辑),它的核心数据结构是一个 双层 Map,定义在 com.alibaba.nacos.naming.core.ServiceManager 中。
抽象出来就是:
Map<namespace, Map<group::serviceName, Service>>
其中 Service 内部又包含:
Map<String, Cluster> clusterMap // clusterName -> Cluster
Cluster 内部维护着临时实例和持久实例的集合:
Set<Instance> ephemeralInstances // 临时实例,默认大多数 Set<Instance> persistentInstances // 持久实例
每个 Instance 就是具体的服务节点,包含 ip、port、weight、healthy、lastBeat、metadata、clusterName 等字段。
为什么用这种双层结构?
第一层 namespace 隔离了不同租户/环境,第二层 group::serviceName 隔离了业务服务,第三层 Cluster 支持多机房部署和就近访问。查询服务时,根据 namespace + group + serviceName 可以直接定位到 Service 对象,再从 clusterMap 中取出对应集群的实例列表。时间复杂度接近 O(1)。
在实际代码中,ServiceManager 有一个 serviceMap 字段,类型为:
ConcurrentHashMap<String, Map<String, Service>> serviceMap = new ConcurrentHashMap<>();
为什么 Service 内部的 clusterMap 不用 ConcurrentHashMap?因为对 Service 的所有写操作都已通过 synchronized 或外层锁控制,没有必要再用并发容器。核心方法如 addInstance()、removeInstance() 都是同步的。
二、一致性哈希环:Distro 协议的分片依据
在第 4 篇我们讲了 Distro 协议用一致性哈希环来决定哪条注册数据由哪个节点负责。这个环在代码中的对应类是 DistroHashRing(位于 naming/consistency/distro 包)。
核心逻辑:
-
集群启动时,每个节点在
DistroHashRing中注册自己的distroKey(通常就是 IP:Port 或 serverId)。 -
每个物理节点会映射出多个虚拟节点(默认 1000 个),通过 MD5 或 SHA 哈希计算位置,存入
TreeMap<Long, String>类型的ring中。 -
当一条注册数据到来(如
Service:com.alibaba.nacos.test:192.168.1.1:8080),DistroHashRing.chooseDistroKey(dataKey)方法:-
对
dataKey进行哈希得到一个 long 值。 -
在
ring.tailMap(hash)中找第一个大于等于该哈希的虚拟节点,如果找不到则取ring的第一个节点(形成环)。 -
取出该虚拟节点对应的物理节点 distroKey,这就是数据归属的责任节点。
-
注意:Distro 的哈希环只负责“写”的归属。对于读请求,所有节点都能直接从本地注册表返回数据,不走哈希环。
当集群扩缩容时,NodeChangeListener 监听到节点变更,会更新 ring,并触发数据迁移——将不再属于本节点的数据块通过 Distro 同步给新的责任节点。代码中通过 DistroProtocol.onMemberChange() 来完成这一过程,它内部会对比旧环和新环,计算需要移动的 DistroData 条目并执行传输。
三、请求处理的责任链模式:FilterChain
Nacos 对服务发现请求(注册、心跳、查询等)的处理,采用了一种优雅的 责任链(Chain of Responsibility)模式,允许在上线前插入各种预处理、校验、日志等逻辑而不修改核心代码。
入口在 InstanceController(HTTP)或 gRPC 对应的 InstanceRequestHandler。以 HTTP 注册为例:
// InstanceController.register()
public String register(@RequestBody Instance instance) {
serviceManager.registerInstance(namespaceId, serviceName, instance);
return "ok";
}
ServiceManager.registerInstance() 内部实际上会调用 Service.processClientBeat() 或 addInstance(),但在进入核心逻辑前,请求会经过一系列 Filter,定义在 com.alibaba.nacos.naming.web 包下的 FilterBase 子类,例如:
-
TrafficReviseFilter:修正流量控制参数。
-
AuthenticationFilter:鉴权。
-
DistroFilter:判断当前节点是否是该请求的责任节点,如果不是,转发到责任节点(AP 模式下的写请求转发)。
-
ClientBeatFilter:处理心跳请求的预处理。
这些 Filter 通过 @Order 注解排序,形成一个链。在 gRPC 模式(2.x)下,对应的逻辑放到了拦截器(Interceptor)中,本质思想相同。
这种设计让 Nacos 的处理流程可插拔、可扩展,是学习框架设计的绝佳案例。
四、推送通道:gRPC 双向流与 UDP 通知
客户端与服务端的通信在 Nacos 1.x 和 2.x 有较大差异,但源码中仍兼容两者。
4.1 gRPC 双向流(2.x 推荐)
客户端 SDK 调用服务发现时,首先会通过 NacosGrpcClient.connect() 建立与 Nacos 服务端的双向流长连接。服务端侧处理类主要是 com.alibaba.nacos.naming.remote.grpc.GrpcConnectionEventListener 和 NamingPushRequestHandler。
当服务实例列表发生变化时,事件发布到 NotifyCenter,PushService(或 NamingPushService)会拿到所有订阅该服务的客户端连接,然后通过 gRPC Stream 的 onNext() 发送 NotifySubscriberRequest 消息给客户端。
客户端侧(SDK)在 NacosGrpcClient 中有一个 StreamObserver 监听服务端推送,收到 NotifySubscriberResponse 后解析实例列表,更新本地缓存并通知业务监听器。
关键源码片段(服务端推送):
// PushService 简化逻辑
for (Subscriber subscriber : subscribers) {
Connection connection = subscriber.getConnection();
if (connection instanceof GrpcConnection) {
connection.request(new NotifySubscriberRequest(serviceName, instances), timeout);
}
}
4.2 UDP 推送(1.x 遗留)
Nacos 1.x 使用 UDP 作为推送通道,原因是轻量、适合大规模广播。服务端 PushService 会维护每个客户端订阅时上报的 UDP 端口和 IP,当数据变更时,向该客户端所在 IP 的 UDP 端口发送一个简单的 JSON 通知(通常只含服务名和 MD5 值)。客户端收到后,会拉取最新全量数据。
UDP 的缺点是丢包,所以客户端有兜底定时拉取。在 2.x 源码中,UDP 推送依然保留在 PushService 中,但 gRPC 是主流。
五、NamingService 注册全流程源码走读
我们以 Java 客户端为例,跟踪一个服务实例注册的完整代码路径。
5.1 客户端发起
NamingService namingService = NacosFactory.createNamingService(properties);
namingService.registerInstance("my-service", "192.168.1.100", 8080, "DEFAULT_GROUP");
SDK 内部实现类 NacosNamingService 的 registerInstance() 方法会构建一个 Instance 对象,设置 ephemeral=true(默认),然后调用 serverProxy.registerService(serviceName, groupName, instance)。
5.2 选择服务端节点
NamingProxy 负责向服务端发送 HTTP 或 gRPC 请求。它维护了一个 server 地址列表(来自配置),通过 ReWriteURL 和 Chooser 选择一台可用的服务器。如果请求失败,自动重试下一台。
5.3 HTTP 模式(1.x)
NamingProxy.registerService() 最终调用 HttpClient.request() 发送 POST 到 /nacos/v1/ns/instance,参数包括 serviceName、ip、port、namespaceId、groupName、clusterName、ephemeral 等。
5.4 gRPC 模式(2.x)
NamingGrpcClientProxy 将注册请求封装为 InstanceRequest 的 protobuf 消息,通过双向流 requestBiStream.onNext(request) 发送。服务端 InstanceRequestHandler 处理并返回响应。
5.5 服务端处理
服务端接收请求后,无论 HTTP 还是 gRPC,最终都走到 ServiceManager.registerInstance():
-
通过
namespaceId、group、serviceName找到或创建Service对象。 -
调用
Service.addInstance(instance)将实例加入对应Cluster的ephemeralInstances集合(临时实例)。 -
如果是临时实例,启动心跳超时检测计时器
ClientBeatCheckTask。 -
调用
ConsistencyService.put(key, instances)通知集群同步。对于 AP 模式,这就是 Distro 协议的入口。 -
DistroProtocol.onPut(key, value)会先判断自己是否责任节点,如果不是则转发到责任节点;如果是,则写入本地,并异步向其他节点同步。 -
注册完成后,
ServiceManager会发布一个ServiceChangedEvent,PushService监听到后,向所有订阅该服务的客户端推送最新的实例列表。
注册流程图总结:
客户端 -> NamingProxy(选服务器) -> HTTP/gRPC -> InstanceController/InstanceRequestHandler
-> ServiceManager.registerInstance() -> Service.addInstance() -> ConsistencyService.put()
-> DistroProtocol.onPut() (转发或本地写) -> 异步同步 -> PushService 推送
六、源码结构速览与关键类索引
为方便你后续阅读源码,这里列出 Nacos 2.x 服务发现模块的核心包和类:
-
naming.core:ServiceManager、Service、Cluster、Instance、ServiceOperator -
naming.consistency.distro:DistroProtocol、DistroHashRing、DistroConsistencyServiceImpl、DistroTaskEngine -
naming.remote.grpc:NamingPushRequestHandler、GrpcConnectionEventListener -
naming.remote.udp:UdpPushService -
naming.web:InstanceController、各种 Filter -
naming.monitor:HealthCheckTask、ClientBeatCheckTask -
client.naming(SDK 侧):NacosNamingService、NamingProxy、NamingGrpcClientProxy
你可以先从 InstanceController.reg() 方法入手,顺着调用链往下看,很快就能把整个流程串起来。
七、总结与下篇预告
本篇核心要点回顾:
-
注册表使用双层 Map 结构,按 namespace -> (group+service) -> Cluster -> Instance 高效存储。
-
一致性哈希环决定 Distro 写责任,通过虚拟节点和 TreeMap 实现,扩缩容时触发数据迁移。
-
请求处理采用 Filter 责任链,实现鉴权、流量修正、Distro 转发等可插拔逻辑。
-
推送通道 gRPC 双向流提供可靠低延迟,UDP 作为 1.x 遗留轻量补充。
-
注册全流程从 SDK 选择服务器,到服务端 ServiceManager 写入,再到 Distro 同步和 PushService 推送。
下一讲我们将继续源码之旅,进入 配置中心模块 的核心实现:配置存储模型、一致性快照与持久化、配置变更事件传播、客户端监听回调链、配置热加载的原理。让我们看看 Raft 和长轮询在代码中是如何无缝协作的。《第 8 篇:源码拆解:配置中心模块关键实现》即将呈现。
本系列持续更新,从原理到源码,让你彻底吃透 Nacos。如果本文对你有帮助,欢迎点赞、收藏,并关注我获取后续文章。若有具体源码问题,欢迎评论区交流!
更多推荐



所有评论(0)