系列导读
前六篇我们从架构、心跳、配置推送、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 就是具体的服务节点,包含 ipportweighthealthylastBeatmetadataclusterName 等字段。

为什么用这种双层结构?
第一层 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 包)。

核心逻辑:

  1. 集群启动时,每个节点在 DistroHashRing 中注册自己的 distroKey(通常就是 IP:Port 或 serverId)。

  2. 每个物理节点会映射出多个虚拟节点(默认 1000 个),通过 MD5 或 SHA 哈希计算位置,存入 TreeMap<Long, String> 类型的 ring 中。

  3. 当一条注册数据到来(如 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

当服务实例列表发生变化时,事件发布到 NotifyCenterPushService(或 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,参数包括 serviceNameipportnamespaceIdgroupNameclusterNameephemeral 等。

5.4 gRPC 模式(2.x)

NamingGrpcClientProxy 将注册请求封装为 InstanceRequest 的 protobuf 消息,通过双向流 requestBiStream.onNext(request) 发送。服务端 InstanceRequestHandler 处理并返回响应。

5.5 服务端处理

服务端接收请求后,无论 HTTP 还是 gRPC,最终都走到 ServiceManager.registerInstance()

  1. 通过 namespaceIdgroupserviceName 找到或创建 Service 对象。

  2. 调用 Service.addInstance(instance) 将实例加入对应 Cluster 的 ephemeralInstances 集合(临时实例)。

  3. 如果是临时实例,启动心跳超时检测计时器 ClientBeatCheckTask

  4. 调用 ConsistencyService.put(key, instances) 通知集群同步。对于 AP 模式,这就是 Distro 协议的入口。

  5. DistroProtocol.onPut(key, value) 会先判断自己是否责任节点,如果不是则转发到责任节点;如果是,则写入本地,并异步向其他节点同步。

  6. 注册完成后,ServiceManager 会发布一个 ServiceChangedEventPushService 监听到后,向所有订阅该服务的客户端推送最新的实例列表。

注册流程图总结:

客户端 -> NamingProxy(选服务器) -> HTTP/gRPC -> InstanceController/InstanceRequestHandler 
       -> ServiceManager.registerInstance() -> Service.addInstance() -> ConsistencyService.put() 
       -> DistroProtocol.onPut() (转发或本地写) -> 异步同步 -> PushService 推送

六、源码结构速览与关键类索引

为方便你后续阅读源码,这里列出 Nacos 2.x 服务发现模块的核心包和类:

  • naming.coreServiceManagerServiceClusterInstanceServiceOperator

  • naming.consistency.distroDistroProtocolDistroHashRingDistroConsistencyServiceImplDistroTaskEngine

  • naming.remote.grpcNamingPushRequestHandlerGrpcConnectionEventListener

  • naming.remote.udpUdpPushService

  • naming.webInstanceController、各种 Filter

  • naming.monitorHealthCheckTaskClientBeatCheckTask

  • client.naming(SDK 侧):NacosNamingServiceNamingProxyNamingGrpcClientProxy

你可以先从 InstanceController.reg() 方法入手,顺着调用链往下看,很快就能把整个流程串起来。


七、总结与下篇预告

本篇核心要点回顾:

  • 注册表使用双层 Map 结构,按 namespace -> (group+service) -> Cluster -> Instance 高效存储。

  • 一致性哈希环决定 Distro 写责任,通过虚拟节点和 TreeMap 实现,扩缩容时触发数据迁移。

  • 请求处理采用 Filter 责任链,实现鉴权、流量修正、Distro 转发等可插拔逻辑。

  • 推送通道 gRPC 双向流提供可靠低延迟,UDP 作为 1.x 遗留轻量补充。

  • 注册全流程从 SDK 选择服务器,到服务端 ServiceManager 写入,再到 Distro 同步和 PushService 推送。

下一讲我们将继续源码之旅,进入 配置中心模块 的核心实现:配置存储模型、一致性快照与持久化、配置变更事件传播、客户端监听回调链、配置热加载的原理。让我们看看 Raft 和长轮询在代码中是如何无缝协作的。《第 8 篇:源码拆解:配置中心模块关键实现》即将呈现。


本系列持续更新,从原理到源码,让你彻底吃透 Nacos。如果本文对你有帮助,欢迎点赞、收藏,并关注我获取后续文章。若有具体源码问题,欢迎评论区交流!

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