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

📅 2026/7/10 19:12:10 👁️ 阅读次数 📝 编程学习
服务端收到一个注册请求,从 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 的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.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拆出ServiceInstanceclientId三个关键对象。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 类型场景生命周期
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 对象privatefinalConcurrentMap<String,Client>clients;// Client 内部持有的实例// key = Service, value = InstancePublishInfoprivatefinalConcurrentHashMap<Service,InstancePublishInfo>publishers;

当客户端查询某个服务的实例列表时,ServiceStorage会遍历所有 Client,聚合出该服务下的全部实例:

查询 order-service 实例

ClientServiceIndexesManager
反向索引: Service → clientId 集合

遍历这些 Client
聚合实例

返回实例列表

ClientServiceIndexesManager维护了Service → 注册了该服务的 clientId 集合的反向索引。查询时不用扫全部 Client,直接通过索引定位,这是 2.x 性能优化的关键之一。


一次注册的完整事件链

Nacos 2.x 用事件驱动串起整个注册流程。理解这条事件链,就理解了注册模块:

事件发布者订阅者作用
ClientRegisterServiceEventClientOperationService索引管理器更新反向索引
ClientChangedEventClientManagerDistro 同步器触发跨节点同步
ServiceChangedEvent索引管理器推送引擎通知订阅者
MetadataEvent元数据管理器元数据存储更新服务元信息

事件之间解耦,每个订阅者只关心自己的职责。这也是为什么注册请求能快速返回——重活都甩给异步事件处理了。


一张图带走:服务注册模块全链路

通知

一致性

核心处理

请求入口

gRPC: InstanceRequestHandler

HTTP: InstanceController(1.x)

ClientOperationService
registerInstance

ClientManager
clientId → Client

ClientServiceIndexesManager
Service → clientId 反向索引

Distro 协议
临时实例 AP

JRaft 协议
持久化实例 CP

NotifyCenter
ServiceChangedEvent

推送引擎
gRPC 通知订阅者

入口(gRPC/HTTP) → ClientOperationService → ClientManager 存内存 + 反向索引 → 一致性协议同步 → NotifyCenter 异步通知订阅者。临时实例全程不落 MySQL。


你们线上服务"注册上了但发现不到",最后排查出的根因是什么?评论区留数字:1=namespace 不匹配 2=group 配错 3=集群节点数据未同步 4=客户端缓存没刷新 5=健康检查把实例剔了