3. 客户端查询服务流程分析
客户端服务调用入口
我们在使用 Feign 调用服务时,需要在 Nacos 寻找上游服务的服务信息,此时会通过调用 NacosNamingService 的 selectInstances 方法调用 Nacos 获取服务实例列表。

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 时序图如下:

查看本地是否有服务信息
// 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);
}
serviceInfoMapValue 对应的是ServiceInfo对象,在这个对象中会有一个List<Instance> hosts属性来存放实例信息
ServiceInfoHolder 的 getServiceInfo 时序图如下:

如果本地缓存没有,则从 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 服务器发送订阅服务请求。
订阅服务实例信息时序图如下:

实例查询定时任务详情
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 时序图:

发送订阅服务请求
// 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]()


浙公网安备 33010602011771号