Nacos Client 源码分析(二)服务订阅与推送消息处理

本文使用的 Nacos 版本为 2.2.2

1. 概述

在上一篇文章《Nacos Client 源码分析(一)事件的发布与订阅》分析了 Nacos Client 的发布订阅机制,但我们现在还不清楚NotifyCenterpublishEvent方法是怎么被调用的以及客户端向服务端订阅服务的具体流程。下面我们对继续分析 Nacos 的源码。

2. 服务订阅

还是从NacosNamingServiceinit方法开始分析,注意到以下几句代码。notifierEventScope标识一个事件的作用范围,也可以理解为事件是面向哪一个客户端,后面会用到。ServiceInfoHolder保存了客户端请求到的服务信息。NamingClientProxyDelegate是命名服务客户端代理的委托,其内部实际使用的是NamingHttpClientProxyNamingGrpcClientProxy

    private void init(Properties properties) throws NacosException {
        // ...
        this.notifierEventScope = UUID.randomUUID().toString();
        // ...
        this.serviceInfoHolder = new ServiceInfoHolder(namespace, this.notifierEventScope, nacosClientProperties);
        this.clientProxy = new NamingClientProxyDelegate(this.namespace, serviceInfoHolder, nacosClientProperties, changeNotifier);
    }

NamingClientProxyDelegate的构造方法如下。

    public NamingClientProxyDelegate(String namespace, ServiceInfoHolder serviceInfoHolder, NacosClientProperties properties,
            InstancesChangeNotifier changeNotifier) throws NacosException {
        // ...
        this.serviceInfoHolder = serviceInfoHolder;
        // ...
        this.httpClientProxy = new NamingHttpClientProxy(namespace, securityProxy, serverListManager, properties);
        this.grpcClientProxy = new NamingGrpcClientProxy(namespace, securityProxy, serverListManager, properties,
                serviceInfoHolder);
    }

我们调用的subscribe方法最终是调用了NamingClientProxyDelegate类的subscribe方法。

    @Override
    public void subscribe(String serviceName, String groupName, List<String> clusters, EventListener listener)
            throws NacosException {
        if (null == listener) {
            return;
        }
        String clusterString = StringUtils.join(clusters, ",");
        changeNotifier.registerListener(groupName, serviceName, clusterString, listener);
        // 发起订阅
        clientProxy.subscribe(serviceName, groupName, clusterString);
    }

NamingClientProxyDelegate类的subscribe方法中,首先会计算出要订阅的服务标识(组名+服务名+集群),然后在serviceInfoHolder 中取出服务信息的缓存,如果服务信息不存在或是这个服务没有被订阅就会使用grpcClientProxy发起订阅请求,最后serviceInfoHolder会对返回的服务信息ServiceInfo进行处理。服务信息中包含了订阅服务的实例。

    @Override
    public ServiceInfo subscribe(String serviceName, String groupName, String clusters) throws NacosException {
        NAMING_LOGGER.info("[SUBSCRIBE-SERVICE] service:{}, group:{}, clusters:{} ", serviceName, groupName, clusters);
        // 1. 计算服务标识
        String serviceNameWithGroup = NamingUtils.getGroupedName(serviceName, groupName);
        String serviceKey = ServiceInfo.getKey(serviceNameWithGroup, clusters);
        serviceInfoUpdateService.scheduleUpdateIfAbsent(serviceName, groupName, clusters);
        // 2. 获取服务信息缓存
        ServiceInfo result = serviceInfoHolder.getServiceInfoMap().get(serviceKey);
        // 3. 发起订阅请求
        if (null == result || !isSubscribed(serviceName, groupName, clusters)) {
            result = grpcClientProxy.subscribe(serviceName, groupName, clusters);
        }
        // 4. 处理服务信息
        serviceInfoHolder.processServiceInfo(result);
        return result;
    }


ServiceInfoHolder在内部维护了一个ConcurrentMap用于缓存服务信息。

processServiceInfo方法的如下,如果服务信息变更会触发事件发布。现在我们可以知道,NotifyCenterpublishEvent方法是由 ServiceInfoHolder调用的,并基于新的ServiceInfo的内容构建一个InstancesChangeEvent。也就是所我们在订阅成功后会立即触发一个实例变更事件。

    public ServiceInfo processServiceInfo(ServiceInfo serviceInfo) {
        // 1. 获取服务标识
        String serviceKey = serviceInfo.getKey();
        if (serviceKey == null) {
            return null;
        }
        // 2. 获取旧的服务信息
        ServiceInfo oldService = serviceInfoMap.get(serviceInfo.getKey());
        if (isEmptyOrErrorPush(serviceInfo)) {
            //empty or error push, just ignore
            return oldService;
        }
        // 3. 缓存新的服务信息
        serviceInfoMap.put(serviceInfo.getKey(), serviceInfo);
        // 4. 判断服务信息是否发生变更
        boolean changed = isChangedServiceInfo(oldService, serviceInfo);
        if (StringUtils.isBlank(serviceInfo.getJsonFromServer())) {
            serviceInfo.setJsonFromServer(JacksonUtils.toJson(serviceInfo));
        }
        MetricsMonitor.getServiceInfoMapSizeMonitor().set(serviceInfoMap.size());
        if (changed) {
            NAMING_LOGGER.info("current ips:({}) service: {} -> {}", serviceInfo.ipCount(), serviceInfo.getKey(),
                    JacksonUtils.toJson(serviceInfo.getHosts()));
            // 5. 当服务信息发生变更时,发布一个实例变化事件
            NotifyCenter.publishEvent(new InstancesChangeEvent(notifierEventScope, serviceInfo.getName(), serviceInfo.getGroupName(),
                    serviceInfo.getClusters(), serviceInfo.getHosts()));
            DiskCache.write(serviceInfo, cacheDir);
        }
        return serviceInfo;
    }

