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)。

两个核心职责:

  1. 连接管理:客户端连接建立时,服务端创建 GrpcConnection 对象,注册到 ConnectionManager 中。ConnectionManager 维护了所有活跃的客户端连接,这是服务端主动推送的基础
  2. 请求路由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 支持的主要请求类型包括:InstanceRequestSubscribeServiceRequestConfigBatchListenRequestConfigQueryRequest 等。

客户端: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 与服务端保持一个持久的通信通道,不仅用于发送请求,更重要的是监听来自服务端的推送事件,如配置变更通知、服务实例变更通知。

服务注册源码分析

客户端注册流程

  1. 入口类 NacosNamingService,初始化时创建 NamingClientProxyDelegate
public class NacosNamingService implements NamingService {

    private void init(Properties properties) throws NacosException {
        this.clientProxy = new NamingClientProxyDelegate(
            this.namespace, serviceInfoHolder, nacosClientProperties, changeNotifier);
    }
}
  1. 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);
}
  1. 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();
    }
}
  1. 注册实例时,通过 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);
}

服务端处理流程

  1. 服务注册请求由 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 的临时实例,信息仅存储在内存中
    }
}
  1. 实例变更后,服务端通过 PushService 遍历订阅者列表,通过 gRPC 长连接将最新实例列表推送给订阅的客户端

连接断开自动重注册

NamingGrpcRedoService 是一个关键的容错组件,它实现了 ConnectionEventListener 接口:

  • 连接建立时:重新执行所有已注册的服务注册和订阅操作
  • 连接断开时:标记注册状态,等待重连

这解决了 gRPC 连接断开后客户端服务信息丢失的问题,无需人工干预。

配置变更推送源码分析

配置中心的核心是实现配置的动态更新,Nacos 2.x 采用了推拉结合的模式。

服务端:配置发布与事件驱动

  1. 配置变更请求到达 ConfigController#publishConfig,先调用 PersistService 将配置写入 MySQL 数据库

  2. 数据落库成功后,通过 NotifyCenter 发布配置变更事件

ConfigChangePublisher.notifyConfigChange(
    new ConfigDataChangeEvent(false, dataId, group, tenant, time.getTime()));

NotifyCenter 是 Nacos 内部的轻量级事件总线,基于阻塞队列 + 异步任务机制,实现模块间解耦。

  1. 订阅了 ConfigDataChangeEventRpcConfigChangeNotifier 被触发
@Component(value = "rpcConfigChangeNotifier")
public class RpcConfigChangeNotifier extends Subscriber<LocalDataChangeEvent> {

    @Override
    public void onEvent(LocalDataChangeEvent event) {
        // 构建 ConfigChangeNotifyRequest,包含变更配置的 dataId、group 等信息
        // 通过 ConnectionManager 找到所有订阅了该配置的客户端连接
        // 调用 connection.request(notifyRequest) 通过 gRPC 长连接主动推送
    }
}

客户端:监听与回调

  1. 客户端核心是 ClientWorker,负责维护所有配置的本地缓存和监听器

  2. 注册监听器时,ClientWorker 将 dataId 和对应的 Listener 封装成 CacheData 对象,存入全局的 ConcurrentHashMap(cacheMap)中

  3. 客户端启动时通过 gRPC 双向流发送 ConfigBatchListenRequest,向服务端注册关注的配置列表

  4. 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 自研的最终一致性协议,设计哲学是"人人为我,我为人人"。

核心机制:

  1. 数据分片:每个节点负责全量数据的一个子集(责任分区),通过哈希算法确定。每个节点是自己责任分区的"主"
  2. 异步同步:数据变更先写入本地内存,然后异步批量复制到其他节点
  3. 读写任意节点:客户端可以向任意节点读写,如果请求的数据不属于该节点的责任分区,该节点会将请求转发给负责的节点
  4. 健康检查与自愈:节点间定期互发心跳,交换数据摘要。发现数据不一致时,触发增量或全量同步
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 协议,保证配置数据的强一致性。

核心机制:

  1. Leader 选举:节点通过投票机制选出 Leader,只有 Leader 能处理写请求。获得多数节点投票的 Candidate 成为新 Leader
  2. 日志复制:写操作先追加到 Leader 的日志中,广播给所有 Follower。多数派确认后,数据才提交生效
  3. 数据持久化:日志写入本地 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 服务不可用时可以降级使用本地缓存。

参考文档

Nacos 官方文档

Nacos 自研 Distro 协议

Nacos GitHub

posted @ 2026-06-30 22:47  Crazy_Joker  阅读(44)  评论(0)    收藏  举报