Nacos2.x核心原理分析
大家好,我是joker,希望你快乐。
上一篇Nacos简介中介绍了 Nacos 的核心功能和使用方式,这篇我们通过跟踪源码的方式,进一步探究 Nacos 2.x 是如何实现高性能的服务注册发现和配置动态推送的。
Nacos 2.x 相比 1.x 最大的架构升级在于通信协议——从 HTTP 短连接升级为 gRPC 长连接。这个变化影响了注册中心、配置中心的整个工作流程,下面我们从通信层开始,逐层深入。
gRPC通信层的建立
Nacos 2.x 性能提升的关键在于用 gRPC 长连接取代了 HTTP 短连接,核心实现位于 nacos-remote 模块。
服务端:BaseGrpcServer与RequestHandlerRegistry
服务端启动时,BaseGrpcServer 会初始化一个 Netty 服务器,监听默认端口 9848(gRPC 端口 = 主端口 + 1000)。
两个核心职责:
- 连接管理:客户端连接建立时,服务端创建
GrpcConnection对象,注册到ConnectionManager中。ConnectionManager维护了所有活跃的客户端连接,这是服务端主动推送的基础 - 请求路由:
RequestHandlerRegistry内部维护了一个 Map,将不同类型的请求映射到对应的处理器
@Service
public class RequestHandlerRegistry implements ApplicationListener<ContextRefreshedEvent> {
Map<String, RequestHandler> registryHandlers = new HashMap<>();
public RequestHandler getByRequestType(String requestType) {
return registryHandlers.get(requestType);
}
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
Map<String, RequestHandler> beansOfType =
event.getApplicationContext().getBeansOfType(RequestHandler.class);
for (RequestHandler requestHandler : beansOfType.values()) {
Class tClass = (Class) ((ParameterizedType) requestHandler.getClass()
.getGenericSuperclass()).getActualTypeArguments()[0];
registryHandlers.putIfAbsent(tClass.getSimpleName(), requestHandler);
}
}
}
可以看到,RequestHandlerRegistry 在 Spring 容器启动时自动扫描所有 RequestHandler 实现类,以请求类型的简单类名为 key 注册到 Map 中。当 gRPC 请求到达时,根据请求类型从 Map 中找到对应的 Handler 执行业务逻辑。
目前 Nacos 支持的主要请求类型包括:InstanceRequest、SubscribeServiceRequest、ConfigBatchListenRequest、ConfigQueryRequest 等。
客户端:GrpcClient与RpcClient
客户端通过 GrpcClient 与服务端建立连接,GrpcClient 继承自 RpcClient,后者封装了连接管理、重连、请求发送等通用逻辑。
public abstract class RpcClient {
public final void start() throws NacosException {
// 启动后台线程,负责连接到服务端
// 连接成功后触发 onConnected() 回调
// 连接断开后触发 onDisConnected() 回调,自动重连
}
}
Nacos 2.x 利用 gRPC 的双向流(Bi-directional Streaming)特性。客户端通过 requestBiStream 与服务端保持一个持久的通信通道,不仅用于发送请求,更重要的是监听来自服务端的推送事件,如配置变更通知、服务实例变更通知。
服务注册源码分析
客户端注册流程
- 入口类
NacosNamingService,初始化时创建NamingClientProxyDelegate
public class NacosNamingService implements NamingService {
private void init(Properties properties) throws NacosException {
this.clientProxy = new NamingClientProxyDelegate(
this.namespace, serviceInfoHolder, nacosClientProperties, changeNotifier);
}
}
NamingClientProxyDelegate内部同时维护了 HTTP 代理和 gRPC 代理,2.x 优先使用 gRPC
public NamingClientProxyDelegate(String namespace, ServiceInfoHolder serviceInfoHolder,
NacosClientProperties properties, InstancesChangeNotifier changeNotifier) throws NacosException {
this.httpClientProxy = new NamingHttpClientProxy(namespace, securityProxy, serverListManager, properties);
this.grpcClientProxy = new NamingGrpcClientProxy(namespace, securityProxy, serverListManager, properties,
serviceInfoHolder);
}
NamingGrpcClientProxy启动时建立 gRPC 连接,注册连接监听器和推送请求处理器
public class NamingGrpcClientProxy {
private void start(ServerListFactory serverListFactory, ServiceInfoHolder serviceInfoHolder) throws NacosException {
rpcClient.serverListFactory(serverListFactory);
rpcClient.registerConnectionListener(redoService);
rpcClient.registerServerRequestHandler(new NamingPushRequestHandler(serviceInfoHolder));
rpcClient.start();
}
}
- 注册实例时,通过 gRPC 发送
InstanceRequest到服务端
public void registerInstance(String serviceName, Instance instance) throws NacosException {
// 构建 InstanceRequest,类型为注册
InstanceRequest request = new InstanceRequest(namespaceId, serviceName, NamingRemoteConstants.REGISTER_INSTANCE, instance);
// 通过 gRPC 发送到服务端
requestToServer(request, InstanceResponse.class);
// 注册成功后,redoService 记录注册信息,用于连接断开重连后自动重新注册
redoService.instanceRegistered(serviceName, instance);
}
服务端处理流程
- 服务注册请求由
InstanceRequestHandler处理
public class InstanceRequestHandler extends RequestHandler<InstanceRequest, InstanceResponse> {
@Override
public InstanceResponse handle(InstanceRequest request, RequestMeta meta) throws NacosException {
// 根据 serviceName 从 ServiceManager 获取或创建 Service 对象
// ServiceManager 内部维护 ConcurrentHashMap,即 serviceMap
// 将 Instance 添加到 Service 的 InstanceManager 中
// 对于 ephemeral=true 的临时实例,信息仅存储在内存中
}
}
- 实例变更后,服务端通过
PushService遍历订阅者列表,通过 gRPC 长连接将最新实例列表推送给订阅的客户端
连接断开自动重注册
NamingGrpcRedoService 是一个关键的容错组件,它实现了 ConnectionEventListener 接口:
- 连接建立时:重新执行所有已注册的服务注册和订阅操作
- 连接断开时:标记注册状态,等待重连
这解决了 gRPC 连接断开后客户端服务信息丢失的问题,无需人工干预。
配置变更推送源码分析
配置中心的核心是实现配置的动态更新,Nacos 2.x 采用了推拉结合的模式。
服务端:配置发布与事件驱动
-
配置变更请求到达
ConfigController#publishConfig,先调用PersistService将配置写入 MySQL 数据库 -
数据落库成功后,通过
NotifyCenter发布配置变更事件
ConfigChangePublisher.notifyConfigChange(
new ConfigDataChangeEvent(false, dataId, group, tenant, time.getTime()));
NotifyCenter 是 Nacos 内部的轻量级事件总线,基于阻塞队列 + 异步任务机制,实现模块间解耦。
- 订阅了
ConfigDataChangeEvent的RpcConfigChangeNotifier被触发
@Component(value = "rpcConfigChangeNotifier")
public class RpcConfigChangeNotifier extends Subscriber<LocalDataChangeEvent> {
@Override
public void onEvent(LocalDataChangeEvent event) {
// 构建 ConfigChangeNotifyRequest,包含变更配置的 dataId、group 等信息
// 通过 ConnectionManager 找到所有订阅了该配置的客户端连接
// 调用 connection.request(notifyRequest) 通过 gRPC 长连接主动推送
}
}
客户端:监听与回调
-
客户端核心是
ClientWorker,负责维护所有配置的本地缓存和监听器 -
注册监听器时,
ClientWorker将 dataId 和对应的 Listener 封装成CacheData对象,存入全局的ConcurrentHashMap(cacheMap)中 -
客户端启动时通过 gRPC 双向流发送
ConfigBatchListenRequest,向服务端注册关注的配置列表 -
当
GrpcClient通过双向流通道接收到服务端的ConfigChangeNotifyRequest后:
// ConfigRpcTransportClient 收到变更通知
// 向阻塞队列添加通知
listenExecutebell.offer(bellItem);
// 延时线程池循环从队列取消息
// 有消息立刻发起 ConfigQueryRequest 拉取最新配置内容
// 从 cacheMap 中找到对应的 CacheData
// 调用 Listener#receiveConfigInfo() 方法,完成配置的动态刷新
注意:Nacos 2.x 的配置推送是推拉结合——服务端推送的只是变更通知,客户端收到通知后再主动拉取最新配置内容。这样设计避免了服务端推送大体积配置数据造成的网络压力。
1.x vs 2.x 配置推送对比
| 维度 | 1.x 长轮询 | 2.x gRPC 推送 |
|---|---|---|
| 通信方式 | HTTP 长轮询,最多挂起 29.5 秒 | gRPC 双向流实时推送 |
| 推送延迟 | 1-30 秒 | 毫秒级,P99 < 300ms |
| 网络开销 | 频繁建连,TIME_WAIT 堆积 | 长连接复用,一条连接承载所有配置监听 |
| 容错机制 | 轮询兜底 | 5 分钟全量拉取兜底 + 本地快照 |
一致性协议:Distro与Raft
Nacos 区分临时实例和持久实例,分别采用不同的一致性协议,这是 Nacos 同时支持 AP 和 CP 的关键设计。
Distro协议(AP模式)
Distro 是 Nacos 自研的最终一致性协议,设计哲学是"人人为我,我为人人"。
核心机制:
- 数据分片:每个节点负责全量数据的一个子集(责任分区),通过哈希算法确定。每个节点是自己责任分区的"主"
- 异步同步:数据变更先写入本地内存,然后异步批量复制到其他节点
- 读写任意节点:客户端可以向任意节点读写,如果请求的数据不属于该节点的责任分区,该节点会将请求转发给负责的节点
- 健康检查与自愈:节点间定期互发心跳,交换数据摘要。发现数据不一致时,触发增量或全量同步
public class DistroProtocol {
public void sync(DistroKey key, DataOperation action) {
for (Member member : memberManager.allMembersWithoutSelf()) {
syncToTarget(key, action, member.getAddress(), delay);
}
}
public boolean onReceive(DistroData data) {
DistroDataProcessor processor = findDataProcessor(data.getType());
return processor.processData(data);
}
}
适用场景:服务注册发现中的临时实例,强调高可用性,容忍短暂不一致。
Raft协议(CP模式)
Nacos 使用 JRaft 实现 Raft 协议,保证配置数据的强一致性。
核心机制:
- Leader 选举:节点通过投票机制选出 Leader,只有 Leader 能处理写请求。获得多数节点投票的 Candidate 成为新 Leader
- 日志复制:写操作先追加到 Leader 的日志中,广播给所有 Follower。多数派确认后,数据才提交生效
- 数据持久化:日志写入本地 WAL(Write-Ahead Log)文件,定期生成快照
适用场景:配置中心、持久化实例存储,对一致性要求高。
Distro vs Raft对比
| 维度 | Distro 协议 | Raft 协议 |
|---|---|---|
| 一致性模型 | 最终一致性(AP) | 强一致性(CP) |
| 节点角色 | 全节点对等,无 Leader | Leader / Follower / Candidate |
| 数据存储 | 分片存储,每个节点存部分数据 | 全量副本,所有节点存完整数据 |
| 写性能 | 高(本地写入 + 异步传播) | 较低(需多数节点确认) |
| 容错 | 容忍网络分区 | 要求 Leader 存活才能写入 |
| 适用模块 | Naming(服务注册发现) | Config(配置中心) |
| 故障恢复 | 自动数据补齐 | 选举新 Leader |
Nacos 的巧妙之处在于:两种协议在同一集群中独立运行,通过 ephemeral 字段区分实例类型,模块隔离避免协议冲突。
Nacos 2.x服务端数据结构
1.x 和 2.x 在服务端的数据结构也有显著变化:
1.x 数据结构:读写共用一份注册表
Map<namespace, Map<group:serviceName, Service>>
采用 Copy On Write 策略,写时复制,读无锁。
2.x 数据结构:读写分离
写注册表:
ConcurrentMap<Service, Set> publisherIndexes
ConcurrentMap<String, IpPortBasedClient> clients
读注册表:
ConcurrentMap<Service, ServiceInfo> serviceDataIndexes
2.x 将写注册表和读注册表分离,写操作更新 publisherIndexes 和 clients,读操作直接从 serviceDataIndexes 获取,减少了读写竞争。
另外 2.x 拉取的服务实例列表会保存到本地磁盘作为快照,在 Nacos 服务不可用时可以降级使用本地缓存。
参考文档
来源:http://www.cnblogs.com/Crazy_Joker
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

浙公网安备 33010602011771号