nacos和springCloud整合源码解析(注册中心源码分析)



前言

进两年多以来,一直用nacos做注册中心,但是nacos是如何和springcloud整合的却很少深入研究,虽然在工作中业务只需要理解原理nacos原理,知道如何使用,以及出文献问题如何解决等这些能力即可,但是不可否认的是,如何能从源码上理解nacos是如何和springCloud整个的,还是很有必要的,在阅读源码的同时,也是向优秀者学习的过程。

一、nacos是什么?

nacos是主要是用来做分布式服务的发现和治理的,同时nacos还具有担任配置中心的能力。当我们服务比交多,而且需要动态扩展时,如何让服务发现并感知其他服务的变化,以及当服务挂掉时,如何通知其他服务,这就是nacos作为注册中心要解决的问题。作为配置中心,nacos对配置文件的管理能力,同时当配置改变时,nacos还提供配置变更的通知能力。(这些是我个人的理解,也许有点大白话)

二、什么是服务的发现和治理?

我们考虑一件事情,现在有两个服务集群 A服务集群,B服务集群,其中A服务中需要用到B服务中提供的某些接口,那么就会产生出如下三个问题:

  1. A服务如何知道有哪些B服务可以调用?
  2. 当B服务如何挂掉,如何通知A服务?
  3. 以什么样的方式通知A或者B服务?

而nacos作为注册中心,要解决的也就是上面三件事情(在这里说明下,nacos只负责服务的注册和发现,并不负责服务之间如何调用的问题,不要混淆了概念,虽然有服务注册,必然涉及服务的调用)

 只要分析注册中心,一般会有一个图,就是上面的,那么这个图应该如何理解?

以nacos为例(单体服务):

首先作为注册中心,我们必须启用一个nacos服务,主要是储存生产者和消费者的相关元信息(主要包括服务名,端口和IP)

消费者和生产者都可以看做是是nacos的客户端,他们都会注册到注册中心,所以不论是消费者还是生成者,都会有一个注册动作和健康检查动作,同时对于消费者而言,还会有一个服务订阅动作,会定时或者实时去注册中心中拉去信息保存到本地。

当消费者获取到生成者信息之后,就可以进行服务调用,而服务调用已经不是注册中心该有的能力,所以本次不在此讨论,等待下次单独写一个文字进行讨论。

到了这里,我想用过nacos的或多或少应该有些疑问,我总结如下:

  1. 当我们引入nacos-client启动服务之后,服务是如何注册或者说推送到nacos注册中心的?
  2. nacos客户端和注册中心之间,应该会有一个健康检查,实时监测服务是否正常,那么这个检查是注册中心主动发起的还是客户端发起的?
  3. 作为消费者,nacos客户端是主动拉去的服务还是nacos注册中心主动推送的?
  4. 当nacos注册中心挂掉之后,服务还能相互调用吗?
  5. 当生产者有变更,消费者如何实现动态感知的?

那么带着这些问题,我们一步步跟着源码看下是如何实现的。我们本次是基于1.4版本的客户端(nacos-client )进行分析的。而在1.x的 Nacos 版本中服务端只支持 http 协议,后来为了提升性能在2.x版本引入了谷歌的 grpc,grpc 是一款长连接协议,极大的减少了 http 请求频繁的连接创建和销毁过程,能大幅度提升性能,节约资源。据官方测试,Nacos服务端 grpc 版本,相比 http 版本的性能提升了9倍以上。单这不影响我们对nacos的源码的分析。在这里我强调一点,我们分析源码是为了了解如何实现的,千万不要死扣一个细节,细节我们可以在整体了解之后有针对的进行分析。

我们本次只分析注册,对于消费者端的服务订阅下次分析
 

三、nacos作为注册中心的源码分析

千头万绪,我们分析nacos作为注册中心在springCloud中是如何整合和工作的,应该从哪里入手呢?网上好多都是直接把源码给贴出来,缺少了一个分析的过程。废话不多,先搭建一个springboot项目,同时引入相关jar包,其中主要有如下三个jar

<!-- Spring Cloud Alibaba -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>2.2.7.RELEASE</version>
</dependency>
<!--配置中心客户端-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
<version>2.2.7.RELEASE</version>
</dependency>

<dependency>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-client</artifactId>
<version>1.4.1</version>
<scope>compile</scope>
</dependency>

