前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。
上篇文章介绍了 Nacos服务注册的原理 ,本篇文章将从客户端和服务端的角度介绍Nacos服务发现的原理。
服务发现是一种机制,用于在分布式系统中动态地查找和识别可用的服务实例。它解决了微服务架构中服务之间的通信和调用的核心问题。
在传统的单体应用中,各个组件之间的通信往往是直接的函数调用或者数据库查询,因为它们都在同一个进程内部。而在微服务架构中,各个服务被拆分成独立的、自治的服务,可能分布在不同的主机或容器中。这就需要一种机制来帮助服务在网络环境下找到彼此。
服务发现机制通过在微服务架构中引入一个独立的服务注册中心,服务实例会将自己的网络地址、端口以及其他标识信息注册到注册中心中。其他服务可以通过查询注册中心来获取所需服务的具体网络位置,从而能够进行跨服务的通信和调用。
Nacos版本如下:
客户端 | 服务端 | |
---|---|---|
版本 | Spring Cloud Alibaba 2021.0.1.0 | 1.4.1 |
对于Spring Cloud Alibaba Nacos想查看源头还是要从spring.factories
文件开始。
我们来看客户端的服务发现自动配置类 NacosDiscoveryClientConfiguration :
@Configuration(proxyBeanMethods = false)
@ConditionalOnDiscoveryEnabled
@ConditionalOnBlockingDiscoveryEnabled
@ConditionalOnNacosDiscoveryEnabled
@AutoConfigureBefore({ SimpleDiscoveryClientAutoConfiguration.class,
CommonsClientAutoConfiguration.class })
@AutoConfigureAfter(NacosDiscoveryAutoConfiguration.class)
public class NacosDiscoveryClientConfiguration {
//Nacos服务发现客户端
@Bean
public DiscoveryClient nacosDiscoveryClient(NacosServiceDiscovery nacosServiceDiscovery) {
return new NacosDiscoveryClient(nacosServiceDiscovery);
}
//定时任务,监听服务变化
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(value = "spring.cloud.nacos.discovery.watch.enabled",
matchIfMissing = true)
public NacosWatch nacosWatch(NacosServiceManager nacosServiceManager,
NacosDiscoveryProperties nacosDiscoveryProperties,
ObjectProvider<ThreadPoolTaskScheduler> taskExecutorObjectProvider) {
return new NacosWatch(nacosServiceManager, nacosDiscoveryProperties,
taskExecutorObjectProvider);
}
}
NacosDiscoveryClientConfiguration 类注入两个类 NacosDiscoveryClient 和 NacosWatch 。
NacosWatch 用于监控服务实例的变化,这里先不过多介绍。
NacosDiscoveryClient 用于管理和维护服务实例的列表,以便客户端进行服务调用和负载均衡。它实现了 DiscoveryClient 接口,DiscoveryClient 是Spring Cloud提供的用于服务发现的客户端。
public class NacosDiscoveryClient implements DiscoveryClient {
private static final Logger log = LoggerFactory.getLogger(NacosDiscoveryClient.class);
/**
* Nacos Discovery Client Description.
*/
public static final String DESCRIPTION = "Spring Cloud Nacos Discovery Client";
//服务发现类
private NacosServiceDiscovery serviceDiscovery;
//是否开启容忍失败获取
@Value("${spring.cloud.nacos.discovery.failure-tolerance-enabled:false}")
private boolean failureToleranceEnabled;
public NacosDiscoveryClient(NacosServiceDiscovery nacosServiceDiscovery) {
this.serviceDiscovery = nacosServiceDiscovery;
}
@Override
public String description() {
return DESCRIPTION;
}
/**
* 根据serviceId获取某个服务实例的列表
* @param serviceId
* @return
*/
@Override
public List<ServiceInstance> getInstances(String serviceId) {
try {
//从注册中心获取服务实例列表
return Optional.of(serviceDiscovery.getInstances(serviceId)).map(instances -> {
//将服务实例放入缓存中
ServiceCache.setInstances(serviceId, instances);
return instances;}).get();
}
catch (Exception e) {
//如果容忍获取失败开启,获取失败后从本地缓存中返回实例列表
if (failureToleranceEnabled) {
return ServiceCache.getInstances(serviceId);
}
throw new RuntimeException(
"Can not get hosts from nacos server. serviceId: " + serviceId, e);
}
}
/**
* 获取所有服务名称
* @return
*/
@Override
public List<String> getServices() {
try {
return Optional.of(serviceDiscovery.getServices()).map(services -> {
ServiceCache.set(services);
return services;
}).get();
}
catch (Exception e) {
log.error("get service name from nacos server fail,", e);
//如果容忍获取失败开启,获取失败后从本地缓存中返回服务名列表
return failureToleranceEnabled ? ServiceCache.get() : Collections.emptyList();
}
}
}
NacosDiscoveryClient 在进行服务调用时,会调用 getInstances()
方法返回该服务的所有实例信息,实际通过 NacosServiceDiscovery#getInstances
方法将 Instance 列表 转为 ServiceInstance 列表并返回。
getInstances()
方法中在服务最新实例列表获取成功后会放入 ServiceCache 缓存中,获取失败抛异常后会从 ServiceCache 缓存中获取实例列表。这就保证了当注册中心挂了以后,客户端在一定程度上保证了服务的正常调用。
public final class ServiceCache {
private ServiceCache() {
}
private static List<String> services = Collections.emptyList();
//key:服务id ,value:服务对应的实例列表
private static Map<String, List<ServiceInstance>> instancesMap = new ConcurrentHashMap<>();
public static void setInstances(String serviceId, List<ServiceInstance> instances) {
instancesMap.put(serviceId, Collections.unmodifiableList(instances));
}
public static List<ServiceInstance> getInstances(String serviceId) {
return Optional.ofNullable(instancesMap.get(serviceId)).orElse(Collections.emptyList());
}
public static void set(List<String> newServices) {
services = Collections.unmodifiableList(newServices);
}
public static List<String> get() {
return services;
}
}
NacosServiceDiscovery 用于服务的发现,当一个微服务需要调用其他服务时,可以通过NacosServiceDiscovery类从Nacos注册中心中查询目标服务的信息,包括服务名称、IP地址、端口号等信息。
重点看下NacosServiceDiscovery#getInstances
方法:
public class NacosServiceDiscovery {
private NacosDiscoveryProperties discoveryProperties;
private NacosServiceManager nacosServiceManager;
public NacosServiceDiscovery(NacosDiscoveryProperties discoveryProperties,
NacosServiceManager nacosServiceManager) {
this.discoveryProperties = discoveryProperties;
this.nacosServiceManager = nacosServiceManager;
}
/**
* 返回某个服务的所有实例信息
* Return all instances for the given service.
* @param serviceId id of service
* @return list of instances
* @throws NacosException nacosException
*/
public List<ServiceInstance> getInstances(String serviceId) throws NacosException {
String group = discoveryProperties.getGroup();
//获取服务的实例列表
List<Instance> instances = namingService().selectInstances(serviceId, group,
true);
//把Instance实例信息转为ServiceInstance信息
return hostToServiceInstanceList(instances, serviceId);
}
/**
* 返回所有的服务名称
* Return the names of all services.
* @return list of service names
* @throws NacosException nacosException
*/
public List<String> getServices() throws NacosException {
String group = discoveryProperties.getGroup();
ListView<String> services = namingService().getServicesOfServer(1,
Integer.MAX_VALUE, group);
return services.getData();
}
//Instance转成ServiceInstance
public static List<ServiceInstance> hostToServiceInstanceList(
List<Instance> instances, String serviceId) {
List<ServiceInstance> result = new ArrayList<>(instances.size());
for (Instance instance : instances) {
ServiceInstance serviceInstance = hostToServiceInstance(instance, serviceId);
if (serviceInstance != null) {
result.add(serviceInstance);
}
}
return result;
}
//Instance转成ServiceInstance
public static ServiceInstance hostToServiceInstance(Instance instance,
String serviceId) {
if (instance == null || !instance.isEnabled() || !instance.isHealthy()) {
return null;
}
NacosServiceInstance nacosServiceInstance = new NacosServiceInstance();
nacosServiceInstance.setHost(instance.getIp());
nacosServiceInstance.setPort(instance.getPort());
nacosServiceInstance.setServiceId(serviceId);
Map<String, String> metadata = new HashMap<>();
metadata.put("nacos.instanceId", instance.getInstanceId());
metadata.put("nacos.weight", instance.getWeight() + "");
metadata.put("nacos.healthy", instance.isHealthy() + "");
metadata.put("nacos.cluster", instance.getClusterName() + "");
if (instance.getMetadata() != null) {
metadata.putAll(instance.getMetadata());
}
metadata.put("nacos.ephemeral", String.valueOf(instance.isEphemeral()));
nacosServiceInstance.setMetadata(metadata);
if (metadata.containsKey("secure")) {
boolean secure = Boolean.parseBoolean(metadata.get("secure"));
nacosServiceInstance.setSecure(secure);
}
return nacosServiceInstance;
}
private NamingService namingService() {
return nacosServiceManager
.getNamingService(discoveryProperties.getNacosProperties());
}
}
当进行服务调用的时候,NacosServiceDiscovery#getInstances()
会从注册中心获取被调用服务的所有实例信息,获取到实例信息之后会通过hostToServiceInstanceList()
方法把实例信息转为 ServiceInstance 列表,ServiceInstance类会存放服务实例的IP、端口号、元数据等信息。
从 getInstances()
方法中可以看到,它是调用的NamingService#selectInstances()
方法来获取实例信息的,接下来下NamingService#selectInstances()
方法的实现:
private HostReactor hostReactor;
@Override
public List<Instance> selectInstances(String serviceName, String groupName, List<String> clusters, boolean healthy,boolean subscribe) throws NacosException {
ServiceInfo serviceInfo;
//如果是订阅方式,从本地缓存获取服务
if (subscribe) {
serviceInfo = hostReactor.getServiceInfo(NamingUtils.getGroupedName(serviceName, groupName),
StringUtils.join(clusters, ","));
} else {
//直接从服务端拉取服务实例列表
serviceInfo = hostReactor
.getServiceInfoDirectlyFromServer(NamingUtils.getGroupedName(serviceName, groupName),
StringUtils.join(clusters, ","));
}
return selectInstances(serviceInfo, healthy);
}
private List<Instance> selectInstances(ServiceInfo serviceInfo, boolean healthy) {
List<Instance> list;
if (serviceInfo == null || CollectionUtils.isEmpty(list = serviceInfo.getHosts())) {
return new ArrayList<Instance>();
}
Iterator<Instance> iterator = list.iterator();
while (iterator.hasNext()) {
Instance instance = iterator.next();
//移除健康状况 != healthy的,或者不接受请求的,或者权重<= 0的
if (healthy != instance.isHealthy() || !instance.isEnabled() || instance.getWeight() <= 0) {
iterator.remove();
}
}
return list;
}
第一个selectInstances(String, String, List
方法会根据 subscribe 判断是否订阅了调用的服务,如果是则从本地缓存中获取服务信息,否则从服务端直接获取服务信息,通常情况下 subscribe 都为true。然后通过它的重载方法selectInstances(ServiceInfo, boolean)
筛选出可用实例,并返回。
接下来主要就是看HostReactor#getServiceInfo()
方法和HostReactor#getServiceInfoDirectlyFromServer()
方法。
HostReactor#getServiceInfoDirectlyFromServer()
方法很简单,就是直接从注册中心获取服务列表。
private final NamingProxy serverProxy;
public ServiceInfo getServiceInfoDirectlyFromServer(final String serviceName, final String clusters)
throws NacosException {
//从注册中心获取最新服务列表服务
String result = serverProxy.queryList(serviceName, clusters, 0, false);
if (StringUtils.isNotEmpty(result)) {
return JacksonUtils.toObj(result, ServiceInfo.class);
}
return null;
}
再看下NamingProxy#queryList()
方法:
public String queryList(String serviceName, String clusters, int udpPort, boolean healthyOnly)
throws NacosException {
final Map<String, String> params = new HashMap<String, String>(8);
params.put(CommonParams.NAMESPACE_ID, namespaceId);
params.put(CommonParams.SERVICE_NAME, serviceName);
params.put("clusters", clusters);
params.put("udpPort", String.valueOf(udpPort));
params.put("clientIP", NetUtils.localIP());
params.put("healthyOnly", String.valueOf(healthyOnly));
//直接调用服务端instance/list接口
return reqApi(UtilAndComs.nacosUrlBase + "/instance/list", params, HttpMethod.GET);
}
queryList()
方法会携带客户端服务名和版本号等信息,直接先服务端发起查询服务列表请求。
重点来看一下HostReactor#getServiceInfo()
方法:
//服务信息缓存
private final Map<String, ServiceInfo> serviceInfoMap;
//待更新服务缓存
private final Map<String, Object> updatingMap;
public ServiceInfo getServiceInfo(final String serviceName, final String clusters) {
NAMING_LOGGER.debug("failover-mode: " + failoverReactor.isFailoverSwitch());
String key = ServiceInfo.getKey(serviceName, clusters);
//快速失败开关是否打开
if (failoverReactor.isFailoverSwitch()) {
return failoverReactor.getService(key);
}
//从serviceInfoMap缓存中获取服务信息
ServiceInfo serviceObj = getServiceInfo0(serviceName, clusters);
//如果serviceInfoMap缓存中没有,则创建一个服务并放入缓存中
if (null == serviceObj) {
//创建一个服务
serviceObj = new ServiceInfo(serviceName, clusters);
//放入缓存中
serviceInfoMap.put(serviceObj.getKey(), serviceObj);
//放入待更新服务缓存中
updatingMap.put(serviceName, new Object());
//从注册中心获取最新服务列表,立即更新集群中的服务信息
updateServiceNow(serviceName, clusters);
//集群信息更新完成,从待更新中删除
updatingMap.remove(serviceName);
} else if (updatingMap.containsKey(serviceName)) {
//如果在待更新的服务缓存中
//更新时间间隔如果大于UPDATE_HOLD_INTERVAL=5s
if (UPDATE_HOLD_INTERVAL > 0) {
// 等待UPDATE_HOLD_INTERVAL时间直到updateServiceNow方法更新完成
synchronized (serviceObj) {
try {
serviceObj.wait(UPDATE_HOLD_INTERVAL);
} catch (InterruptedException e) {
NAMING_LOGGER
.error("[getServiceInfo] serviceName:" + serviceName + ", clusters:" + clusters, e);
}
}
}
}
//添加一个定时更新服务的任务
scheduleUpdateIfAbsent(serviceName, clusters);
return serviceInfoMap.get(serviceObj.getKey());
}
private void updateServiceNow(String serviceName, String clusters) {
try {
updateService(serviceName, clusters);
} catch (NacosException e) {
NAMING_LOGGER.error("[NA] failed to update serviceName: " + serviceName, e);
}
}
/**
* Update service now.
*
* @param serviceName service name
* @param clusters clusters
*/
public void updateService(String serviceName, String clusters) throws NacosException {
ServiceInfo oldService = getServiceInfo0(serviceName, clusters);
try {
//获取集群中服务列表
String result = serverProxy.queryList(serviceName, clusters, pushReceiver.getUdpPort(), false);
if (StringUtils.isNotEmpty(result)) {
processServiceJson(result);
}
} finally {
if (oldService != null) {
synchronized (oldService) {
oldService.notifyAll();
}
}
}
}
接下来看一下scheduleUpdateIfAbsent()
方法:
private final Map<String, ScheduledFuture<?>> futureMap = new HashMap<String, ScheduledFuture<?>>();
public void scheduleUpdateIfAbsent(String serviceName, String clusters) {
if (futureMap.get(ServiceInfo.getKey(serviceName, clusters)) != null) {
return;
}
synchronized (futureMap) {
if (futureMap.get(ServiceInfo.getKey(serviceName, clusters)) != null) {
return;
}
//添加一个定时任务
ScheduledFuture<?> future = addTask(new UpdateTask(serviceName, clusters));
futureMap.put(ServiceInfo.getKey(serviceName, clusters), future);
}
}
public synchronized ScheduledFuture<?> addTask(UpdateTask task) {
//延迟1秒执行定时任务,UpdateTask定时任务每10秒执行一次
return executor.schedule(task, DEFAULT_DELAY, TimeUnit.MILLISECONDS);
}
scheduleUpdateIfAbsent()
方法中会添加一个每10s执行一次的 UpdateTask 定时任务。 UpdateTask 类是 HostReactor 的一个内部类,它实现了 Runnable 接口。它用于实现服务实例的动态更新和维护,保证服务实例列表的及时更新和健康状态的维护,从而提高服务的可用性和稳定性。
至此客户端服务发现就结束了。
总结一下客户端的服务发现,消费者发起服务调用,通常会采用 subscribe 方式获取服务实例列表,也就是从本地缓存中获取实例列表,如果注册中心宕机,客户端也能调用到服务,提高了Nacos的可用性。
服务端发现主要就是看服务端收到客户端请求后会做哪些操作,
@GetMapping("/list")
@Secured(parser = NamingResourceParser.class, action = ActionTypes.READ)
public ObjectNode list(HttpServletRequest request) throws Exception {
String namespaceId = WebUtils.optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID);
String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME);
//检查服务名格式
checkServiceNameFormat(serviceName);
String agent = WebUtils.getUserAgent(request);
String clusters = WebUtils.optional(request, "clusters", StringUtils.EMPTY);
String clientIP = WebUtils.optional(request, "clientIP", StringUtils.EMPTY);
int udpPort = Integer.parseInt(WebUtils.optional(request, "udpPort", "0"));
String env = WebUtils.optional(request, "env", StringUtils.EMPTY);
boolean isCheck = Boolean.parseBoolean(WebUtils.optional(request, "isCheck", "false"));
String app = WebUtils.optional(request, "app", StringUtils.EMPTY);
String tenant = WebUtils.optional(request, "tid", StringUtils.EMPTY);
boolean healthyOnly = Boolean.parseBoolean(WebUtils.optional(request, "healthyOnly", "false"));
//获取服务全部信息
return doSrvIpxt(namespaceId, serviceName, agent, clusters, clientIP, udpPort, env, isCheck, app, tenant,
healthyOnly);
}
InstanceController#list()
方法会对请求参数进行解析,然后传递给 doSrvIpxt()
方法。doSrvIpxt()
方法首先会从 ServiceManager 本地缓存中获取服务信息,如果获取不到直接返回;否则进行各种判断去查询服务实例的全部信息,然后再返回给客户端。
@Autowired
private ServiceManager serviceManager;
public ObjectNode doSrvIpxt(String namespaceId, String serviceName, String agent, String clusters, String clientIP,
int udpPort, String env, boolean isCheck, String app, String tid, boolean healthyOnly) throws Exception {
ClientInfo clientInfo = new ClientInfo(agent);
ObjectNode result = JacksonUtils.createEmptyJsonNode();
//从缓存中获取服务
Service service = serviceManager.getService(namespaceId, serviceName);
long cacheMillis = switchDomain.getDefaultCacheMillis();
// now try to enable the push
try {
if (udpPort > 0 && pushService.canEnablePush(agent)) {
pushService
.addClient(namespaceId, serviceName, clusters, agent, new InetSocketAddress(clientIP, udpPort),
pushDataSource, tid, app);
cacheMillis = switchDomain.getPushCacheMillis(serviceName);
}
} catch (Exception e) {
Loggers.SRV_LOG
.error("[NACOS-API] failed to added push client {}, {}:{}", clientInfo, clientIP, udpPort, e);
cacheMillis = switchDomain.getDefaultCacheMillis();
}
//缓存中没有,说明该服务不存在或者下线了
if (service == null) {
if (Loggers.SRV_LOG.isDebugEnabled()) {
Loggers.SRV_LOG.debug("no instance to serve for service: {}", serviceName);
}
result.put("name", serviceName);
result.put("clusters", clusters);
result.put("cacheMillis", cacheMillis);
result.replace("hosts", JacksonUtils.createEmptyArrayNode());
return result;
}
//检查服务是否能用
checkIfDisabled(service);
//省略若干代码
result.replace("hosts", hosts);
if (clientInfo.type == ClientInfo.ClientType.JAVA
&& clientInfo.version.compareTo(VersionUtil.parseVersion("1.0.0")) >= 0) {
result.put("dom", serviceName);
} else {
result.put("dom", NamingUtils.getServiceName(serviceName));
}
result.put("name", serviceName);
result.put("cacheMillis", cacheMillis);
result.put("lastRefTime", System.currentTimeMillis());
result.put("checksum", service.getChecksum());
result.put("useSpecifiedURL", false);
result.put("clusters", clusters);
result.put("env", env);
result.replace("metadata", JacksonUtils.transferToJsonNode(service.getMetadata()));
return result;
}
至此,服务端的服务发现就结束,也相对简单一些。
总结一下:Nacos注册中心会在本地缓存或持久化存储中查找指定服务名的实例列表,并返回给服务消费者。如果本地缓存中没有该服务的实例信息,Nacos注册中心会直接返回。
最后总结一下服务发现的流程:
NacosDiscoveryClient#getInstances()
方法获取服务实例列表 ,然后调用NacosServiceDiscovery#getInstances()
方法NacosServiceDiscovery#getInstances()
方法会调用 NamingService#selectInstances()
方法去查询实例信息,查询到之后会放入 ServiceCache 服务缓存中,如果因为网络或服务端宕机等原因出现异常,如果缓存中有的话,会从 ServiceCache 返回该服务实例信息,这提高了Nacos服务调用的可用性;在NamingService#selectInstances()
方法, 因为是subscribe=true
,所以会调用 HostReactor#getServiceInfo()
方法。HostReactor#getServiceInfo()
方法中,会先从serviceInfoMap 缓存中获取服务信息,如果没有则会创建一个服务,然后会把创建的服务放到待更新缓存 updatingMap 中,接着就去更新服务实例信息,更新完成再从 updatingMap 中剔除。 如果 serviceInfoMap 缓存中能获取到服务信息,则会判断是否该服务是否在待更新缓存 updatingMap 中,在的话就会要等待更新任务完成。HostReactor#getServiceInfo()
方法中会调用scheduleUpdateIfAbsent()
方法创建一个 UpdateTask 定时任务,定时任务延迟1s执行,之后会每隔10s执行一次。UpdateTask 任务主要工作就是用于实现服务实例的动态更新和维护,保证服务实例列表的及时更新和健康状态的维护,从而提高服务的可用性和稳定性。InstanceController#list()
方法,该方法会处理请求参数,然后将参数传递给doSrvIpxt()
方法,最后doSrvIpxt()
方法会从缓存中获取服务信息,获取不到直接返回;否则会对服务进行各种条件判断,最后返回服务实例全部信息。