Nacos 第 7 篇:源码拆解:服务发现模块关键实现

Nacos 第 7 篇:源码拆解:服务发现模块关键实现 系列导读前六篇我们从架构、心跳、配置推送、Distro、Raft再到集群部署把 Nacos 的理论体系全部铺开了。但真正让你从“会用”跳到“通透”的是打开源码看这些设计是如何一行行落地的。从本篇开始我们将连续两篇深入源码今天聚焦服务发现模块Naming注册表用什么结构一致性哈希环如何指挥 Distro 同步gRPC 推送通道怎样建立责任链如何优雅处理请求NamingService 注册全流程是怎样的打开你的 IDE或 GitHub我们开始挖地三尺。一、服务注册表双层 ConcurrentHashMap 的内存结构Nacos 的服务注册表全在内存中持久化另有逻辑它的核心数据结构是一个双层 Map定义在com.alibaba.nacos.naming.core.ServiceManager中。抽象出来就是Mapnamespace, Mapgroup::serviceName, Service其中Service内部又包含MapString, Cluster clusterMap // clusterName - ClusterCluster内部维护着临时实例和持久实例的集合SetInstance ephemeralInstances // 临时实例默认大多数 SetInstance persistentInstances // 持久实例每个Instance就是具体的服务节点包含ip、port、weight、healthy、lastBeat、metadata、clusterName等字段。为什么用这种双层结构第一层namespace隔离了不同租户/环境第二层group::serviceName隔离了业务服务第三层Cluster支持多机房部署和就近访问。查询服务时根据 namespace group serviceName 可以直接定位到Service对象再从clusterMap中取出对应集群的实例列表。时间复杂度接近 O(1)。在实际代码中ServiceManager有一个serviceMap字段类型为ConcurrentHashMapString, MapString, 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 哈希计算位置存入TreeMapLong, String类型的ring中。当一条注册数据到来如Service:com.alibaba.nacos.test:192.168.1.1:8080DistroHashRing.chooseDistroKey(dataKey)方法对dataKey进行哈希得到一个 long 值。在ring.tailMap(hash)中找第一个大于等于该哈希的虚拟节点如果找不到则取ring的第一个节点形成环。取出该虚拟节点对应的物理节点 distroKey这就是数据归属的责任节点。注意Distro 的哈希环只负责“写”的归属。对于读请求所有节点都能直接从本地注册表返回数据不走哈希环。当集群扩缩容时NodeChangeListener监听到节点变更会更新ring并触发数据迁移——将不再属于本节点的数据块通过 Distro 同步给新的责任节点。代码中通过DistroProtocol.onMemberChange()来完成这一过程它内部会对比旧环和新环计算需要移动的DistroData条目并执行传输。三、请求处理的责任链模式FilterChainNacos 对服务发现请求注册、心跳、查询等的处理采用了一种优雅的责任链Chain of Responsibility模式允许在上线前插入各种预处理、校验、日志等逻辑而不修改核心代码。入口在InstanceControllerHTTP或 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对象设置ephemeraltrue默认然后调用serverProxy.registerService(serviceName, groupName, instance)。5.2 选择服务端节点NamingProxy负责向服务端发送 HTTP 或 gRPC 请求。它维护了一个 server 地址列表来自配置通过ReWriteURL和Chooser选择一台可用的服务器。如果请求失败自动重试下一台。5.3 HTTP 模式1.xNamingProxy.registerService()最终调用HttpClient.request()发送 POST 到/nacos/v1/ns/instance参数包括serviceName、ip、port、namespaceId、groupName、clusterName、ephemeral等。5.4 gRPC 模式2.xNamingGrpcClientProxy将注册请求封装为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会发布一个ServiceChangedEventPushService监听到后向所有订阅该服务的客户端推送最新的实例列表。注册流程图总结客户端 - NamingProxy(选服务器) - HTTP/gRPC - InstanceController/InstanceRequestHandler - ServiceManager.registerInstance() - Service.addInstance() - ConsistencyService.put() - DistroProtocol.onPut() (转发或本地写) - 异步同步 - PushService 推送六、源码结构速览与关键类索引为方便你后续阅读源码这里列出 Nacos 2.x 服务发现模块的核心包和类naming.coreServiceManager、Service、Cluster、Instance、ServiceOperatornaming.consistency.distroDistroProtocol、DistroHashRing、DistroConsistencyServiceImpl、DistroTaskEnginenaming.remote.grpcNamingPushRequestHandler、GrpcConnectionEventListenernaming.remote.udpUdpPushServicenaming.webInstanceController、各种 Filternaming.monitorHealthCheckTask、ClientBeatCheckTaskclient.namingSDK 侧NacosNamingService、NamingProxy、NamingGrpcClientProxy你可以先从InstanceController.reg()方法入手顺着调用链往下看很快就能把整个流程串起来。七、总结与下篇预告本篇核心要点回顾注册表使用双层 Map 结构按 namespace - (groupservice) - Cluster - Instance 高效存储。一致性哈希环决定 Distro 写责任通过虚拟节点和 TreeMap 实现扩缩容时触发数据迁移。请求处理采用 Filter 责任链实现鉴权、流量修正、Distro 转发等可插拔逻辑。推送通道 gRPC 双向流提供可靠低延迟UDP 作为 1.x 遗留轻量补充。注册全流程从 SDK 选择服务器到服务端 ServiceManager 写入再到 Distro 同步和 PushService 推送。下一讲我们将继续源码之旅进入配置中心模块的核心实现配置存储模型、一致性快照与持久化、配置变更事件传播、客户端监听回调链、配置热加载的原理。让我们看看 Raft 和长轮询在代码中是如何无缝协作的。《第 8 篇源码拆解配置中心模块关键实现》即将呈现。本系列持续更新从原理到源码让你彻底吃透 Nacos。如果本文对你有帮助欢迎点赞、收藏并关注我获取后续文章。若有具体源码问题欢迎评论区交流