前言
本文基于以下版本讲解
- Spring Cloud Alibaba Nacos Discovery 2.2.4
- Nacos Client 1.4.1
作为阿里开源的一款Spring Cloud实现,在国内基本取代了Netflix实现成为了Java生态微服务框架首选,它是如何实现客户端的呢,在其中有什么设计精妙之处?
Spring Cloud Alibaba Nacos 服务发现
入口
Spring Cloud Alibaba Nacos使用META-INF下的spring.factories来将一些Bean的配置文件加载到Spring容器。
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.alibaba.cloud.nacos.discovery.NacosDiscoveryAutoConfiguration,\
com.alibaba.cloud.nacos.ribbon.RibbonNacosAutoConfiguration,\
com.alibaba.cloud.nacos.endpoint.NacosDiscoveryEndpointAutoConfiguration,\
com.alibaba.cloud.nacos.registry.NacosServiceRegistryAutoConfiguration,\
com.alibaba.cloud.nacos.discovery.NacosDiscoveryClientConfiguration,\
com.alibaba.cloud.nacos.discovery.reactive.NacosReactiveDiscoveryClientConfiguration,\
com.alibaba.cloud.nacos.discovery.configclient.NacosConfigServerAutoConfiguration,\
com.alibaba.cloud.nacos.NacosServiceAutoConfiguration
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
com.alibaba.cloud.nacos.discovery.configclient.NacosDiscoveryClientConfigServiceBootstrapConfiguration
主要Bean的功能以及相互之间的关系
关于服务发现我们只用关注com.alibaba.cloud.nacos.discovery下的一些类。 先顺着Spring Cloud Alibaba Naco的配置一点一点的往里看。 在NacosServiceAutoConfiguration 中配置了:NacosServiceManager 在NacosDiscoveryAutoConfiguration 中配置了:NacosDiscoveryProperties ,NacosServiceDiscovery 在NacosDiscoveryClientConfiguration 配置了:DiscoveryClient ,NacosWatch 配置类不是重点,重点是里面配置的Bean。汇总一下,通过spring.factories配置的Bean:NacosServiceManager ,NacosDiscoveryProperties ,NacosServiceDiscovery ,DiscoveryClient ,NacosWatch 。这些配置里面Bean之间的关系如下: 
可以看到在上图中出现比较频繁就是NacosServiceManager和NacosDiscoveryProperties这两个Bean。为什么他们出现这么频繁?我们带着问题继续往下看。我们由表及里从NacosDiscoveryClient这个类开始介绍。
不得已创建的Bean: NacosDiscoveryClient
上面说到我们是由表及里的探索,那为什么NacosDiscoveryClient是表呢?Spring Cloud作为一个抽象服务框架,它提供了一些微服务功能的抽象接口在Spring Cloud Commont。Spring Cloud Commont中关于服务发现的抽象的discovery包下有一个DiscoveryClient,是不是和NacosDiscoveryClient的名称有些类似?不用怀疑NacosDiscoveryClient就是实现了DiscoveryClient接口,提供了获取所有服务名称 和通过指定名称获取所有实例 的功能。所以NacosDiscoveryClient是Spring Cloud Alibaba Nacos的最表层,对外提供服务。
NacosDiscoveryClient是Spring Cloud Alibaba不得不实现的一个类,这是Spring Cloud 规范,所以可以把它理解成适配类。通过它适配Spring Cloud和Spring Cloud Alibaba。Spring Cloud Alibaba提供获取所有服务名称 和通过指定名称获取所有实例 功能是隐藏在NacosDiscoveryClient成员变量中的NacosServiceDiscovery,见上图。
承上启下的转换器: NacosServiceDiscovery
虽然NacosServiceDiscovery被NacosDiscoveryClient引用实现了获取所有服务名称 和通过指定名称获取所有实例 功能。但NacosServiceDiscovery依然是Spring Cloud Alibaba中的一个类,并不是Nacos包中的类。也就是说真正的功能的实现也不是在它这,但是它并不是像NacosDiscoveryClient那样简单的封装。它起到了Nacos服务信息和Spring Cloud Commont服务信息之间适配(转换)的作用,通过这个类打通了Spring Cloud Commont与Nacos的服务发现与注册服务。 NacosServiceDiscovery的承上(连接Nacos):通过注入的两个Bean–NacosServiceManager ,NacosDiscoveryProperties 来实现。通过NacosServiceManager ,NacosDiscoveryProperties 来获取Nacos包下的NamingService,并通过NamingService来获取服务的信息。
com.alibaba.nacos.api.naming.NamingService :实现这个接口的类会提供服务实例的注册,取消注册,获取某个服务下的所有实例信息,也可以通过某些条件(健康状态)筛选实例,服务订阅等功能。
NacosServiceDiscovery的启下(连接Spring Cloud):在NacosServiceDiscovery中有一个com.alibaba.cloud.nacos.discovery.NacosServiceDiscovery#hostToServiceInstance 方法,这个方法的作用就是将Nacos的服务信息转为Spring Cloud Commont定义的org.springframework.cloud.client.ServiceInstance 。
工具人双子星:NacosServiceManager和NacosDiscoveryProperties
NacosDiscoveryProperties:保存Nacos的一些配置信息,已经做一些默认配置处理。 NacosServiceManager:通过NacosDiscoveryProperties的配置信息构建出Nacos包中的NamingService和NamingMaintainService方便在Spring Cloud Alibaba Nacos中使用。
上面将Spring Cloud Alibaba Nacos服务发现的基本Bean之间关系已经他们的功能都简单介绍了一下。它们的实现逻辑都不复杂,都是对Nacos的一些核心类的封装。在了解了基本信息,我们就更进一步对Spring Cloud Alibaba Nacos进行分析。
被削弱的NacosWatch
NacosWatch在之前版本的Spring Cloud Alibaba中具备很多功能,包含:服务获取、改变、发布监听事件等等。可以说NacosWatch原来很强,是Spring Cloud Alibaba Nacos的核心功能类。但是在较新版本中,NacosWatch只做下面几个简单的功能: 周期性发送心跳事件,这里的心跳事件不是向Nacos服务器发送,而是Spring Cloud Commont下的心跳事件。 订阅服务变更事件,用于修改当前实例的元数据变更。
上面说到NacosWatch被削弱了,那从NacosWatch移除的功能放到哪了呢?我可以觉得是Nacos团队应该是出于高内聚的考虑,将服务注册与发现核心的功能都统一放在Nacos包中,在Spring Cloud Alibaba Nacos中只是对Nacos核心模块的封装,以适配Spring Cloud规范,所以NacosWatch被移除的功能都放到了下层的Nacos包中。这段只是个人理解。
Spring Cloud Alibaba Nacos 服务注册
上面讨论的都是关于服务发现的,那服务是何时注册到Nacos中,以何种方式呢?
AbstractAutoServiceRegistration,NacosAutoServiceRegistration父子合力完成注册
Spring Cloud Alibaba Nacos不是自己独立完成注册动作,而是使用Spring Cloud Commons包中定义好的规范:AbstractAutoServiceRegistration。看名称就能知道这是一个抽象类。这个抽象类中有几个点需要关注:onApplicationEvent(),接收WebServerInitializedEvent事件,在服务启动的时会发布这个事件。在onApplicationEvent()中会调用org.springframework.cloud.client.serviceregistry.AbstractAutoServiceRegistration#bind方法,里面最主要的是调用了一个start方法。NacosAutoServiceRegistration就是重写start方法,在start方法也没有太多的逻辑,重点是调用了注册的方法register。
@Override
public void register(Registration registration) {
...
//和服务发现一样,都是获取NamingService
NamingService namingService = namingService();
String serviceId = registration.getServiceId();
String group = nacosDiscoveryProperties.getGroup();
Instance instance = getNacosInstanceFromRegistration(registration);
try {
//通过NamingService完成注册
namingService.registerInstance(serviceId, group, instance);
...
}
catch (Exception e) {
...
}
}
这里有父类模板方法设计模式的影子,同样也有使用生命周期方法start,stop的设计等使设计更清晰。Spring Cloud Alibaba Nacos通过监听服务启动事件来完成注册(这里面同样也借助了Spring Cloud Commons)。我们也能看到服务发现与注册都和NamingService这个类相关。
小结&一些问题
上面基本就是Spring Cloud Alibaba Nacos服务发现的设计思路。在Spring Cloud Alibaba Nacos中上面我也说到,它只是做一些核心功能的封装用于适配Spring cloud,所以逻辑都不会复杂。这里顺便提一句,Spring Cloud Alibaba Nacos也同样实现了响应式的客户端(可能也是迫于无奈,毕竟Spring Cloud commons里面有这样的接口)。看到这里了,有些同学就会有一些疑问了:你把这些类的功能和关系都说了,可是我还是不懂Nacos具体的实现逻辑,也是云里雾里的。能不能说一些实际的。 下面我们说点实际的,带着问题来看Nacos的核心服务发现代码。 1.Nacos是如何感应到服务的上下线的? 2.Nacos服务端发生故障宕机后Nacos客户端会有什么反应? 3.Nacos客户端是如何存储服务信息的,会存放哪些服务信息?是注册中心所有服务的数据吗?
在没有查看Nacos源码之前其实大多数人也能猜一下。 1.通过心跳去检测服务之间的健康状态,大多数分布式系统都是这样的机制。 2.本地会有缓存,使用以前缓存的服务信息。 3.使用Map之类的容器。
那Nacos客户端实际实现是这样吗?
Nacos核心模块
Nacos分为三个模块:nacos-api,nacos-client,nacos-common。api主要是放一下接口,接口的实现放在client中,common放一些通用的类,比如:HttpClient,生命周期方法,Nacos自己实现的事件通知。举一个例子:上面提到的NamingService就是一个接口在nacos-api中,它的实现NacosNamingService在nacos-client中。
NamingService简介
上面Spring Cloud Alibaba Nacos服务发现与注册都和NamingService这个类有关系,我们就先介绍一下这个接口。它提供下面的这些功能:
- 注册实例
- 取消注册实例
- 获取指定服务的实例,支持条件筛选(是否健康,是否订阅)
- 订阅指定名称服务的变化
- 取消订阅
- 获取Nacos服务器中所有服务名称
- 获取所有订阅的服务
- 获取服务健康状态
- 关闭服务发现与注册
NacosNamingMaintainService
上面列举了NamingService提供的一些功能,但是没有删除服务 ,修改服务 的一些操作。Nacos团队将这些操作给NacosNamingMaintainService了。笔者也不是很明白为什么Nacos团队要将服务相关的操作分成两个类,NamingService中的职责也不是很纯粹,如果将服务相关与实例相关的操作分开是不是更好?研究了半天发现NacosNamingMaintainService的操作和NamingService操作的一点区别:NacosNamingMaintainService都是直接操作Nacos服务器,NamingService会有一些本地缓存的操作。在NacosNamingMaintainService修改了服务没有修改缓存,而是由定时任务拉取服务变化,修改本地缓存。
NamingService的实现NacosNamingService详解
作为NamingServcie的实现,NacosNamingServcie提供了所有NamingService中声明的功能。
NacosNamingService构造方法
public NacosNamingService(Properties properties) throws NacosException {
init(properties);
}
private void init(Properties properties) throws NacosException {
ValidatorUtils.checkInitParam(properties);
this.namespace = InitUtils.initNamespaceForNaming(properties);
InitUtils.initSerialization();
initServerAddr(properties);
InitUtils.initWebRootContext(properties);
initCacheDir();
initLogName(properties);
this.serverProxy = new NamingProxy(this.namespace, this.endpoint, this.serverList, properties);
this.beatReactor = new BeatReactor(this.serverProxy, initClientBeatThreadCount(properties));
this.hostReactor = new HostReactor(this.serverProxy, beatReactor, this.cacheDir, isLoadCacheAtStart(properties),
isPushEmptyProtect(properties), initPollingThreadCount(properties));
}
构造方法接收Nacos配置信息,主要的初始化在init方法中完成。很显眼的三个成员变量初始化被单独的放在最后:NamingProxy ,BeatReactor ,HostReactor 。
NamingProxy
它的主要职责是通过Http和Nacos服务器通信,所以它提供了服务发现和注册服务器接口调用的方法,提供给NacosNamingService和NacosNamingMaintainService使用。同时也有一个另外的一个定时获取Endpoint的任务,这个功能放在这有点迷。
BeatReactor
提供和Nacos服务器维持心跳的服务。建立的心跳的前提是当前节点是临时节点,在注册到Nacos服务端时添加心跳,如果由于短暂网络中断,GC等原因导致服务端任务该实例下线,会主动发起重新注册。
HostReactor
笔者认为它更像一个功能增强类,在指定调用Nacos服务端获取信息的基础上,添加了服务实例信息缓存和事件通知,定时查询服务信息等功能。笔者认为Nacos其实使用一种策略模式提供几种不同的模式配置。现在是通过本地缓存然后定时去Nacos服务器拉取服务信息比较服务是否发生变化,发生变化后会发出事件通知。如果后续除了Http主动拉取的方式,新增了Nacos服务端主动推送的模式(当然现在也有tcp推送)如何更优雅的新增呢?(以上都是笔者的愚见,不一定对)。 上面的一些愚见也将HostReactor的主要作用说出来了,它就是在Nacos客户端查询某个服务实例的时候会缓存它的信息,并产生一个定时拉取这个服务的任务,在它发生变化后会通知到对应的监听器。所以这个类的很重要,名称也用了Host。 理解了NacosNamingService这三个主要成员变量的作用,Nacos Client的功能也就差不多了。
Nacos容灾处理
Nacos客户端是如何应对Nacos服务端挂掉的情况呢?其实Nacos客户端本来就会将本节点需要使用到的服务的信息存放在了本地内存中,同样也会写入磁盘。FailoverReactor在其中也起到到了重要作用,它会定时的将内存中服务信息写入本地磁盘。通过下面的代码我们可以看到Nacos判断有没有发生异常时读一个文件UtilAndComs.FAILOVER_SWITCH 内容来判断。不过笔者找了很久也没有找到在哪里会修改这个文件内容,可能是需要手动修改吧。。。
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);
}
...
}
com.alibaba.nacos.client.naming.backups.FailoverReactor.SwitchRefresher#run
try {
File switchFile = new File(failoverDir + UtilAndComs.FAILOVER_SWITCH);
if (!switchFile.exists()) {
switchParams.put("failover-mode", "false");
NAMING_LOGGER.debug("failover switch is not found, " + switchFile.getName());
return;
}
long modified = switchFile.lastModified();
if (lastModifiedMillis < modified) {
lastModifiedMillis = modified;
String failover = ConcurrentDiskUtil.getFileContent(failoverDir + UtilAndComs.FAILOVER_SWITCH,
Charset.defaultCharset().toString());
if (!StringUtils.isEmpty(failover)) {
String[] lines = failover.split(DiskCache.getLineSeparator());
for (String line : lines) {
String line1 = line.trim();
if ("1".equals(line1)) {
switchParams.put("failover-mode", "true");
NAMING_LOGGER.info("failover-mode is on");
new FailoverFileReader().run();
} else if ("0".equals(line1)) {
switchParams.put("failover-mode", "false");
NAMING_LOGGER.info("failover-mode is off");
}
}
} else {
switchParams.put("failover-mode", "false");
}
}
} catch (Throwable e) {
NAMING_LOGGER.error("[NA] failed to read failover switch.", e);
}
将当前节点需要的服务信息写入磁盘还有一个好处是加快启动。如果当前实例使用到的服务较多需要大量的从Nacos服务器请求,则可以通过配置选择启动时从磁盘加载以前写入的服务信息。
填坑
上面列举了几个问题,下面就一一解答一下,如有不对的地方欢迎指正。 1.Nacos是如何感应到服务的上下线的? Nacos服务器通过与客户端之间维持心跳感应实例是否健康,但是这也是只针对临时实例。如果没有特别的设定,都是临时实例。如果是持久实例在发生异常后,是不会被Nacos服务器删除,只是会显示不健康。 Nacos客户端是定时从Nacos服务器拉取服务实例信息与本地缓存信息比对来感应服务上下线。
2.Nacos服务端发生故障宕机后Nacos客户端会有什么反应? 没有什么反应。只是在请求Nacos服务的时候会报错。
3.Nacos客户端是如何存储服务信息的,会存放哪些服务信息?是注册中心所有服务的数据吗? 确实是存放在一个Map结构中。不会存放所有服务的数据,只是会在请求该服务时缓存对应的实例信息,并添加一个定时任务去监控服务信息。
总结
以上就是Nacos客户端的主要内容了。当然还有很多其他细节,比如请求Nacos服务器相同接口相同内容的请求qps限定为5等等。HostReactor只是讲了他的一些作用,并没有讲解里面的实现,感兴趣的读者可以自行详细洋酒。笔者在前面也提到过之前很多逻辑都是在NacsoWatch这个类中,后面都下沉了,也是看到了Nacos团队的努力,希望Nacos越做越好。 本篇博客里面的各种观点都是笔者的一些愚见,如有不对之处欢迎指正。
参考资料
Nacos客户端源码
|