服务端收到一个注册请求,从 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 的
InstanceRequestHandler,1.x 是 HTTP 的InstanceController。本文以 2.x gRPC 链路为主,同时标注 1.x 的对应类。
请求入口:gRPC Handler 还是 HTTP Controller
Nacos 2.x 客户端默认走 gRPC,所以注册请求先落到 gRPC 的请求处理器:
// 2.x gRPC 入口@ComponentpublicclassInstanceRequestHandlerextendsRequestHandler<InstanceRequest,InstanceResponse>{@OverridepublicInstanceResponsehandle(InstanceRequestrequest,RequestMetameta){// 根据请求类型分发:注册 / 注销switch(request.getType()){caseNamingRemoteConstants.REGISTER_INSTANCE:returnregisterInstance(...);caseNamingRemoteConstants.DE_REGISTER_INSTANCE:returnderegisterInstance(...);}}}对比一下两个版本的入口,方便你在源码里定位:
| 版本 | 通信方式 | 入口类 | 核心方法 |
|---|---|---|---|
| 2.x | gRPC | InstanceRequestHandler | handle(InstanceRequest, RequestMeta) |
| 1.x | HTTP | InstanceController | register(HttpServletRequest) |
两条路最终都汇聚到同一个地方——ServiceManager或 2.x 的EphemeralClientOperationServiceImpl。入口不同,核心处理逻辑复用。
五层链路:从请求到内存
把一次注册拆开,服务端内部走了 5 层:
第一层:Handler 解析请求
InstanceRequestHandler把 gRPC 的InstanceRequest拆出Service、Instance、clientId三个关键对象。clientId是 2.x 的重要变化——每个客户端连接对应一个唯一 clientId,服务端以 client 为维度管理它注册的所有实例。
第二层:ClientOperationService 处理注册
// EphemeralClientOperationServiceImpl(临时实例)@OverridepublicvoidregisterInstance(Serviceservice,Instanceinstance,StringclientId){// 1. 拿到该 clientId 对应的 Client 对象Clientclient=clientManager.getClient(clientId);// 2. 把实例塞进 Client 的持有列表InstancePublishInfoinstanceInfo=getPublishInfo(instance);client.addServiceInstance(service,instanceInfo);// 3. 发布注册事件,触发后续同步和通知NotifyCenter.publishEvent(newClientRegisterServiceEvent(service,clientId));}这里体现了 2.x 的核心设计变化:数据以 Client 为中心组织,而不是 1.x 的以 Service 为中心。一个客户端断连,它注册的所有实例一次性清理,效率更高。
第三层:ClientManager 管理连接
ClientManager维护clientId → Client的映射。Client 分两类:
| Client 类型 | 场景 | 生命周期 |
|---|---|---|
ConnectionBasedClient | gRPC 长连接注册的临时实例 | 连接断开即清理 |
IpPortBasedClient | HTTP 注册的临时实例 | 靠心跳保活 |
| 持久化实例 | 通过 Raft 存储 | 显式注销才删除 |
第四层:ConsistencyService 决定协议
这是最关键的分叉点。实例是临时的还是持久化的,决定了走哪个一致性协议:
临时实例走 AP,追求可用性——本节点写完立即返回,异步同步。持久化实例走 CP,追求一致性——必须多数派确认才算成功。
第五层:异步通知订阅者
注册成功后,NotifyCenter发布ServiceChangedEvent。订阅了这个服务的客户端会收到 gRPC 推送,拿到最新的实例列表。这一步是异步的,不阻塞注册请求的返回。
数据存哪了:内存注册表结构
很多人以为实例注册后存进了 MySQL。错。临时实例只存在内存里,MySQL 不参与临时实例的存储。
2.x 的内存注册表核心是这几个数据结构:
// ClientManager 中的核心 Map// key = clientId, value = Client 对象privatefinalConcurrentMap<String,Client>clients;// Client 内部持有的实例// key = Service, value = InstancePublishInfoprivatefinalConcurrentHashMap<Service,InstancePublishInfo>publishers;当客户端查询某个服务的实例列表时,ServiceStorage会遍历所有 Client,聚合出该服务下的全部实例:
ClientServiceIndexesManager维护了Service → 注册了该服务的 clientId 集合的反向索引。查询时不用扫全部 Client,直接通过索引定位,这是 2.x 性能优化的关键之一。
一次注册的完整事件链
Nacos 2.x 用事件驱动串起整个注册流程。理解这条事件链,就理解了注册模块:
| 事件 | 发布者 | 订阅者 | 作用 |
|---|---|---|---|
ClientRegisterServiceEvent | ClientOperationService | 索引管理器 | 更新反向索引 |
ClientChangedEvent | ClientManager | Distro 同步器 | 触发跨节点同步 |
ServiceChangedEvent | 索引管理器 | 推送引擎 | 通知订阅者 |
MetadataEvent | 元数据管理器 | 元数据存储 | 更新服务元信息 |
事件之间解耦,每个订阅者只关心自己的职责。这也是为什么注册请求能快速返回——重活都甩给异步事件处理了。
一张图带走:服务注册模块全链路
入口(gRPC/HTTP) → ClientOperationService → ClientManager 存内存 + 反向索引 → 一致性协议同步 → NotifyCenter 异步通知订阅者。临时实例全程不落 MySQL。
你们线上服务"注册上了但发现不到",最后排查出的根因是什么?评论区留数字:1=namespace 不匹配 2=group 配错 3=集群节点数据未同步 4=客户端缓存没刷新 5=健康检查把实例剔了