Nacos注册中心源码解析(一):服务注册

2,389 阅读5分钟

引言

image.png 这是Nacos官方的架构图,可以发现主要的两大模块:ConfigService(配置中心)、NamingService(注册中心),至于什么是配置中心和注册中心这种人尽皆知的问题就不解释了,本篇文章就是站在源码的角度去研究一下nacos的注册流程并且感受一下他所用到的一些值得我们学习的思想。

服务端源码环境搭建

  1. 拉取源码:github.com/alibaba/nac…
  2. 本地启动服务端单机模式需设置参数:-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 image.png 点进去会发现创建了一个非常重要的Bean:

@Bean
@ConditionalOnBean(AutoServiceRegistrationProperties.class)
public NacosAutoServiceRegistration nacosAutoServiceRegistration(
      NacosServiceRegistry registry,
      AutoServiceRegistrationProperties autoServiceRegistrationProperties,
      NacosRegistration registration) {
   return new NacosAutoServiceRegistration(registry,
         autoServiceRegistrationProperties, registration);
}

想要知道这个类是干什么的,首先看一下它的类图: image.png 继承了ApplicationListener说明他可以进行事件发布,那么我们找一下onApplicationEvent方法看看做了什么事情,跳过套娃的bind()方法,直接找到最核心的方法:start(): image.png 图中1和3的代码都是在注册前后分别发布了事件,这也是nacos客户端的一个扩展点,我们可以自己去监听这些事件做自己的业务处理;注册的主要逻辑还是在第2步,点进去点到最里面发现它是在register()方法中调用了namingService.registerInstance(): image.png 这个心跳的定时任务记下来,先看一下注册的逻辑,点进去,找到调用服务器接口的那段代码: image.png 到了这一步,客户端注册就结束了。看到这里不难发现一个问题:在Nacos中,不需要使用@EnableDiscoveryClient就可以实现服务的注册。

服务端源码分析

我们切换到Nacos服务端看一下怎么处理服务注册请求的,请求的接口是/nacos/v1/ns/instance,在这个接口中调用了serviceManager.registerInstance(namespaceId, serviceName, instance);

ServiceManager#registerInstance

看下这个方法做了哪些事情 image.png 做事情的主要就1,2两个步骤,先点进去看下1步,点到最里面调用createServiceIfAbsent方法:

ServiceManager#createServiceIfAbsent

image.png 比较核心的就是getService()putServiceAndInit()方法

  • getService()image.png image.png原来是从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是空的。到目前为止,我们发现了有很多疑惑的点:

  1. SerivceMap是干什么的?
  2. 创建出来的这个Service对象又是干什么的? 这里先做个标记,带着这个问题继续往下走。
  • putServiceAndInit(): 代码再回到putServiceAndInit()方法这里,点进去,核心的代码就这两行: image.pngputService()这个方法名字应该能猜到一些猫腻,要把Service放到哪里呢?点进去看: image.png 原来是把Serivce对象放到了ServiceMap里面了,也就是说下次我们再调用getSerivice(namespaceId)的时候就可以获取到一个Serivice对象了。再看一下sevice.init()方法: image.png 启动了一个定时任务用来处理心跳检测的,看一下clientBeatCkeckTask对象的run方法: image.png 在这个方法里面主要是循环当前service的每一个临时实例 用当前时间减去最后一次心跳时间 是否大于心跳超时时间来判断心跳是否超时,如果大于这个时间会执行instance.setHealthy(false)将实例的健康状态改为false;但是这个定时任务不会立即执行,会每5秒执行一次: image.png

ServiceManager#createEmptyService方法的主线业务已经分析完毕,我们来小小的总结一下他到底做了什么:

  1. 创建一个Serivice对象,内部包含了一个clusterMap。
  2. 将service对象放入到SeriviceMap中,结构为:Map<namespaceId, Map<groupName::serviceName, Service>>。
  3. 开启一个定时任务用来检测实例的心跳是否超时,每5秒执行一次。

ServiceManager#addInstance

从上面的源码分析完之后Serivce对象内部结构还没有真正的初始化完。剩余的逻辑都在addInstance方法中,先剧透一下,看下我在这个方法上加的注释,这样一会也好理解: image.png 点进去看下这个方法咋实现的: image.png

addIpAddresses()

主要看一下addIpAddresses()方法里面做了那些事情,一直点到最里面的updateIpAddresses()方法: image.png 这段代码就是创建一个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里面了,看一下下一步怎么做的,点到最里面: image.png

  • onPut(key, value):image.pngimage.png在这里将instance包装成Datum放到dataStore里面并生成一个Key,这个dataStore相当于一个暂存的点。最后task.offer将这个key和执行的动作包装成一个元组扔到内存队列里面就不管了,直接返回了。这里很神奇啊,不是说要把instance存入Cluster里面吗,怎么搞了个内存队列塞进去了,因为要做异步了。那我们找找在哪里做的,看下Notifier的结构:image.png发现他是实现Runnable接口的,那说明肯定有实现run方法,去run方法里面找找看能不能发现什么(注意:从现在开始以下代码的执行都是异步的,主线程已经结束了):image.png从队列中将元组拿出来调用handler方法去处理,下面是handler方法的部分代码:image.png这里的listener.onChange方法实现类是Service,一直点进去会调用updateIPs(value.getInstanceList(), KeyBuilder.matchEphemeralInstanceListKey(key))方法,主要看这行代码:image.png这个方法里面会将已经注册过的实例列表复制一份,将新的实例和老的实例都更新到一个集合中,最终再将这个集合更新到真正的实例列表,是一种写时复制的思想,主要时为了解决并发冲突,在写的过程中,其他线程读到的还是旧数据,等真正写完之后再将数据更新回去。

分析完之后我们看一下注册中心的结构长什么样子: 注册表结构.png

忧劳可以兴国,逸豫可以亡身。