Nacos Client 源码分析(二)服务订阅与推送消息处理
本文使用的 Nacos 版本为 2.2.2
1. 概述
在上一篇文章《Nacos Client 源码分析(一)事件的发布与订阅》分析了 Nacos Client 的发布订阅机制,但我们现在还不清楚NotifyCenter的publishEvent方法是怎么被调用的以及客户端向服务端订阅服务的具体流程。下面我们对继续分析 Nacos 的源码。
2. 服务订阅
还是从NacosNamingService 的init方法开始分析,注意到以下几句代码。notifierEventScope标识一个事件的作用范围,也可以理解为事件是面向哪一个客户端,后面会用到。ServiceInfoHolder保存了客户端请求到的服务信息。NamingClientProxyDelegate是命名服务客户端代理的委托,其内部实际使用的是NamingHttpClientProxy和NamingGrpcClientProxy。
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方法的如下,如果服务信息变更会触发事件发布。现在我们可以知道,NotifyCenter的publishEvent方法是由 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请求,如果是的话就从请求中得到服务信息,并调用serviceInfoHolder的processServiceInfo方法,从而实现事件的发布。
@Override
public Response requestReply(Request request) {
if (request instanceof NotifySubscriberRequest) {
NotifySubscriberRequest notifyRequest = (NotifySubscriberRequest) request;
serviceInfoHolder.processServiceInfo(notifyRequest.getServiceInfo());
return new NotifySubscriberResponse();
}
return null;
}
那么GrpcClient和NamingPushRequestHandler有是什么时候创建的呢。我们知道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,即事件作用范围来标识事件所属的订阅者。
从DefaultPublisher的receiveEvent可以看出,通知订阅者前要先进行事件范围的匹配,只有匹配成功了才会继续执行。
for (Subscriber subscriber : subscribers) {
if (!subscriber.scopeMatches(event)) {
continue;
}
// ...
}
下面是InstancesChangeNotifier的scopeMatches()方法,可以看出订阅者和事件都有一个事件作用范围。
@Override
public boolean scopeMatches(InstancesChangeEvent event) {
return this.eventScope.equals(event.scope());
}
前面提到了,NacosNamingService的init方法生成一个随机的唯一标识,这就是该客户端的事件范围。首先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()));

浙公网安备 33010602011771号