3. 客户端查询服务流程分析

客户端服务调用入口

我们在使用 Feign 调用服务时,需要在 Nacos 寻找上游服务的服务信息,此时会通过调用 NacosNamingService 的 selectInstances 方法调用 Nacos 获取服务实例列表。
image

NacosNamingService 的 selectInstances 方法代码如下:

// NacosNamingService
@Override
public List<Instance> selectInstances(String serviceName, String groupName, List<String> clusters, boolean healthy,
		boolean subscribe) throws NacosException {
	
	ServiceInfo serviceInfo;
	String clusterString = StringUtils.join(clusters, ",");
	// 判断是否需要 订阅,默认为 true
	if (subscribe) {
		// 查询 Nacos 本地缓存数据
		serviceInfo = serviceInfoHolder.getServiceInfo(serviceName, groupName, clusterString);
		// 如果本地缓存数据为空,则通过 client 对象请求服务端获取数据,这里是调用的订阅方法
		if (null == serviceInfo) {
			// clientProxy 为 NamingClientProxyDelegate 类。
			serviceInfo = clientProxy.subscribe(serviceName, groupName, clusterString);
		}
	} else {
		serviceInfo = clientProxy.queryInstancesOfService(serviceName, groupName, clusterString, 0, false);
	}
	// 返回数据
	return selectInstances(serviceInfo, healthy);
}
  • 首先判断是否需要订阅上游服务信息
    • 如果需要,则先查看本地是否有服务信息,
    • 如果本地缓存没有,则从 nacos 获取服务信息。

NacosNamingService 的 selectInstance 时序图如下:
image

查看本地是否有服务信息

// ServiceInfoHolder
private final ConcurrentMap<String, ServiceInfo> serviceInfoMap;

public ServiceInfo getServiceInfo(final String serviceName, final String groupName, final String clusters) {
    NAMING_LOGGER.debug("failover-mode: {}", failoverReactor.isFailoverSwitch());
    // 获取 key
    String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);
    String key = ServiceInfo.getKey(groupedServiceName, clusters);
    if (failoverReactor.isFailoverSwitch()) {
        return failoverReactor.getService(key);
    }
    // 通过 key 从本地缓存中获取数据
    return serviceInfoMap.get(key);
}
  • serviceInfoMap Value 对应的是 ServiceInfo 对象,在这个对象中会有一个 List<Instance> hosts 属性来存放实例信息

ServiceInfoHolder 的 getServiceInfo 时序图如下:
image

如果本地缓存没有,则从 nacos 获取服务信息

// NamingClientProxyDelegate
@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)) {
        // 如果还是为空,则会使用 grpc 来请求服务端
        result = grpcClientProxy.subscribe(serviceName, groupName, clusters);
    }
    // 更新本地缓存
    serviceInfoHolder.processServiceInfo(result);
    return result;
}
  • 首先获取带有 group 名称的 serviceName
  • 根据 serviceName 和 clusters 名称生成一个 serviceKey
  • 开启服务实例查询的定时任务,定时任务会开启线程,向 nacos 发送查询实例请求,并将结果存入 serviceInfoHolder 的 serviceInfoMap 中。
  • 开启任务后,在查询一次 serviceInfoMap 的缓存数据。如果还是没查询到 Service 数据,则使用当前线程向 Nacos 服务器发送订阅服务请求。

订阅服务实例信息时序图如下:
image

实例查询定时任务详情

serviceInfoUpdateService.scheduleUpdateIfAbsent(serviceName, groupName, clusters); 方法会进入 ServiceInfoUpdateService 类的 scheduleUpdateIfAbsent 方法

public void scheduleUpdateIfAbsent(String serviceName, String groupName, String clusters) {
	if (!asyncQuerySubscribeService) {
		return;
	}
	// 生成一个 serverKey
	String serviceKey = ServiceInfo.getKey(NamingUtils.getGroupedName(serviceName, groupName), clusters);
	// 判断当前 serviceKey 是否有开启定时任务,如果有就不开启了,futureMap用来存储已开启任务的 serviceKey
	if (futureMap.get(serviceKey) != null) {
		return;
	}
	// 对 futureMap 加锁,避免并发修改futureMap 的值。
	synchronized (futureMap) {
		// 加锁成功后,再次检查是否存在 servicekey,双重检测
		if (futureMap.get(serviceKey) != null) {
			return;
		}
		// 向线程池提交 UpdateTask 任务
		ScheduledFuture<?> future = addTask(new UpdateTask(serviceName, groupName, clusters));
		futureMap.put(serviceKey, future);
	}
}
private final ScheduledExecutorService executor;

