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()));
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)