前面我们介绍了Nacos的简单使用,此篇,我们将围绕Nacos的客户端来展开,一步步揭开Nacos神秘的面纱,这次我们使用的是Nacos1.4.1(21年初)版本,尽管现在已经出了2.0版本,他们之间最大的改变是1.X的Http请求,2.X使用的是grpc,但是市面上用得最多的仍然是1.X版本,我们只需要学会他的思想 ,万变不离其宗.Spring Cloud版本为Hoxton.SR8,Spring Cloud Alibaba版本为2.2.5.RELEASE.
我们Nacos服务端(注册中心)启动好之后,我们的业务微服务,例如仓库服务,库存服务只需要引入Nacos客户端的pom,加上配置文件就可以注册上我们的Nacos服务,我们的仓库服务,库存服务就是Nacos客户端,它引入了Nacos client,才能注册到Nacos服务.至于Nacos AP和CP模式后面会讲,这里先有一个概念.
下图是Nacos客户端向Nacos服务端注册的时候的一个参数ephemeral()是否临时节点),默认是true,true就代表使用的AP模式,服务端集群同步客户端注册过来的数据的时候用的阿里的Distro协议,需要注意的一点就是,AP模式下所有服务端集群节点都是平等的,即没有主从节点的概念,所有节点都有可能接受客户端的注册请求,然后会将注册的实例数据同步到其它集群节点。
如下图我的客户端 订单服务要注册到我们的Nacos集群(集群为了避免单点故障),他只会请求到集群中的某一台机器,假设现在注册到了NacosServer1,那我下一次去NacosServer2拉取数据的时候不就找不到数据.所以我们需要NacosServer1,NacosServer2,NacosServer3之间数据同步.AP模式是没有leader的概念,集群之间数据同步会最终一致.
1.我们需要注册到Nacos服务端,所以一定会有注册到服务端功能
2.Nacos服务端需要知道我客户端是否还存活,所以客户端必须会有一个定时任务,定时向服务端发送心跳.
3.Nacos客户端必须知道有哪一些服务注册到了服务端,这样才能调用另外的服务.所以客户端必须会有一个定时任务,定时拉取服务端对应的服务列表.
下面所有源码介绍都会围绕这3个功能去说.
1.入口一:熟悉SpringBoot自动配置原理我们会知道,SpringBoot会自动扫描我们所有jar下面的Spring.factories,并且加载我们的自动配置类.如下图的pom.xml以及对应Nacos-discovery的Spring.factories文件
2.入口二:启动类上面@EnableDiscoveryClient,但是新版本我们已经不需要写这个注解了,因为自动配置帮我们做了他应该做的事情,如下图,这个注解已经不需要了.
pom.xml
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
这个注解做了两件事,一个是import了EnableDiscoveryClientImportSelector,另外一个就是默认autoRegister为true,开启自动注册,并且引入AutoServiceRegistrationConfiguration到Spring容器.后面会说这个注解为什么加不加都行.
1.4.1 NacosServiceAutoConfiguration
首先是NacosServiceAutoConfiguration,这里只加载了NacosServiceManager,是service核心管理类,NamingService就是他管理的
1.4.2 NacosDiscoveryAutoConfiguration
这个是用于服务发现的,他加载了两个类NacosDiscoveryProperties和NacosServiceDiscovery
NacosDiscoveryProperties是配置类,我们配置注册中心的信息就是在这里配置。
NacosServiceDiscovery主要是用于服务发现。
1.4.3 NacosServiceRegistryAutoConfiguration
1.NacosServiceRegistryAutoConfiguration,这个是用于自动注册的.我们看到他上面的注解信息,@AutoConfigureAfter确保AutoServiceRegistrationConfiguration先加载,然后判断spring.cloud.service-registry.auto-registration.enabled是否为true,为true才可以加载。所以上面@EnableDiscoveryClient的autoRegister如果设置为false,则不加载。 因为spring.cloud.service-registry.auto-registration.enabled默认是true的,所以@EnableDiscoveryClient其实不引入,也是可以加载NacosServiceRegistryAutoConfiguration.所以才说为什么不需要@EnableDiscoveryClient.
2.这个类会加载NacosServiceRegistry,NacosRegistration,NacosAutoServiceRegistration.
NacosServiceRegistry,NacosRegistration会注入到NacosAutoServiceRegistration.所以重点在于NacosAutoServiceRegistration.
NacosServiceRegistry主要用于注册、取消注册。
NacosRegistration是当前实例的信息,比如实例名称、实例id、端口、ip等.
NacosAutoServiceRegistration初始化的时候会调用方法调用上面的NacosServiceRegistry去注册到服务端.后面会详细说这里
1.4.4 NacosDiscoveryClientConfiguration
这里加载了两个类,DiscoveryClient和NacosWatch.
DiscoveryClient主要用于实例获取和服务获取。
NacosWatch实现了SmartLifecycle接口,所以会调用start和stop方法。
start方法主要是加入NamingEvent监听、获取NacosNamingService、注册监听、发布HeartbeatEvent事件。
我们可以看到NacosAutoServiceRegistration他实现了ApplicationEventListener,所以启动的时候,Spring会调用其中的onApplicationEvent方法,而这个方法在他的父类AbstractAutoServiceRegistration里面.
AbstractAutoServiceRegistration#bind,onApplicationEvent调用了bind,bind主要逻辑调用了this.start().
我们初始化NacosAutoServiceRegistration的时候传入了NacosServiceRegistry,这个类里面有注册,取消注册的方法,而上面的start()方法最终会调用到NacosServiceRegistry#register
这里的registration,参数是我们初始化的时候传进来的NacosRegistration是当前实例的信息,比如实例名称、实例id、端口、ip等.
上面的NacosServiceRegistry#register,最终会调用到我的NacosNamingService#registerInstance.
这个NacosNamingService就是在我们1.4红色类NacosWatch实例化的,这样就串起来了.在这个registerInstance方法我们做了两件事情
1.调用serverProxy#registerService,这里才是真正的注册.serverProxy的类是NamingProxy
2.判断当前节点是否临时实例(默认true后面讲AP,CP架构会解释)如果是则调用beatReactor#addBeatInfo,心跳任务
这里我们发现了他发送了一个POST请求,路径是/v1/ns/instance,我们在Nacos官网可以看到,这个接口是,Nacos服务端注册实例的接口,由此我们可以知道两个关键信息,Nacos客户端和服务端交互用的http请求,Nacos服务端就是一个普通的web服务,并且,他是随机请求一台Nacos服务,即我们配置文件配置的server-addr: 127.0.0.1:8848,127.0.0.1:8849,127.0.0.1:8850的随机一个.
这里有同学会有疑问,假如我的Nacos服务是集群呢?随机请求一台Nacos服务他如何同步到整个集群?我们后面讲Nacos集群会解释,他利用Distro协议来保证整个集群数据最终一致.
NamingProxy#reqApi 这里是随机挑选一个服务去请求 Random,并且如果异常,会重试,单机的时候nacosDomain不为空,则重试最大次数,集群的时候会一个个节点重试.最后还失败,抛异常.
beatReactor#buildBeatInfo 封装心跳信息
beatReactor#addBeatInfo 新增心跳任务
beatReactor#addBeatInfo,这个方法,它定义了一个延时线程池,默认延时5秒之后执行BeatTask#run
既然是线程池我们就得看BeatTask#run
他这里给服务端发送了心跳,如果返回发现没心跳信息则重新注册,然后继续套娃线程池丢下一个5秒的延时任务,保持心跳.这样做的好处是不需要受固定的时间间隔的约束
这里我们发现了他发送了一个PUT请求,路径是/v1/ns/instance/beat,我们在Nacos官网可以看到,这个接口是,Nacos服务端注册实例心跳保活接口,和上面的请求一样.
这里有同学会有疑问,假如我的Nacos服务是集群呢?心跳随机请求一台Nacos服务他如何同步到整个集群?我们后面讲Nacos集群会解释
这是1.4的第二个红标类这里加载了两个类,DiscoveryClient和NacosWatch.
DiscoveryClient主要用于实例获取和服务获取。
NacosWatch实现了SmartLifecycle接口,所以Spring启动会调用start和stop方法。
start方法主要是加入NamingEvent监听、获取NacosNamingService、注册监听、发布HeartbeatEvent事件。
上面的NacosServiceManager#getNamingService会调用buildNamingService这个方法,这里加锁做了双重校验,不存在就通过反射创建NacosNamingService
最终通过反射创建NacosNamingService
这个类实例化会调用NacosNamingService#init
主要是初始化namespace、序列化、注册中心服务地址、WebRootContext上下文、缓存路径、日志名称、NamingProxy、BeatReactor、HostReactor。 NamingProxy负责和Nacos服务的通信,比如服务注册、服务取消注册、心跳等。 BeatReactor负责检测心跳。 HostReactor负责获取、更新并保存服务信息。
private void init(Properties properties) throws NacosException {
ValidatorUtils.checkInitParam(properties);
// namespace默认public
this.namespace = InitUtils.initNamespaceForNaming(properties);
// 序列化初始化
InitUtils.initSerialization();
// 注册中心服务地址初始化,这个从配置文件取
initServerAddr(properties);
//初始化WebRootContext上下文
InitUtils.initWebRootContext();
// 初始化缓存路径 System.getProperty("user.home") + "/nacos/naming/" + namespace
initCacheDir();
// 初始化日志名称naming.log
initLogName(properties);
// 初始化NamingProxy
this.serverProxy = new NamingProxy(this.namespace, this.endpoint, this.serverList, properties);
// 初始化BeatReactor
this.beatReactor = new BeatReactor(this.serverProxy, initClientBeatThreadCount(properties));
// 初始化HostReactor
this.hostReactor = new HostReactor(this.eventDispatcher, this.serverProxy, beatReactor, this.cacheDir,
isLoadCacheAtStart(properties), initPollingThreadCount(properties));
}
我们可以看到这个类的方法都是负责和Nacos服务的通信,比如服务注册、服务取消注册、心跳等,他还有一个功能,就是定时任务去endpoint拉取我最新的Nacos地址,这一点我们了解一下就行.
关于endpoint是什么,其实就是一个地址服务器,请求他,返回一连串Nacos服务地址,但是我们一般用得不多,我们一般是在配置文件配置一个nginx反向代理nacos集群,endpoint详情看下面博客介绍.
Nacos 集群部署模式最佳实践_阿里巴巴中间件-CSDN博客
下图是NamingProxy的一些方法
这个类上面1.4已经描述过,服务注册之后,会开启一个定时任务,不断给服务端发心跳
这个类主要是获取,更新并保存我传入的微服务信息.
我们再来看一下NacosWatch#start
这里初始化了NamingService,并且调用了namingService#subscribe
namingService#subscribe最终会调用HostReactor#subcribe方法
他会调用HostReactor#getServiceInfo
里面有个成员变量serviceInfoMap,用来专门存服务的信息,假设我们user服务启动,他就会调用updateServiceNow去Nacos获取user服务所有服务的信息,并且有一个定时任务定时去更新user服务的信息,包括当前有多少个节点等.假设我user服务去调用order服务,这个时候也会调用getServiceInfo这个方法去Nacos服务获取,然后缓存到serviceInfoMap,并且开启定时任务.Nacos重写了Spring cloud规范的接口,所以Feing获取其他微服务的信息会调用Nacos的getServiceInfo.
public ServiceInfo getServiceInfo(final String serviceName, final String clusters) {
NAMING_LOGGER.debug("failover-mode: " + failoverReactor.isFailoverSwitch());
// key为{服务名}@@{集群名}。
String key = ServiceInfo.getKey(serviceName, clusters);
if (failoverReactor.isFailoverSwitch()) {
return failoverReactor.getService(key);
}
// 读取本地服务列表的缓存,缓存是一个Map,格式:Map
ServiceInfo serviceObj = getServiceInfo0(serviceName, clusters);
if (null == serviceObj) {
//创建空ServiceInfo
serviceObj = new ServiceInfo(serviceName, clusters);
serviceInfoMap.put(serviceObj.getKey(), serviceObj);
updatingMap.put(serviceName, new Object());
//远程去Nacos调用接口获取服务信息
updateServiceNow(serviceName, clusters);
updatingMap.remove(serviceName);
} else if (updatingMap.containsKey(serviceName)) {
// 缓存中有,但是需要更新
if (UPDATE_HOLD_INTERVAL > 0) {
// hold a moment waiting for update finish
synchronized (serviceObj) {
try {
serviceObj.wait(UPDATE_HOLD_INTERVAL);
} catch (InterruptedException e) {
NAMING_LOGGER
.error("[getServiceInfo] serviceName:" + serviceName + ", clusters:" + clusters, e);
}
}
}
}
//开启定时任务定时去Nacos更新服务的信息
scheduleUpdateIfAbsent(serviceName, clusters);
return serviceInfoMap.get(serviceObj.getKey());
}
updateServiceNow会调用updateService获取服务信息,并且更新我们的serviceInfoMap,这里不仅仅获取健康的节点,个人猜想这个和Nacos的健康阈值有关
调用Nacos服务的/v1/ns/instance/list,获取服务信息
返回结果
这里面addTask,肯定是放进一个阻塞队列或者加入一个线程池里面
果然开启了一个延时任务,1秒后执行,所以我们应该去看UpateTask的run方法.Update肯定实现了Runnable
这个类是HostReactor的一个内部类,他的Run方法就是调用上面的updateService方法去刷新服务的信息,然后finally里面又套娃开启了一个延时任务,多少秒之后重新执行.
public class UpdateTask implements Runnable {
long lastRefTime = Long.MAX_VALUE;
private final String clusters;
private final String serviceName;
/**
* the fail situation. 1:can't connect to server 2:serviceInfo's hosts is empty
*/
private int failCount = 0;
public UpdateTask(String serviceName, String clusters) {
this.serviceName = serviceName;
this.clusters = clusters;
}
private void incFailCount() {
int limit = 6;
if (failCount == limit) {
return;
}
failCount++;
}
private void resetFailCount() {
failCount = 0;
}
@Override
public void run() {
long delayTime = DEFAULT_DELAY;
try {
ServiceInfo serviceObj = serviceInfoMap.get(ServiceInfo.getKey(serviceName, clusters));
if (serviceObj == null) {
updateService(serviceName, clusters);
return;
}
if (serviceObj.getLastRefTime() <= lastRefTime) {
updateService(serviceName, clusters);
serviceObj = serviceInfoMap.get(ServiceInfo.getKey(serviceName, clusters));
} else {
// if serviceName already updated by push, we should not override it
// since the push data may be different from pull through force push
refreshOnly(serviceName, clusters);
}
lastRefTime = serviceObj.getLastRefTime();
if (!notifier.isSubscribed(serviceName, clusters) && !futureMap
.containsKey(ServiceInfo.getKey(serviceName, clusters))) {
// abort the update task
NAMING_LOGGER.info("update task is stopped, service:" + serviceName + ", clusters:" + clusters);
return;
}
if (CollectionUtils.isEmpty(serviceObj.getHosts())) {
incFailCount();
return;
}
delayTime = serviceObj.getCacheMillis();
resetFailCount();
} catch (Throwable e) {
incFailCount();
NAMING_LOGGER.warn("[NA] failed to update serviceName: " + serviceName, e);
} finally {
executor.schedule(this, Math.min(delayTime << failCount, DEFAULT_DELAY * 60), TimeUnit.MILLISECONDS);
}
}
}
构建hostReactor的时候,他会初始化我们的serviceInfoMap,并且他还new FailoverReactor和
new PushReceiver
当服务注册到Nacos服务端的时候,假设我们的HostReactor#UpdateTask没有及时去拉取最新的服务信息,这段定时任务的时间段内我们的serviceInfoMap肯定还是老的,所以Nacos服务端还有一个机制,当服务注册到Nacos服务端的时候,用UDP去广播给其他服务端,告诉其他服务端新注册了一个服务,你们更新一下serviceInfoMap.这样就可以近实时的刷新我们缓存的服务列表
这是故障转移模式,也就是做容灾备份
我们都知道即使Nacos服务挂了,我们的Nacos客户端仍然可以依靠serviceInfoMap保证一定的可用信.但是注册中心发生故障最坏的一个情况是整个 Server 端宕机,这时候 Nacos 依旧有高可用机制做兜底。
注册中心发生故障最坏的一个情况是整个 Server 端宕机,如果三个Server 端都宕机了,怎么办呢?这时候 Nacos 依旧有高可用机制做兜底。
本地缓存文件 Failover 机制,Nacos 存在本地文件缓存机制,nacos-client 在接收到 nacos-server 的服务推送之后,会在内存中保存一份,随后会落盘存储一份快照snapshot 。有了这份快照,本地的RPC调用,还是能正常的进行。
关键是,这个本地文件缓存机制,默认是关闭的。
Nacos 注册中心宕机,Dubbo /springcloud 应用发生重启,会不会影响 RPC 调用。如果了解了 Nacos 的 Failover 机制,应当得到和上一题同样的回答:不会。
这份文件有两种价值,一是用来排查服务端是否正常推送了服务;二是当客户端加载服务时,如果无法从服务端拉取到数据,会默认从本地文件中加载。snapshot 默认的存储路径为:{USER_HOME}/nacos/naming/ 中:
在生产环境,推荐开启该参数,以避免注册中心宕机后,导致服务不可用,在服务注册发现场景,可用性和一致性 trade off 时,我们大多数时候会优先考虑可用性。
另外:{USER_HOME}/nacos/naming/{namespace} 下除了缓存文件之外还有一个 failover 文件夹,里面存放着和 snapshot 一致的文件夹。
这是 Nacos 的另一个 failover 机制,snapshot 是按照某个历史时刻的服务快照恢复恢复,而 failover 中的服务可以人为修改,以应对一些极端场景。
该可用性保证存在于 nacos-client 端。
可以参考下面的源码分享
Nacos源码分析05-客户端本地缓存与故障转移_努力推石头的西西弗斯-CSDN博客
一文详解 Nacos 高可用特性 - 知乎
我有一段很简单的代码就是我当前user服务调用mall-order服务的接口,ribbon和Fegin调用的结果一样,Feign底层就是ribbon,如下代码段.
@RequestMapping(value = "/findOrderByUserId/{id}")
public R findOrderByUserId(@PathVariable("id") Integer id) {
log.info("根据userId:"+id+"查询订单信息");
// RestTemplate调用
// String url = "http://localhost:8020/order/findOrderByUserId/"+id;
// R result = restTemplate.getForObject(url,R.class);
//模拟ribbon实现
//String url = getUri("mall-order")+"/order/findOrderByUserId/"+id;
// 添加@LoadBalanced
String url = "http://mall-order/order/findOrderByUserId/"+id;
R result = restTemplate.getForObject(url,R.class);
//feign调用
//R result = orderFeignService.findOrderByUserId(id);
return result;
}
当我第一次调用的时候我们在上面HostReactor#getServiceInfo打一个断点.
很明显我们的ribbon负载均衡器去调用Nacos获取了服务列表.
这里我们简单说一下feign的原理,他是包装了我们的ribbon,然后通过我们的restTemplate拦截器,去修改我们的地址.
例如我当前调用http://mall-order/order/findOrderByUserId/1,他会去Nacos服务器获取我们的mall-order服务的所有ip:port,然后缓存起来. 例如我现在获取了127.0.0.1:8001,127.0.0.1:8002,他会根据你选的负载均衡策略选出一个地址,然后替换为http://127.0.0.1:8001/order/findOrderByUserId/1
我们看一下我我们ribbonLoadBalancer负载均衡器初始化的时候传入了我们的NacosServerList,所以不注入我们默认的ribbonServerList,然后获取服务列表的时候自然是去Nacos服务获取,然后缓起来,以后每次都去缓存获取,然后有一个定时任务去我们的serviceInfoMap获取服务信息去更新Feign自己的缓存.
1.我们需要注册到Nacos服务端,所以一定会有注册到服务端功能
2.Nacos服务端需要知道我客户端是否还存活,所以客户端必须会有一个定时任务,定时向服务端发送心跳.
3.Nacos客户端必须知道有哪一些服务注册到了服务端,这样才能调用另外的服务.所以客户端必须会有一个定时任务,定时拉取服务端所有的服务列表.
我们会发现上述3个主要功能都是通过Http请求去Nacos服务端获取的,并且他们有许多定时任务去获取我们的服务列表以及发送心跳.
我们也了解到Nacos高可用不仅仅是服务端做集群,也可以是客户端缓存了服务列表以及failover机制.