private synchronized ScheduledFuture<?> addTask(UpdateTask task) {
	return executor.schedule(task, DEFAULT_DELAY, TimeUnit.MILLISECONDS);
}

UpdateTask 的 run 方法

// UpdateTask
public UpdateTask(String serviceName, String groupName, String clusters) {
	this.serviceName = serviceName;
	this.groupName = groupName;
	this.clusters = clusters;
	this.groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);
	this.serviceKey = ServiceInfo.getKey(groupedServiceName, clusters);
}

@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) {
			// 为空就会通过 gRPC 去查询服务端的数据
			serviceObj = namingClientProxy.queryInstancesOfService(serviceName, groupName, clusters, 0, false);
			// 更新本地缓存
			serviceInfoHolder.processServiceInfo(serviceObj);
			// 更新获取时间
			lastRefTime = serviceObj.getLastRefTime();
			return;
		}
		// 如果本地缓存不为空,会判断该本地缓存最后一次刷新的时间,是否小于等于最后一次数据刷新时间
		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;
		}
		// 计算下一次定时任务执行的时间,这里的结果是 6s
		delayTime = serviceObj.getCacheMillis() * DEFAULT_UPDATE_CACHE_TIME_MULTIPLE;
		// 当请求成功之后,会重置错误次数为 0
		resetFailCount();
	} catch (NacosException e) {
		handleNacosException(e);
	} catch (Throwable e) {
		handleUnknownException(e); // 处理未知错误
	} finally {
		if (!isCancel) {
			// 这里就根据请求失败的次数,来动态调整下一次执行定时任务的时间
			executor.schedule(this, Math.min(delayTime << failCount, DEFAULT_DELAY * 60),
					TimeUnit.MILLISECONDS);
		}
	}
}

private void handleUnknownException(Throwable throwable) {  
	// 出现错误后,记录错误的次数,如果次数超过6次,则退出任务,不再执行。
    incFailCount();  
    NAMING_LOGGER.warn("[NA] failed to update serviceName: {}", groupedServiceName, throwable);  
}  
  
private void incFailCount() {  
    int limit = 6;  
    if (failCount == limit) {  
        return;  
    }  
    failCount++;  
}

UpdateTask 的 run 时序图:
image

发送订阅服务请求

// NamingGrpcClientProxy
@Override
public ServiceInfo subscribe(String serviceName, String groupName, String clusters) throws NacosException {
    if (NAMING_LOGGER.isDebugEnabled()) {
        NAMING_LOGGER.debug("[GRPC-SUBSCRIBE] service:{}, group:{}, cluster:{} ", serviceName, groupName, clusters);
    }
    // 在重试服务中缓存订阅服务请求,如果订阅失败,则使用重试服务发起重试请求。
    redoService.cacheSubscriberForRedo(serviceName, groupName, clusters);
    // 执行订阅
    return doSubscribe(serviceName, groupName, clusters);
}


public ServiceInfo doSubscribe(String serviceName, String groupName, String clusters) throws NacosException {
    // 订阅类型请求
    SubscribeServiceRequest request = new SubscribeServiceRequest(namespaceId, groupName, serviceName, clusters,
            true);
	// 发送订阅请求
    SubscribeServiceResponse response = requestToServer(request, SubscribeServiceResponse.class);
    redoService.subscriberRegistered(serviceName, groupName, clusters);
    return response.getServiceInfo();
}

  • subscribe 方法最重要的步骤是创建一个订阅类型请求,并发送到 Nacos 服务端
  • SubscribeServiceRequest 的请求参数如下所示:
    image
posted @ 2024-11-13 14:08  Jacob-Chen  阅读(72)  评论(0)    收藏  举报