我们引入注册中心,一般会在springboot启动类上加上如下一个注解@EnableDiscoveryClient,那么我们就从这个注解入手,先看下这个注解干了什么事情(这个注解其实在新版springCloud可以忽略掉,不加入也能使用,至于为什么先不分析,在这里提出来是为了找一个分析的入口)

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.context.annotation.Import;

/**
 * Annotation to enable a DiscoveryClient implementation.
 * @author Spencer Gibb
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(EnableDiscoveryClientImportSelector.class)
public @interface EnableDiscoveryClient {

	/**
	 * If true, the ServiceRegistry will automatically register the local server.
	 * @return - {@code true} if you want to automatically register.
	 */
	boolean autoRegister() default true;

}

当我们看到@import的时候就应该知道,这个也是一个bean的注册类这个注解的作用就是在bean加载过程中,实例化之前,把我们指定的bean解析成BeanDefinition。

@import注解的实现看ConfigurationClassPostProcesser这个类,我们可以通过实现importSelector或者继承ImportBeanDefinitionRegistrar类来把我们自定义的bean加载到容器中

可以看到,这个注解默认自是加载一个配置类,由此可猜想,核心入口就在这个配置类中。我们找到这个类,按照ISP规范,这个类一定在spring.factories中,我们可以从spring-cloud-common包下找看看。

找到一个相似的,点进去看下


在这里发现了我们要找的类,同时到这里也可以反向说明,@EnableDiscoveryClient这个注解为什么不需要在使用,因为该类只是默认了配置项,而配置项现在已经优化默认为ture. 在该配置类中没有任何有用的信息,但是在这个类所在的Configuration类中发现了一个很重要的接口AutoServiceRegistration,根据词意可翻译成服务的自动注册。这个类是个接口,按照spring的设计理念,该接口一定是扩展接口,可让用户自定义进行实现。我们找到这个接口的实现类图

类图如下:

可以看到有一个很重要的实现 NacosAutoServiceRegistration,这也侧面反映了sprinCloud的一个设计原则,只是定义规范,但是具体的实现完全依赖各个组件本身,把依赖倒置原则用到了极致。

找到了入口,那么我们先从类图上认识下这个类,这里不推荐看类的具体实现,我们先看下类图:

通过类图可发现,该类实现了一个关键接口ApplicationListener,如果对springBoot有了解的同学一定知道,只要实现了该接口一定是和springBoot启动过程中的相关监听事件相关,这个监听事件的方法在本类中没有找到,从父类中找到了,如下:

 通过事件名称可联想到,是在web服务初始化初始化启动之后才进行该时间的发布,然后监听到事件后执行bind方法,该方法做两件事情:

第一绑定端口,服务注册要素之一(需要知道注册服务的端口信息)

第二 启动start开始进行服务注册。

我们看下start方法干了什么事情:

 抽丝剥茧,略过无用代码,只看重要部分,该方法只是做了一件事情,进行服务注册。

 register这个方法如果要找到需要跳跃很多重载重写方法,最终一定是在子类中中实现,具体业务逻辑如下,把服务注册到nacos中指定的命名空间和分组下。

到这里nacos的服务注册大体脉络我们应该已经搞清楚了,总结来说就是如下几个步骤:

  1. springBoot启动自动装载配置,进行bean实例化和初始化,
  2. 之后进行服务启动,当服务启动成功后进行事件广播
  3. NacosSereviceRegistry类监听该事件,进行端口设定,之后进行服务注册

但是到这里我们应该会有几个疑问:

  1. 事件发布在源码的哪里进行发布的?
  2. 在AutoServiceRegistrationAutoConfiguration这个类中只是进行了注入,但是却不知道什么被加载到容器的,NacosAutoServiceRegistration这个类什么时候加载到容器中的?
  3. 注册的具体逻辑是什么?

要解决上面问题,我们不得不从另外一个角度看下源码问题,刚刚我们只是从springCloud和nacos整合的角度看了下 服务是什么时候被注册的,但是注册之前的准备工作应该是什么样的呢?

这就好比做菜,客人只是点了菜,餐厅按照规范上了菜,但是做菜前的准备工作有哪些,菜如果做?这些都和客人无关。同样,NacosAutoServiceRegistration如何被注入的,以及注册的心跳如何实现的,这些都应该是nacos本身自己的实现,和springCloud无关。(说这么多废话,只是为了阐述一个源码学习的思路)。

