Nacos源码(六):客户端服务发现源码分析
1、客户端服务发现源码入口
在Nacos源码(二):客户端服务注册源码分析中,在nacos-2.2.0源码包中提供的nacos-example的NamingExample示例中,可以发现客户端的服务发现是在NamingService的getAllInstances方法中完成的。
NamingService中的getAllInstances有许多重载的额方法,可以满足各种业务场景下获取实例信息。
最终都会执行 getAllInstances(String serviceName, String groupName, List clusters, boolean subscribe) 方法。
1 /** 2 * 获取服务实例列表 3 */ 4 public List<Instance> getAllInstances(String serviceName, String groupName, List<String> clusters, 5 boolean subscribe) throws NacosException { 6 ServiceInfo serviceInfo; 7 String clusterString = StringUtils.join(clusters, ","); 8 // 是否是订阅模式 9 if (subscribe) { 10 // 先从客户端缓存获取服务信息 11 serviceInfo = serviceInfoHolder.getServiceInfo(serviceName, groupName, clusterString); 12 if (null == serviceInfo || !clientProxy.isSubscribed(serviceName, groupName, clusterString)) { 13 // 若本地缓存不存在服务信息,则进行订阅 14 serviceInfo = clientProxy.subscribe(serviceName, groupName, clusterString); 15 } 16 } else { 17 // 若未订阅服务信息,直接从Nacos服务器进行查询 18 serviceInfo = clientProxy.queryInstancesOfService(serviceName, groupName, clusterString, 0, false); 19 } 20 // 从服务信息中获取实例列表 21 List<Instance> list; 22 if (serviceInfo == null || CollectionUtils.isEmpty(list = serviceInfo.getHosts())) { 23 return new ArrayList<>(); 24 } 25 // 返回实例列表 26 return list; 27 }
2、客户端的服务发现主要步骤
2.1、获取服务信息serviceInfo
若客户端是订阅模式,则直接从客户端本地缓存中获取服务信息;若本地缓存中没有,则为首次调用,先进行订阅,在订阅完成后获取服务信息。
若客户端是非订阅模式,直接请求Nacos服务端,获取服务信息。
2.1.1、订阅模式
消费者订阅Nacos中的服务列表,并基于UDP协议来接收服务变更通知。当Nacos中的服务列表更新时,会发送UDP广播给所有订阅者。这一种是在服务端界面操作或者主动调用服务注册接口或者下线接口等都会主动推送给客户端需要更改的注册列表。
2.1.2、拉取模式
客户端定期主动从Nacos拉取服务列表并缓存起来,当客户端的服务被调用时,优先读取本地缓存中的服务列表。
客户端主动拉取可以通过Http请求,调用 /v1/ns/instance/list 接口完成服务信息的拉取。
NamingHttpClientProxy#queryInstanceOfService 详情如下:
也可以通过 gRPC 请求完成服务信息的拉取。
2.2、从服务信息中获取实例列表
3、本地缓存获取服务信息
从服务信息包装类中获取本地缓存的服务信息,ServiceInfoHolder#getServiceInfo(...)
// 本地缓存服务信息 private final ConcurrentMap<String, ServiceInfo> serviceInfoMap; /** * 从客户端本地缓存中获取服务信息 */ public ServiceInfo getServiceInfo(final String serviceName, final String groupName, final String clusters) { String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName); // 获取key clusters不为空时:key = groupedServiceName@@clusters;为空时,key = groupedServiceName String key = ServiceInfo.getKey(groupedServiceName, clusters); // 故障转移处理 if (failoverReactor.isFailoverSwitch()) { return failoverReactor.getService(key); } // 根据key获取本地缓存的服务信息ServiceInfo return serviceInfoMap.get(key); }
ServiceInfoHolder 维护一个本地缓存 serviceInfoMap,根据指定的key获取缓存中的服务信息返回。
涉及客户端的故障转移处理,在后续进行分析。
4、订阅并返回服务信息
本地缓存中没有服务实例信息,执行订阅,获取返回的服务信息。
1 /** 2 * 执行订阅,并返回实例信息 3 */ 4 @Override 5 public ServiceInfo subscribe(String serviceName, String groupName, String clusters) throws NacosException { 6 // serviceNameWithGroup = groupName@@serviceName 7 String serviceNameWithGroup = NamingUtils.getGroupedName(serviceName, groupName); 8 // serviceKey = serviceNameWithGroup@@clusters || serviceNameWithGroup 9 String serviceKey = ServiceInfo.getKey(serviceNameWithGroup, clusters); 10 // 定时调度 服务信息更新服务任务 UpdateTask 11 serviceInfoUpdateService.scheduleUpdateIfAbsent(serviceName, groupName, clusters); 12 // 获取服务信息 13 ServiceInfo result = serviceInfoHolder.getServiceInfoMap().get(serviceKey); 14 if (null == result || !isSubscribed(serviceName, groupName, clusters)) { 15 // 若服务实例信息为null,基于gRPC协议进行订阅逻辑处理,并获取Nacos服务端返回的服务信息 16 result = grpcClientProxy.subscribe(serviceName, groupName, clusters); 17 } 18 // 客户端本地缓存从Nacos服务端获取的服务实例信息 19 serviceInfoHolder.processServiceInfo(result); 20 return result; 21 }
4.1、服务信息更新任务UpdateTask
若客户端已订阅,执行服务信息更新任务 ServiceInfoUpdateService#UpdateTask,UpdateTask是ServiceInfoUpdateService的内部类,更新服务信息实际上是发起请求获取Nacos服务端的服务信息,再缓存到客户端本地。
若首次执行,则不会执行此定时任务,需要先完成订阅,才能进行本地缓存与服务端的ServiceInfo信息同步。
4.2、订阅
再次从本地缓存中获取服务信息,若还是无法获取到,基于gRPC协议进行订阅逻辑处理,并获取Nacos服务端返回的服务信息,将服务信息缓存到客户端。
NamingGrpcClientProxy#subscribe 详情如下:
5、客户端服务发现流程