服务端收到一个注册请求,从 Controller 到内存 Map 走了 5 层:服务注册模块源码走读

发布时间:2026/7/10 19:11:14

服务端收到一个注册请求,从 Controller 到内存 Map 走了 5 层:服务注册模块源码走读 服务端收到一个注册请求从 Controller 到内存 Map 走了 5 层服务注册模块源码走读文章目录服务端收到一个注册请求从 Controller 到内存 Map 走了 5 层服务注册模块源码走读客户端发出注册请求之后服务端做了什么请求入口gRPC Handler 还是 HTTP Controller五层链路从请求到内存第一层Handler 解析请求第二层ClientOperationService 处理注册第三层ClientManager 管理连接第四层ConsistencyService 决定协议第五层异步通知订阅者数据存哪了内存注册表结构一次注册的完整事件链一张图带走服务注册模块全链路客户端发出注册请求之后服务端做了什么上一篇讲客户端 SDK 时我们看到registerInstance最终通过 gRPC 把一个InstanceRequest发给了服务端。那服务端收到这个请求之后呢它要把这个实例存到哪里存的时候走 Distro 还是 Raft其他节点怎么知道多了一个实例这篇文章站在服务端视角从请求入口开始拆开一次注册请求在 Nacos-Server 内部的完整链路Controller 收请求 → ServiceManager 管理服务 → ConsistencyService 决定一致性协议 → 数据落内存 → 异步通知订阅者。每一步给出类名和方法你可以对着 nacos-naming 模块的源码跟。⚠️ 本文基于 Nacos 2.3.0 源码。2.x 的注册入口是 gRPC 的InstanceRequestHandler1.x 是 HTTP 的InstanceController。本文以 2.x gRPC 链路为主同时标注 1.x 的对应类。请求入口gRPC Handler 还是 HTTP ControllerNacos 2.x 客户端默认走 gRPC所以注册请求先落到 gRPC 的请求处理器// 2.x gRPC 入口ComponentpublicclassInstanceRequestHandlerextendsRequestHandlerInstanceRequest,InstanceResponse{OverridepublicInstanceResponsehandle(InstanceRequestrequest,RequestMetameta){// 根据请求类型分发注册 / 注销switch(request.getType()){caseNamingRemoteConstants.REGISTER_INSTANCE:returnregisterInstance(...);caseNamingRemoteConstants.DE_REGISTER_INSTANCE:returnderegisterInstance(...);}}}对比一下两个版本的入口方便你在源码里定位版本通信方式入口类核心方法2.xgRPCInstanceRequestHandlerhandle(InstanceRequest, RequestMeta)1.xHTTPInstanceControllerregister(HttpServletRequest)两条路最终都汇聚到同一个地方——ServiceManager或 2.x 的EphemeralClientOperationServiceImpl。入口不同核心处理逻辑复用。五层链路从请求到内存把一次注册拆开服务端内部走了 5 层订阅者通知ConsistencyServiceClientManagerClientOperationServiceInstanceRequestHandler客户端 gRPC订阅者通知ConsistencyServiceClientManagerClientOperationServiceInstanceRequestHandler客户端 gRPCInstanceRequest(REGISTER)registerInstance(service, instance, clientId)更新 Client 持有的实例触发 ClientRegisterServiceEventDistro 协议同步到其他节点发布 ServiceChangedEvent推送给订阅该服务的客户端第一层Handler 解析请求InstanceRequestHandler把 gRPC 的InstanceRequest拆出Service、Instance、clientId三个关键对象。clientId是 2.x 的重要变化——每个客户端连接对应一个唯一 clientId服务端以 client 为维度管理它注册的所有实例。第二层ClientOperationService 处理注册// EphemeralClientOperationServiceImpl临时实例OverridepublicvoidregisterInstance(Serviceservice,Instanceinstance,StringclientId){// 1. 拿到该 clientId 对应的 Client 对象ClientclientclientManager.getClient(clientId);// 2. 把实例塞进 Client 的持有列表InstancePublishInfoinstanceInfogetPublishInfo(instance);client.addServiceInstance(service,instanceInfo);// 3. 发布注册事件触发后续同步和通知NotifyCenter.publishEvent(newClientRegisterServiceEvent(service,clientId));}这里体现了 2.x 的核心设计变化数据以 Client 为中心组织而不是 1.x 的以 Service 为中心。一个客户端断连它注册的所有实例一次性清理效率更高。第三层ClientManager 管理连接ClientManager维护clientId → Client的映射。Client 分两类Client 类型场景生命周期ConnectionBasedClientgRPC 长连接注册的临时实例连接断开即清理IpPortBasedClientHTTP 注册的临时实例靠心跳保活持久化实例通过 Raft 存储显式注销才删除第四层ConsistencyService 决定协议这是最关键的分叉点。实例是临时的还是持久化的决定了走哪个一致性协议true 临时实例false 持久化实例注册请求ephemeral?Distro 协议 (AP)本节点先写异步同步其他节点JRaft 协议 (CP)Leader 提交日志多数派确认写入内存注册表临时实例走 AP追求可用性——本节点写完立即返回异步同步。持久化实例走 CP追求一致性——必须多数派确认才算成功。第五层异步通知订阅者注册成功后NotifyCenter发布ServiceChangedEvent。订阅了这个服务的客户端会收到 gRPC 推送拿到最新的实例列表。这一步是异步的不阻塞注册请求的返回。数据存哪了内存注册表结构很多人以为实例注册后存进了 MySQL。错。临时实例只存在内存里MySQL 不参与临时实例的存储。2.x 的内存注册表核心是这几个数据结构// ClientManager 中的核心 Map// key clientId, value Client 对象privatefinalConcurrentMapString,Clientclients;// Client 内部持有的实例// key Service, value InstancePublishInfoprivatefinalConcurrentHashMapService,InstancePublishInfopublishers;当客户端查询某个服务的实例列表时ServiceStorage会遍历所有 Client聚合出该服务下的全部实例查询 order-service 实例ClientServiceIndexesManager反向索引: Service → clientId 集合遍历这些 Client聚合实例返回实例列表ClientServiceIndexesManager维护了Service → 注册了该服务的 clientId 集合的反向索引。查询时不用扫全部 Client直接通过索引定位这是 2.x 性能优化的关键之一。一次注册的完整事件链Nacos 2.x 用事件驱动串起整个注册流程。理解这条事件链就理解了注册模块事件发布者订阅者作用ClientRegisterServiceEventClientOperationService索引管理器更新反向索引ClientChangedEventClientManagerDistro 同步器触发跨节点同步ServiceChangedEvent索引管理器推送引擎通知订阅者MetadataEvent元数据管理器元数据存储更新服务元信息事件之间解耦每个订阅者只关心自己的职责。这也是为什么注册请求能快速返回——重活都甩给异步事件处理了。一张图带走服务注册模块全链路通知一致性核心处理请求入口gRPC: InstanceRequestHandlerHTTP: InstanceController(1.x)ClientOperationServiceregisterInstanceClientManagerclientId → ClientClientServiceIndexesManagerService → clientId 反向索引Distro 协议临时实例 APJRaft 协议持久化实例 CPNotifyCenterServiceChangedEvent推送引擎gRPC 通知订阅者入口(gRPC/HTTP) → ClientOperationService → ClientManager 存内存 反向索引 → 一致性协议同步 → NotifyCenter 异步通知订阅者。临时实例全程不落 MySQL。你们线上服务注册上了但发现不到最后排查出的根因是什么评论区留数字1namespace 不匹配 2group 配错 3集群节点数据未同步 4客户端缓存没刷新 5健康检查把实例剔了

相关新闻