引言
这是Nacos官方的架构图,可以发现主要的两大模块:ConfigService(配置中心)、NamingService(注册中心),至于什么是配置中心和注册中心这种人尽皆知的问题就不解释了,本篇文章就是站在源码的角度去研究一下nacos的注册流程并且感受一下他所用到的一些值得我们学习的思想。
服务端源码环境搭建
- 拉取源码:github.com/alibaba/nac…
- 本地启动服务端单机模式需设置参数:
-Dnacos.standalone=true
客户端源码分析
Nacos客户端需要导入依赖:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
注意:在Nacos中如果让客户端主动注册服务器需要导入web启动器
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
下面开始进行源码分析:
首先我们找到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客户端的一个扩展点,我们可以自己去监听这些事件做自己的业务处理;注册的主要逻辑还是在第2步,点进去点到最里面发现它是在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。在getSerivce()方法下面去创建一个Serivce对象,那我们先来看一下这个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是空的。到目前为止,我们发现了有很多疑惑的点:
- SerivceMap是干什么的?
- 创建出来的这个Service对象又是干什么的? 这里先做个标记,带着这个问题继续往下走。
- putServiceAndInit():
代码再回到
putServiceAndInit()
方法这里,点进去,核心的代码就这两行: 看putService()
这个方法名字应该能猜到一些猫腻,要把Service放到哪里呢?点进去看: 原来是把Serivce对象放到了ServiceMap里面了,也就是说下次我们再调用getSerivice(namespaceId)
的时候就可以获取到一个Serivice对象了。再看一下sevice.init()
方法: 启动了一个定时任务用来处理心跳检测的,看一下clientBeatCkeckTask
对象的run方法: 在这个方法里面主要是循环当前service的每一个临时实例 用当前时间减去最后一次心跳时间 是否大于心跳超时时间来判断心跳是否超时,如果大于这个时间会执行instance.setHealthy(false)
将实例的健康状态改为false;但是这个定时任务不会立即执行,会每5秒执行一次:
ServiceManager#createEmptyService方法的主线业务已经分析完毕,我们来小小的总结一下他到底做了什么:
- 创建一个Serivice对象,内部包含了一个clusterMap。
- 将service对象放入到SeriviceMap中,结构为:Map<namespaceId, Map<groupName::serviceName, Service>>。
- 开启一个定时任务用来检测实例的心跳是否超时,每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))方法,主要看这行代码:这个方法里面会将已经注册过的实例列表复制一份,将新的实例和老的实例都更新到一个集合中,最终再将这个集合更新到真正的实例列表,是一种写时复制的思想,主要时为了解决并发冲突,在写的过程中,其他线程读到的还是旧数据,等真正写完之后再将数据更新回去。
分析完之后我们看一下注册中心的结构长什么样子:
忧劳可以兴国,逸豫可以亡身。