下面分析下NacosAutoServiceRegistration什么时候被注入的,以及服务注册的心跳问题。同样的思路,按照springBoot可插拔的设计原则,我们可以看下我们引入包下是否应该有spring.factories。

nacos引入两个包,其中下面那个是服务注册的,果然下面有spring.factories这个文件夹。进入看下有什么:

 在这个配置中发现了三个主要模块,下面一一说明:

  1. EnableAutoConfiguration这个下面的所有配置类,是@SpringBootConfiguration注解会扫描并启动。@EnableAutoConfiguration 注解加载所有EnableAutoConfiguration下执行的配置类

  2. BootStrapConfiguration这个配置类下的所有配置类是在SpringBoot容器启动时的引导容器也就是祖容器进行加载的,可以不用考虑下,目前引导容器不会加载NacosDiscoveryClientConfigServiceBootstrapConfiguration这个配置类,这种方式有点过时。

  3. ApplicationListener是容器的监听类,也可以不考虑,通过配置可知,主要是用来做日志处理的,和我们要讨论的主流程无关。

基于以上分析,我们重点关注下EnableAutoConfiguration下的几个配置类,其中针对服务注册,主要关注NacosServiceRegistryAutoConfiguration这一个配置类,后面的其他配置类用到的时候再说,RibbonNacosAutoConfiguration这个配置类主要是做nacos和ribbon整合用到,是服务调用的配置类,如果有同学想知道,当通过restTemplate或者feign进行接口调用时,服务名称如何替换成具体的IP的,可以关注下这个配置类。

先看下NacosServiceRegistryAutoConfiguration这个类有什么

 从源码中可以看出,这个类中主要向容器中注入了三个bean对象,下面一一说明这三个类都是干什么用的:

  1. NacosServiceRegistry 这个类是具体干活的,它主要的工作就是进行服务注册,以及撤销注册,和获取注册的状态信息等。

  2. NacosRegistration 这个类是服务注册的元信息配置类,代表了一个注册服务的注册信息,可通过这个类获取注册的IP,端口,服务名称等。

  3. NacosAutoServiceRegistration 这个类,我想看到这里的同学应该有点印象,在文章最开始的时候,就已经说过这个类,它集成AutoServiceRegistration,实现springCloud的规范,同时也是一个监听接口,进行时间监听,然后进行服务注册。

看到这里我们可以解决上面的一个疑惑问题,NacosAutoServiceRegistration是在容器启动时,在进行nacos配置信息装配的时候加载到容器的。

那么NacosAutoServiceRegistration具体是如何进行服务注册的呢?在文章开头就说,NacosAutoServiceRegistration监听实践,获取事件后进行端口绑定,之后进行注册,其实它的具体注册逻辑是委托给NacosServiceRegistry 这个类进行处理的(源码中跟踪下很容易发现,就是一个子父类调用问题,最终在子类中会有NacosServiceRegistry 的注入,进行实际的注册动作),我们直接贴出注册那一步分的代码:

先获取命名空间,之后进行注册实例的组装

跳转到具体的注册逻辑

 注册逻辑中主要干了两件事情,第一件事情是进行心跳监测BeatReactor 这个里面实际是上启动的是一个定时任务。第二件事情就是服务的注册,我们先看服务注册是如何注册的:

 到这里应该很熟悉,就是进行注册信息的组装,然后会执行具体的服务注册,其实就是执行一个API调用,把这些信息发送给nacos注册中心,我略过了很多重载方法直接看到具体的调用方法,最终调用的是一个callServer,用过nacos的同学会很清楚,由于网络问题nacos会时不时的报一个异常,这个异常就是这里出错的,由于1.4.1版本的nacos用的是HTTP短连接进行服务注册的,定时频繁的进行服务注册,偶尔就会出现这个错误。

 我们再看下callServer中是干了什么事情:

其实也很简单,就是判断下是域名注册还是IP注册,之后进行一个请求地址的拼装,然后进行超文本协议的一个接口调用,把暴露的服务信息注册到注册中心服务端

我们再看下心跳监测是如何进行的

 可以看到,是直接启用一个定时的线程池任务进行任务执行。这个线程池是在构造方法中初始化的,我们点开BeatTask看下具体的任务执行,由于代码比较长久直接贴代码:

