Nacos的核心功能
服务注册: Nacos Client会通过发送REST请求的方式向Nacos Server注册自己的服务。提供自身的元数据,比如ip地址、端口等信息。
Nacos Server接收到注册请求后,就会把这些元数据信息存储在一个双层的内存map中。
服务心跳:在服务注册后,Nacos Client会维护一个定时心跳来持续通知Nacos Server,说明服务一直处于可用状态,防止被剔除,默认5s发送一次心跳。
服务同步: Nacos Server集群之间会互相同步服务实例,用来保证服务信息的一致性。
服务发现: 服务消费者(Nacos Client)在调用服务提供者的服务时,会发送一个REST请求给Nacos Server,获取上面注册的服务清单,并且缓存在Nacos Client本地,同时会在Nacos Client 本地开启一个定时任务定时拉取服务端最新的注册表信息更新到本地缓存。
服务健康检查: Nacos Server会开启一个定时任务用来检查注册服务实例的健康情况,对于超过15s没有收到客户端心跳的实例会将它的healthy属性置为false(客户端服务发现时不会发现),如果某个实例超过30s没有收到心跳,直接剔除该实例(被剔除的实例如果恢复发送心跳则会重新注册)。
服务注册表
数据模型
Nacos数据模型key由三元组唯一确定,Namespace默认是空串,公共命名空间(public),分组默认是DEFAULT_GROUP**
源码篇
服务注册
客户端
下面开始进行源码分析: 首先我们找到nacos-discovery
依赖包的启动入口,springboot项目的启动器一般都是以自动配置的方式启动的,所以我们去它的spring.factories
文件寻找自动配置类:
com.alibaba.cloud.nacos.registry.NacosServiceRegistryAutoConfiguration
点进去会发现创建了一个非常重要的Bean:
@Bean
@ConditionalOnBean(AutoServiceRegistrationProperties.class)
public NacosAutoServiceRegistration nacosAutoServiceRegistration(
NacosServiceRegistry registry,
AutoServiceRegistrationProperties autoServiceRegistrationProperties,
NacosRegistration registration) {
return new NacosAutoServiceRegistration(registry,
autoServiceRegistrationProperties, registration);
}
想要知道这个类是干什么的,首先看一下它的类图:
继承了ApplicationListener
说明他可以进行事件发布,那么我们找一下onApplicationEvent
方法看看做了什么事情,跳过套娃的bind()方法,直接找到最核心的方法:start():
图中1和3的代码都是在注册前后分别发布了事件,这也是nacos客户端的一个扩展点,我们可以自己去监听这些事件做自己的业务处理;注册的主要逻辑还是在第二步,点进去点到最里面发现它就是register()方法中调用了namingService.registerInstance()
:
这个心跳的定时任务记下来,先看一下注册的逻辑,点进去,找到调用服务器接口的那段代码:
到了这一步,客户端注册就结束了,看到这里不难发现一个问题:在Nacos中,不需要使用@EnableDiscoveryClient就可以实现服务的注册。
服务端
我们切换到Nacos服务端看一下怎么处理服务注册请求的,请求的接口是/nacos/v1/ns/instance
,在这个接口中调用了serviceManager.registerInstance(namespaceId, serviceName, instance);
ServiceManager#registerInstance
看下这个方法做了哪些事情
做事情的主要就1,2两个步骤,先点进去看下1步,点到最里面调用createServiceIfAbsent
方法:
ServiceManager#createServiceIfAbsent
比较核心的就是getService()
和putServiceAndInit()
方法
- getService();
原来是从serviceMap 中通过namespaceId获取信息的,目前serviceMap中没有任何数据,所以这里返回null。在getService()方法下面去创建一个Service对象,那我们先来看一下这个Service对象的组成结构,下面只贴部分代码:
/**
* 用来检测心跳的定时任务
*/
@JsonIgnore
private ClientBeatCheckTask clientBeatCheckTask = new ClientBeatCheckTask(this);
private String namespaceId;
/**
* 如果一段时间没有发送beat,IP将被删除,默认超时时间为30秒。
*/
private long ipDeleteTimeout = 30 * 1000;
private Map<String, Cluster> clusterMap = new HashMap<>();
里面除了心跳检测的定时任务以外还有一个很重要的clusterMap,此时这个ClusterMap是空的。到目前为止,我们发现了很多有疑惑的点:
1、ServiceMap是干什么的?
2、创建出来的这个Service对象又是干什么的?
- putServiceAndInit():
代码再回到putServiceAndInit() 方法这里,点进去,核心的代码就这两行;
看putService()
这个方法名字应该能猜到一些猫腻,要把Service放到哪里呢?点进去看:
原来是把Serivce对象放到了ServiceMap里面了,也就是说下次我们再调用getSerivice(namespaceId)
的时候就可以获取到一个Serivice对象了。再看一下sevice.init()
方法:
启动了一个定时任务用来处理心跳检测的,看一下clientBeatCkeckTask
对象的run方法:
在这个方法里面主要是循环当前service的每一个临时实例 用当前时间减去最后一次心跳时间 是否大于心跳超时时间来判断心跳是否超时,如果大于这个时间会执行instance.setHealthy(false)
将实例的健康状态改为false;但是这个定时任务不会立即执行,会每5秒执行一次:
ServiceManager#createEmptyService方法的主线业务已经分析完毕,我们来小小的总结一下他到底做了什么:
1、创建一个Service对象,内部包含了一个clusterMap。
2、将service对象放入到ServiceMap中,结构为 :Map
3、开启一个定时任务用来检测实例的心跳是否超时,每5秒执行一次。
ServiceManager#addInstance
从上面的源码分析完之后Serivce对象内部结构还没有真正的初始化完。剩余的逻辑都在addInstance方法中,先剧透一下,看下我在这个方法上加的注释,这样一会也好理解:
点进去看下这个方法怎么实现的
addIpAddresses()
主要看一下addIpAddresses()方法里面做了那些事情,一直点到最里面的updateIpAddresses()方法:
这段代码就是创建一个cluster对象,将cluster对象放到service的clusterMap中。那么再看一下Cluster对象长什么样子:
@JsonIgnore
private Set<Instance> persistentInstances = new HashSet<>();
@JsonIgnore
private Set<Instance> ephemeralInstances = new HashSet<>();
这两个Set非常重要,存放的就是注册上来的实例,persistentInstances是持久实例,ephemeralInstances是临时实例,现在这两个Set还是空的。
consistencyService.put(key, instances)
现在Service也初始化完了,按照正常的逻辑来说就差最后的将注册的这个instance存入到Cluster里面了,看一下下一步怎么做的,点到最里面:
onPut(key, value):
在这里将instance包装成Datum放到dataStore里面并生成一个key,这个dataStore相当于一个暂存的点。最后task.offer将这个key和执行的动作包装成一个元组扔到内存队列里面就不管了,直接返回了。这里很神奇啊,不是说要把instance存入Cluster里面吗?怎么搞了内存队列塞进去了,因为要做异步了。那我们找找在哪里做的,看下Notifier结构:
发现他是实现Runnable接口的,那说明肯定有实现run方法,去run方法里面找找看能不能发现什么(注意:从现在开始以下代码的执行都是异步的,主线程已经结束了):
从队列中将元组拿出来调用handler方法去处理,下面是handler方法的部分代码:
这里的listener.onChange方法实现类是Service,一直点进去会调用updateIPs(value.getInstanceList(), KeyBuilder.matchEphemeralInstanceListKey(key))方法,主要看这行代码:
这个方法里面会将已经注册过的实例列表复制一份,将新的实例和老的实例都更新到一个集合汇总,最终再将这个集合更新到真正的实例列表,是一种写时复制的思想,主要是为了解决并发冲突,在写的过程汇总,其他线程读到的还是旧数据,等真正写完之后再将数据更新回去。
分析完之后我们看一下注册中心的结构长什么样子:
服务注册流程总结:
client引入nacos-discovery jar包中有一个监听方法,监听Spring容器初始化完成后,会进行节点注册,client端先要进行自动装配,扫描到了nacosAutoServiceRegistration,开启了5s的心跳发送以及向服务端进行注册实例。
nacos的server中有serviceMap,会将改实例节点存储到serviceMap中,第一个map的key是命名空间,第二个map的key是group的serviceName,value就是service类该服务实例,没有的话就行创建。
service类中有检测心跳的定时任务以及维护了一个map clusterMap,custerMap中key为clusterName, value为cluster,cluster里面有两个set一个set为存储临时节点,另一个set存储持久节点。
之后,要存储到内存注册表操作,如果是临时节点key为该实例名+ephemeral字符串,进行存储前通过判断Key是否有ephemeral字符串,如果不是临时节点就要持久化。这里有一个异步操作(本地线程实例放到内存队列中,另一个线程会从该队列总获取实例放入到server集群中)的过程,能投提升性能。
源码精髓
- 注册实例信息更新到内存注册表中。
- 同步实例信息到nacos server集群其他节点
内存注册表中更新数据使用了copyonwrite思想:内存注册表也是一个map来存储服务,写操作会重新创建一个新的map,将旧的服务复制到新的map中,然后进行更新操作,新map替换旧map。
源码精髓
这种copyonwrite思想跟eureka对比优点:时效性高,eureka二级缓存需要等待微服务拉取最新数据才进行填充,而Nacos在更新完新map就与旧map进行合并操作,响应时间快。
配置中心
Nacos Client采用的是pull的形式进行拉取,然后会发送信息到Nacos Server,这个时候会有一个等待期29.5的时候立即发给NacosClient,如果没有更新的话,会一直等到30s然后再发。