正文
Nacos作为服务注册中心,整体的一个工作示意图如下所示。
作为一个客户端如果想要知道关心的服务的实例信息,首先可以去订阅 这个服务,只要订阅了这个服务,后续这个服务如果发生了实例信息变更,服务端就会推送 变更消息到客户端,同时客户端也会针对某个服务创建定时拉取的任务,也就是周期性的将服务的实例信息拉取下来并完成缓存。
本文将结合源码,对上述流程进行学习。
往期文章如下。
Nacos2注册中心服务端启动流程
Nacos2服务注册流程
Nacos2心跳与健康检查
一. 客户端订阅服务
在Nacos 中,查询某一个服务的实例时就会一并进行服务订阅,这段逻辑在nacos-client 包的NacosNamingService 中,具体是selectInstances() 方法,该方法有很多重载方法,我们需要关注的是下面这一段代码。
java
@Override
public List<Instance> selectInstances(String serviceName, String groupName, List<String> clusters, boolean healthy,
boolean subscribe) throws NacosException {
// ServiceInfo就代表我们要查询的服务的实例的信息
ServiceInfo serviceInfo;
String clusterString = StringUtils.join(clusters, ",");
if (subscribe) {
// 优先从缓存中获取ServiceInfo
serviceInfo = serviceInfoHolder.getServiceInfo(serviceName, groupName, clusterString);
if (null == serviceInfo) {
// 缓存中没有对应服务的ServiceInfo则向服务端发起订阅请求
// 在向服务端发起订阅请求的同时服务端也会返回实例信息回来
serviceInfo = clientProxy.subscribe(serviceName, groupName, clusterString);
}
} else {
// 仅从服务端获取对应实例信息而不订阅
// 通常这个逻辑是不用关注的
serviceInfo = clientProxy.queryInstancesOfService(serviceName, groupName, clusterString, 0, false);
}
// 最终拿到ServiceInfo就将里面的Instance实例全部返回
return selectInstances(serviceInfo, healthy);
}
上述方法需要重点关注下面这些点。
- ServiceInfo 代表客户端要查询的服务的信息 。服务下的所有实例的信息Instance 就包含在ServiceInfo里面;
- ServiceInfo 会优先从缓存中获取 。这里的缓存就是ServiceInfoHolder对象,缓存信息表示如下:
java
// 键是groupName + @@ + serviceName
// 值就是对应的服务实例信息对象
ConcurrentMap<String, ServiceInfo> serviceInfoMap;
- 如果缓存中没有对应 ServiceInfo 则向服务端发起订阅。
向服务端发起订阅的逻辑在NamingClientProxyDelegate 的subscribe() 方法中,如下所示。
java
@Override
public ServiceInfo subscribe(String serviceName, String groupName, String clusters) throws NacosException {
NAMING_LOGGER.info("[SUBSCRIBE-SERVICE] service:{}, group:{}, clusters:{} ", serviceName, groupName, clusters);
String serviceNameWithGroup = NamingUtils.getGroupedName(serviceName, groupName);
String serviceKey = ServiceInfo.getKey(serviceNameWithGroup, clusters);
// 这里是定时拉取服务的逻辑
// 后面会单独分析这一块儿
serviceInfoUpdateService.scheduleUpdateIfAbsent(serviceName, groupName, clusters);
// 定时拉取服务后可能缓存中就有对应的ServiceInfo了
// 所以这里需要再从缓存中获取一次
ServiceInfo result = serviceInfoHolder.getServiceInfoMap().get(serviceKey);
if (null == result || !isSubscribed(serviceName, groupName, clusters)) {
// 通过grpc客户端向服务端发送SubscribeServiceRequest进行订阅创建
// 创建订阅的同时也会得到服务实例信息
result = grpcClientProxy.subscribe(serviceName, groupName, clusters);
}
// 更新缓存
serviceInfoHolder.processServiceInfo(result);
return result;
}
到这里其实客户端的订阅服务流程就结束了,其中有一个遗留的点就是在发起订阅前,有一步是创建拉取服务的定时任务,这块儿逻辑单独放到第三节进行分析,除此之外的逻辑,这里进行一个小结。
- 在 Nacos 客户端查询服务实例信息会优先从本地缓存中获取 。本地缓存叫做ServiceInfoHolder ,以键值对的形式缓存了服务实例信息ServiceInfo ,其中键是服务所属组拼接上服务名,值就是ServiceInfo;
- 缓存中如果获取不到服务实例信息则通过 grpc 客户端向服务端发送 SubscribeServiceRequest 来创建服务订阅。
二. 服务端处理服务订阅
客户端发送的服务订阅请求叫做SubscribeServiceRequest ,相应的服务端的处理者肯定就是SubscribeServiceRequestHandler 方法,其handle() 方法如下所示。
java
@Override
@Secured(action = ActionTypes.READ, parser = NamingResourceParser.class)
public SubscribeServiceResponse handle(SubscribeServiceRequest request, RequestMeta meta) throws NacosException {
// 进行要订阅的服务的信息的获取与封装
String namespaceId = request.getNamespace();
String serviceName = request.getServiceName();
String groupName = request.getGroupName();
String app = request.getHeader("app", "unknown");
String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);
Service service = Service.newService(namespaceId, groupName, serviceName, true);
Subscriber subscriber = new Subscriber(meta.getClientIp(), meta.getClientVersion(), app,
meta.getClientIp(), namespaceId, groupedServiceName, 0, request.getClusters());
ServiceInfo serviceInfo = ServiceUtil.selectInstancesWithHealthyProtection(serviceStorage.getData(service),
metadataManager.getServiceMetadata(service).orElse(null), subscriber);
if (request.isSubscribe()) {
// 在这里会发布一个ClientSubscribeServiceEvent事件
clientOperationService.subscribeService(service, subscriber, meta.getConnectionId());
} else {
clientOperationService.unsubscribeService(service, subscriber, meta.getConnectionId());
}
return new SubscribeServiceResponse(ResponseCode.SUCCESS.getCode(), "success", serviceInfo);
}
上述方法中,首先会从请求中获取出订阅的服务的相关信息,然后就会发布一个对于该服务的订阅事件ClientSubscribeServiceEvent ,同时ClientServiceIndexesManager会监听该事件,对应的监听逻辑如下所示。
java
private void handleClientOperation(ClientOperationEvent event) {
Service service = event.getService();
String clientId = event.getClientId();
if (event instanceof ClientOperationEvent.ClientRegisterServiceEvent) {
addPublisherIndexes(service, clientId);
} else if (event instanceof ClientOperationEvent.ClientDeregisterServiceEvent) {
removePublisherIndexes(service, clientId);
} else if (event instanceof ClientOperationEvent.ClientSubscribeServiceEvent) {
// 在这里处理订阅事件
addSubscriberIndexes(service, clientId);
} else if (event instanceof ClientOperationEvent.ClientUnsubscribeServiceEvent) {
removeSubscriberIndexes(service, clientId);
}
}
继续跟进处理订阅事件的逻辑。
java
private void addSubscriberIndexes(Service service, String clientId) {
// 先建立起客户端与订阅的服务的映射关系
subscriberIndexes.computeIfAbsent(service, (key) -> new ConcurrentHashSet<>());
if (subscriberIndexes.get(service).add(clientId)) {
// 发布一个ServiceSubscribedEvent事件
NotifyCenter.publishEvent(new ServiceEvent.ServiceSubscribedEvent(service, clientId));
}
}
ClientServiceIndexesManager 在处理订阅事件时,首先会建立起客户端与订阅的服务的映射关系,然后再发布一个ServiceSubscribedEvent 事件,该事件的监听者是NamingSubscriberServiceV2Impl,对应处理逻辑如下。
java
@Override
public void onEvent(Event event) {
if (!upgradeJudgement.isUseGrpcFeatures()) {
return;
}
if (event instanceof ServiceEvent.ServiceChangedEvent) {
// 例如服务注册或实例健康探测失败时会触发ServiceChangedEvent事件
// 此时会将对应服务的实例信息推送给所有的订阅者
ServiceEvent.ServiceChangedEvent serviceChangedEvent = (ServiceEvent.ServiceChangedEvent) event;
Service service = serviceChangedEvent.getService();
delayTaskEngine.addTask(service, new PushDelayTask(service, PushConfig.getInstance().getPushTaskDelay()));
} else if (event instanceof ServiceEvent.ServiceSubscribedEvent) {
// 在某个客户端订阅服务时会触发ServiceSubscribedEvent事件
// 此时会将服务的实例信息推送给这个客户端
ServiceEvent.ServiceSubscribedEvent subscribedEvent = (ServiceEvent.ServiceSubscribedEvent) event;
Service service = subscribedEvent.getService();
delayTaskEngine.addTask(service, new PushDelayTask(service, PushConfig.getInstance().getPushTaskDelay(),
subscribedEvent.getClientId()));
}
}
在上述逻辑中,主要会监听两类事件,分别是ServiceChangedEvent 和ServiceSubscribedEvent,前者表明某个服务的实例信息发生了变更,比如服务有新的实例进行注册,或者服务有实例健康探测不过,此时会将服务的最新实例信息推送给这个服务的所有订阅者,后者则表明某个客户端订阅了某个服务,此时会将这个服务的实例信息推送给这个客户端。
这里注意到推送的逻辑使用了PushDelayTaskExecuteEngine任务引擎,这里解释一下其工作机制。
首先PushDelayTaskExecuteEngine 在其构造函数中会调用到其父类NacosDelayTaskExecuteEngine的构造函数,如下所示。
java
public PushDelayTaskExecuteEngine(ClientManager clientManager, ClientServiceIndexesManager indexesManager,
ServiceStorage serviceStorage, NamingMetadataManager metadataManager,
PushExecutor pushExecutor, SwitchDomain switchDomain) {
super(PushDelayTaskExecuteEngine.class.getSimpleName(), Loggers.PUSH);
this.clientManager = clientManager;
this.indexesManager = indexesManager;
this.serviceStorage = serviceStorage;
this.metadataManager = metadataManager;
this.pushExecutor = pushExecutor;
this.switchDomain = switchDomain;
// 如果没有为任务配置专门的执行器
// 则任务的执行由该默认执行器来完成
setDefaultTaskProcessor(new PushDelayTaskProcessor(this));
}
public NacosDelayTaskExecuteEngine(String name, Logger logger) {
this(name, 32, logger, 100L);
}
public NacosDelayTaskExecuteEngine(String name, int initCapacity, Logger logger, long processInterval) {
super(logger);
tasks = new ConcurrentHashMap<>(initCapacity);
processingExecutor = ExecutorFactory.newSingleScheduledExecutorService(new NameThreadFactory(name));
// 在这里会启动一个定时任务来周期性的执行ProcessRunnable
processingExecutor
.scheduleWithFixedDelay(new ProcessRunnable(), processInterval, processInterval, TimeUnit.MILLISECONDS);
}
也就是在PushDelayTaskExecuteEngine 引擎创建出来的时候,就会启动一个定时任务来周期性的执行ProcessRunnable ,可以理解为ProcessRunnable 的run() 方法会被周期性的调用,那么跟进一下run() 方法,如下所示。
java
@Override
public void run() {
try {
processTasks();
} catch (Throwable e) {
getEngineLog().error(e.toString(), e);
}
}
protected void processTasks() {
// 从tasks中拿到所有要执行的任务的键
// tasks是存放任务的Map
Collection<Object> keys = getAllTaskKeys();
for (Object taskKey : keys) {
// 从tasks中将每一个要执行的任务获取出来
// 同时需要将任务从tasks中移除
// 也就是一个任务只执行一次
AbstractDelayTask task = removeTask(taskKey);
if (null == task) {
continue;
}
// 获取执行器
// 通常获取出来是PushDelayTaskProcessor
NacosTaskProcessor processor = getProcessor(taskKey);
if (null == processor) {
getEngineLog().error("processor not found for task, so discarded. " + task);
continue;
}
try {
// 执行任务
if (!processor.process(task)) {
retryFailedTask(taskKey, task);
}
} catch (Throwable e) {
getEngineLog().error("Nacos task execute error ", e);
retryFailedTask(taskKey, task);
}
}
}
上述逻辑其实就是会周期性的去消费tasks 中的任务来执行,并且一个任务仅执行一次。那么现在问题就是tasks 中的任务从哪里来的呢,回到上面的NamingSubscriberServiceV2Impl 的onEvent() 方法。
java
@Override
public void onEvent(Event event) {
if (!upgradeJudgement.isUseGrpcFeatures()) {
return;
}
if (event instanceof ServiceEvent.ServiceChangedEvent) {
ServiceEvent.ServiceChangedEvent serviceChangedEvent = (ServiceEvent.ServiceChangedEvent) event;
Service service = serviceChangedEvent.getService();
// 这里会往tasks中添加PushDelayTask
delayTaskEngine.addTask(service, new PushDelayTask(service, PushConfig.getInstance().getPushTaskDelay()));
} else if (event instanceof ServiceEvent.ServiceSubscribedEvent) {
ServiceEvent.ServiceSubscribedEvent subscribedEvent = (ServiceEvent.ServiceSubscribedEvent) event;
Service service = subscribedEvent.getService();
// 这里会往tasks中添加PushDelayTask
delayTaskEngine.addTask(service, new PushDelayTask(service, PushConfig.getInstance().getPushTaskDelay(),
subscribedEvent.getClientId()));
}
}
也就是监听到ServiceChangedEvent 或ServiceSubscribedEvent 时,就会添加推送任务PushDelayTask 到tasks 中,同时又有一个线程在周期性的消费tasks 中的任务来执行,这就是PushDelayTaskExecuteEngine 任务引擎的一个工作机制,这么设计的好处就是逻辑之间高度解耦,坏处就是看代码费眼睛。
最后分析一下每一个PushDelayTask 是怎么被PushDelayTaskProcessor 执行的,跟进其process() 方法,如下所示。
java
@Override
public boolean process(NacosTask task) {
PushDelayTask pushDelayTask = (PushDelayTask) task;
Service service = pushDelayTask.getService();
// 将pushDelayTask封装成PushExecuteTask
// 然后调用异步线程来执行PushExecuteTask
// 最终调用到PushExecuteTask的run()方法
NamingExecuteTaskDispatcher.getInstance()
.dispatchAndExecuteTask(service, new PushExecuteTask(service, executeEngine, pushDelayTask));
return true;
}
// PushExecuteTask的run()方法
@Override
public void run() {
try {
PushDataWrapper wrapper = generatePushData();
for (String each : getTargetClientIds()) {
Client client = delayTaskEngine.getClientManager().getClient(each);
if (null == client) {
continue;
}
Subscriber subscriber = delayTaskEngine.getClientManager().getClient(each).getSubscriber(service);
// 这里最终会调用到PushExecutorRpcImpl的doPushWithCallback()方法
delayTaskEngine.getPushExecutor().doPushWithCallback(each, subscriber, wrapper,
new NamingPushCallback(each, subscriber, wrapper.getOriginalData(), delayTask.isPushToAll()));
}
} catch (Exception e) {
Loggers.PUSH.error("Push task for service" + service.getGroupedServiceName() + " execute failed ", e);
delayTaskEngine.addTask(service, new PushDelayTask(service, 1000L));
}
}
// PushExecutorRpcImpl的doPushWithCallback()方法
@Override
public void doPushWithCallback(String clientId, Subscriber subscriber, PushDataWrapper data, PushCallBack callBack) {
// 最终向客户端发送NotifySubscriberRequest来推送服务实例信息
pushService.pushWithCallback(clientId, NotifySubscriberRequest.buildNotifySubscriberRequest(getServiceInfo(data, subscriber)),
callBack, GlobalExecutor.getCallbackExecutor());
}
上述方法最终就是构建出NotifySubscriberRequest 来将服务实例信息发送给客户端,而客户端处理NotifySubscriberRequest 的是NamingPushRequestHandler,对应处理逻辑如下所示。
java
@Override
public Response requestReply(Request request) {
if (request instanceof NotifySubscriberRequest) {
NotifySubscriberRequest notifyResponse = (NotifySubscriberRequest) request;
// 服务实例信息缓存到ServiceInfoHolder中
serviceInfoHolder.processServiceInfo(notifyResponse.getServiceInfo());
return new NotifySubscriberResponse();
}
return null;
}
客户端收到服务实例推送消息后,会将服务实例最新信息更新到ServiceInfoHolder缓存中。
至此服务端处理服务订阅的逻辑分析完毕,流程稍微有点长,这里进行一个小结。
- 服务端收到 SubscribeServiceRequest 请求后会发布 ClientSubscribeServiceEvent 事件;
- ClientServiceIndexesManager 监听 ClientSubscribeServiceEvent 事件并处理 。这里会做两件事情,首先建立起客户端与订阅的服务的映射关系,然后发布ServiceSubscribedEvent事件;
- NamingSubscriberServiceV2Impl 监听 ServiceSubscribedEvent 事件并处理 。这里会将服务的实例信息推送给客户端,做法是创建推送任务PushDelayTask 并提交给PushDelayTaskExecuteEngine任务引擎;
- PushDelayTaskExecuteEngine 任务引擎最终会创建 NotifySubscriberRequest 将服务实例信息发送给客户端;
- 客户端的 NamingPushRequestHandler 会将最新服务实例信息更新到 ServiceInfoHolder 缓存。
补充一点,就是NamingSubscriberServiceV2Impl 同时也会监听ServiceChangedEvent事件,该事件表示对应服务的实例信息发生了变更,此时就需要将变更的消息推送给服务的所有订阅者。
三. 客户端定时拉取服务
回到第一节,客户端向服务端发起订阅的逻辑在NamingClientProxyDelegate 的subscribe() 方法中,再看下这部分代码。
java
@Override
public ServiceInfo subscribe(String serviceName, String groupName, String clusters) throws NacosException {
NAMING_LOGGER.info("[SUBSCRIBE-SERVICE] service:{}, group:{}, clusters:{} ", serviceName, groupName, clusters);
String serviceNameWithGroup = NamingUtils.getGroupedName(serviceName, groupName);
String serviceKey = ServiceInfo.getKey(serviceNameWithGroup, clusters);
// 在这里创建拉取服务的定时任务
serviceInfoUpdateService.scheduleUpdateIfAbsent(serviceName, groupName, clusters);
ServiceInfo result = serviceInfoHolder.getServiceInfoMap().get(serviceKey);
if (null == result || !isSubscribed(serviceName, groupName, clusters)) {
result = grpcClientProxy.subscribe(serviceName, groupName, clusters);
}
serviceInfoHolder.processServiceInfo(result);
return result;
}
在客户端向服务端订阅服务前,客户端还会针对该服务创建一个定时拉取的任务,继续跟进一下ServiceInfoUpdateService 的scheduleUpdateIfAbsent() 方法,如下所示。
java
public void scheduleUpdateIfAbsent(String serviceName, String groupName, String clusters) {
String serviceKey = ServiceInfo.getKey(NamingUtils.getGroupedName(serviceName, groupName), clusters);
if (futureMap.get(serviceKey) != null) {
return;
}
synchronized (futureMap) {
if (futureMap.get(serviceKey) != null) {
return;
}
// 这里的定时任务就是UpdateTask
ScheduledFuture<?> future = addTask(new UpdateTask(serviceName, groupName, clusters));
futureMap.put(serviceKey, future);
}
}
上述方法中创建出来的定时任务就是UpdateTask ,因此定时拉取服务实例信息的逻辑就在UpdateTask 的run() 方法中。
java
@Override
public void run() {
long delayTime = DEFAULT_DELAY;
try {
if (!changeNotifier.isSubscribed(groupName, serviceName, clusters) && !futureMap.containsKey(
serviceKey)) {
NAMING_LOGGER.info("update task is stopped, service:{}, clusters:{}", groupedServiceName, clusters);
isCancel = true;
return;
}
// 先从缓存中获取服务实例信息
ServiceInfo serviceObj = serviceInfoHolder.getServiceInfoMap().get(serviceKey);
if (serviceObj == null) {
// 获取不到则向服务端查询
serviceObj = namingClientProxy.queryInstancesOfService(serviceName, groupName, clusters, 0, false);
// 放到缓存中
serviceInfoHolder.processServiceInfo(serviceObj);
lastRefTime = serviceObj.getLastRefTime();
return;
}
// lastRefTime默认是Long的最大值
if (serviceObj.getLastRefTime() <= lastRefTime) {
// 因此这里的逻辑一般都会执行到
// 所以缓存中就算有服务实例信息还是要向服务端查询一次
serviceObj = namingClientProxy.queryInstancesOfService(serviceName, groupName, clusters, 0, false);
serviceInfoHolder.processServiceInfo(serviceObj);
}
lastRefTime = serviceObj.getLastRefTime();
if (CollectionUtils.isEmpty(serviceObj.getHosts())) {
incFailCount();
return;
}
// delayTime = 1000 * 6
delayTime = serviceObj.getCacheMillis() * DEFAULT_UPDATE_CACHE_TIME_MULTIPLE;
// 重置failCount次数为0
resetFailCount();
} catch (Throwable e) {
// 定时拉取服务实例过程中如果发生异常
// 则failCount加1
// 这里failCount最多只能增加到6
incFailCount();
NAMING_LOGGER.warn("[NA] failed to update serviceName: {}", groupedServiceName, e);
} finally {
if (!isCancel) {
// 定时任务的执行周期一开始是6秒
// 然后依次按照2的failCount幂次方递增
// 最多增加到60秒
// 这里是一个衰减重试的过程
executor.schedule(this, Math.min(delayTime << failCount, DEFAULT_DELAY * 60),
TimeUnit.MILLISECONDS);
}
}
}
上述方法就是客户端定时拉取服务实例信息的整个逻辑,这里小结如下。
- 定时任务触发后就会向服务端拉取服务实例信息并更新缓存;
- 如果定时任务执行成功则 failCount 重置为 0;
- 如果定时任务执行失败则 failCount 加 1;
- 定时任务的执行周期是一个衰减重试的过程 。一开始周期为6s ,如果定时任务触发时发生异常,则周期依次按照2 的failCount 幂次方递增,但最多增加到60s。
总结
详细总结就是第一节 ,第二节 和第三节的小结汇总,这里不再重复给出。
总结不易,如果本文对你有帮助,烦请点赞,收藏加关注,谢谢帅气漂亮的你。