@Override
public void run() {
    if (beatInfo.isStopped()) {
        return;
    }
    // 获取心跳周期
    long nextTime = beatInfo.getPeriod();
    try {
        // 发送心跳,告诉服务端哪一个服务目前还正常
        JsonNode result = serverProxy.sendBeat(beatInfo, BeatReactor.this.lightBeatEnabled);
        long interval = result.get("clientBeatInterval").asLong();
        boolean lightBeatEnabled = false;
        if (result.has(CommonParams.LIGHT_BEAT_ENABLED)) {
            lightBeatEnabled = result.get(CommonParams.LIGHT_BEAT_ENABLED).asBoolean();
        }
        BeatReactor.this.lightBeatEnabled = lightBeatEnabled;
        if (interval > 0) {
            nextTime = interval;
        }
        // 判断心跳结果
        int code = NamingResponseCode.OK;
        if (result.has(CommonParams.CODE)) {
            code = result.get(CommonParams.CODE).asInt();
        }
        if (code == NamingResponseCode.RESOURCE_NOT_FOUND) {
            // 如果失败,则需要 重新注册实例
            Instance instance = new Instance();
            instance.setPort(beatInfo.getPort());
            instance.setIp(beatInfo.getIp());
            instance.setWeight(beatInfo.getWeight());
            instance.setMetadata(beatInfo.getMetadata());
            instance.setClusterName(beatInfo.getCluster());
            instance.setServiceName(beatInfo.getServiceName());
            instance.setInstanceId(instance.getInstanceId());
            instance.setEphemeral(true);
            try {
                serverProxy.registerService(beatInfo.getServiceName(),
                                            NamingUtils.getGroupName(beatInfo.getServiceName()), instance);
            } catch (Exception ignore) {
            }
        }
    } catch (NacosException ex) {
        NAMING_LOGGER.error("[CLIENT-BEAT] failed to send beat: {}, code: {}, msg: {}",
                            JacksonUtils.toJson(beatInfo), ex.getErrCode(), ex.getErrMsg());
 
    } catch (Exception unknownEx) {
        NAMING_LOGGER.error("[CLIENT-BEAT] failed to send beat: {}, unknown exception msg: {}",
                            JacksonUtils.toJson(beatInfo), unknownEx.getMessage(), unknownEx);
    } finally {
        executorService.schedule(new BeatTask(beatInfo), nextTime, TimeUnit.MILLISECONDS);
    }
}

从源码中可以看到,心跳是客户端主动定时向服务端推送该服务的信息,服务端收到该信息后,判断心跳锁上送的服务信息是否存在,如果不存在,则客户端会进行重新注册,如果存在,则不做任何操作,最后会计算下次的心跳发送时间,再次进行心跳上送,周而复始进行。

到这里我想nacos的服务的注册逻辑已经大致弄清楚了,知道nacos是如何和springCloud整合的,唯一还有一点小小的不足就是,还没有分析这个注册的事件是发生在那一块代码,针对这种情况,我们打一下端点看下,首先我们应该把端点打在我们接受到时间进行bind这块逻辑下:

总结

提示:这里对文章进行总结:

例如:以上就是今天要讲的内容,本文仅仅简单介绍了pandas的使用,而pandas提供了大量能使我们快速便捷地处理数据的函数和方法。

 

只有可以看到上面这些执行链路,我们从中发现了很熟悉的代码,refresh, 阅读过spring源码的同学应该很清楚,这是spring容器初始化的时候的一个必要过程,里面承载了spring的核心业务逻辑和执行入口,我们找到这个方法,并看下它的finishRefresh()方法

 可以看到,在spring容器的生产周期处理类中进行事件发布的,最终是在容器服务启程成功后进行服务注册:

 到这里nacos整个的注册流程都已经清楚了,下面总结下:

  1. 当容器启动时,进bean的加载是,nacos遵循springBoot的自动装配原则,进行nacos基本信息注册,主要包含NacosAutoServiceRegistration 和 NacosServiceRegistration
  2. NacosAutoServiceRegistration服务实现spring容器的applicationListener接口,监听容器服务启动完毕后的事件信息,之后进行服务注册
  3. 在服务注册时,会同时启动一个心跳监测任务,定时向注册中心推送心跳,当注册中心返回无此服务时,会进行服务的重新注册。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值