3. 处理服务端消息推送

以上是在首次订阅过程中进行的事件发布,那么其他情况下服务实例变更事件又是如何被发布的呢。比如我们手动让一个实例下线。

publishEvent方法开始查看栈帧调用,我们发现了 gRPC 的onNext方法。在 gRPC 的流式 RPC 中,客户端在接收到服务端发送的流式数据时,可以通过onNext()方法来处理每一条接收到的数据。onNext()方法会接收一个消息对象,这个对象就是服务端发送给客户端的一条数据。当客户端接收到消息时,会自动调用onNext()方法来进行处理。

这就是说客户端向服务端发送订阅请求后,会使用流式 RPC 来处理服务端的消息推送。从 Nacos 定义的 proto 文件来看,采用的应该是 Bidirectional Streaming RPC(双向流式 RPC)。

service BiRequestStream {
  // Sends a biStreamRequest
  rpc requestBiStream (stream Payload) returns (stream Payload) {
  }
}

GrpcClient中。我们找到onNext()方法的定义。Payload是服务端返回的原始信息,先将其转为Request,然后执行handServerRequest()方法。

handleServerRequest()方法会遍历所有的ServerRequestHandler依次处理服务请求。

    protected Response handleServerRequest(final Request request) {
        // ...
        for (ServerRequestHandler serverRequestHandler : serverRequestHandlers) {
            try {
                Response response = serverRequestHandler.requestReply(request);
                // ...
            } catch (Exception e) {
                 // ...
            }
        }
        return null;
    }

我们要关注的就是这个NamingPushRequestHandler。进入其requestReply()方法,该方法会先判断这是不是一个NotifySubscriberRequest请求,如果是的话就从请求中得到服务信息,并调用serviceInfoHolderprocessServiceInfo方法,从而实现事件的发布。

    @Override
    public Response requestReply(Request request) {
        if (request instanceof NotifySubscriberRequest) {
            NotifySubscriberRequest notifyRequest = (NotifySubscriberRequest) request;
            serviceInfoHolder.processServiceInfo(notifyRequest.getServiceInfo());
            return new NotifySubscriberResponse();
        }
        return null;
    }

那么GrpcClientNamingPushRequestHandler有是什么时候创建的呢。我们知道NamingClientProxyDelegate实际上是使用了NamingGrpcClientProxy,这也是一个代理类。我们看一下它的构造方法。

    public NamingGrpcClientProxy(String namespaceId, SecurityProxy securityProxy, ServerListFactory serverListFactory,
            NacosClientProperties properties, ServiceInfoHolder serviceInfoHolder) throws NacosException {
        // ...
        this.rpcClient = RpcClientFactory.createClient(uuid, ConnectionType.GRPC, labels, RpcClientTlsConfig.properties(properties.asProperties()));
        // ...
        start(serverListFactory, serviceInfoHolder);
    }

其在构造方法中创建了一个RpcClient,并且需要传入一个ServiceInfoHolder用于start()方法,这个serviceInfoHolder正是在NacosNamingServic中创建的。
start()方法中,会为rpcClient添加一个NamingPushRequestHandler,serviceInfoHolder作为构造参数被传入。

    private void start(ServerListFactory serverListFactory, ServiceInfoHolder serviceInfoHolder) throws NacosException {
        rpcClient.serverListFactory(serverListFactory);
        rpcClient.registerConnectionListener(redoService);
        rpcClient.registerServerRequestHandler(new NamingPushRequestHandler(serviceInfoHolder));
        rpcClient.start();
        NotifyCenter.registerSubscriber(this);
    }

4. 事件作用范围

NotifyCenter中,是一种事件类型对应一个发布者,所有的事件都会经过NotifyCenter来发布。但如果我有两个订阅者订阅都关注了InstancesChangeEvent事件(比如创建两个不同的NacosNamingService),如何确定InstancesChangeEvent事件面向那个订阅者呢。实际上,Nacos Client 使用了 Event Scope,即事件作用范围来标识事件所属的订阅者。
DefaultPublisherreceiveEvent可以看出,通知订阅者前要先进行事件范围的匹配,只有匹配成功了才会继续执行。

  for (Subscriber subscriber : subscribers) {
          if (!subscriber.scopeMatches(event)) {
              continue;
          }
      // ...
  }

下面是InstancesChangeNotifierscopeMatches()方法,可以看出订阅者和事件都有一个事件作用范围。

   @Override
    public boolean scopeMatches(InstancesChangeEvent event) {
        return this.eventScope.equals(event.scope());
    }

前面提到了,NacosNamingServiceinit方法生成一个随机的唯一标识,这就是该客户端的事件范围。首先notifierEventScope会用于构建该客户端的InstancesChangeNotifier订阅者,客户端的ServiceInfoHolder也会使用该标识构建。当客户端收到服务端发送的消息时,使用ServiceInfoHolder处理服务信息,创建的事件都会带上notifierEventScope。这样该客户端的订阅者和其关注的事件就能匹配成功了。

    this.notifierEventScope = UUID.randomUUID().toString();
    this.changeNotifier = new InstancesChangeNotifier(this.notifierEventScope);
    this.serviceInfoHolder = new ServiceInfoHolder(namespace, this.notifierEventScope, nacosClientProperties);
    NotifyCenter.publishEvent(new InstancesChangeEvent(notifierEventScope, serviceInfo.getName(), 
                               serviceInfo.getGroupName(), serviceInfo.getClusters(), serviceInfo.getHosts()));
posted @   DaleLee  阅读(918)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示