> 文档中心 > SpringCloud Alibaba之Nacos客户端服务注册源码分析

SpringCloud Alibaba之Nacos客户端服务注册源码分析


为什么要分析源码

  1. 提升技术功底:学习源码里面的优秀的设计思想,比如一些问题的解决问题思路,还有一些优秀的设计模式,提升自己的技术功底。

  2. 深度掌握框架:源码看多了,对于一个新技术或者框架的掌握速度会有大幅度提升,看下框架的演示Demo就基本上知道了底层实现原理,学习框架的速度会非常快。

  3. 快速定位问题:遇到问题,特别是框架源码的Bug问题,能够快速定位,这就是多看源码所带来的的好处和优势。

  4. 提高面试成功率:面试一线互联网大厂,一般都会问到框架源码级别的实现,如果掌握了源码,会大大提升面试成功几率和薪资待遇。

  5. 参与开源社区:参与到开源项目的研发,结识更多大牛,对于自己以后的提升好处多多。

看源码的方法

  1. 先使用:先看官方网站提供的文档,快速掌握框架的基本使用

  2. 关注核心功能:在使用的过程中关注框架的核心功能,然后来观察这些核心功能的代码

  3. 总结归纳:总结源码中的一些核心点,同时最好能够跟着源码来做出核心流程图,这样就可以把源码中的核心亮点找出并且标记,后续就可能会借鉴到实际工作项目中,同时要善于用Debug,来观看源码的执行过程,观察一些关键变量的值的变化。当我们把框架的所有功能点的源码都分析完成后,回到主流程再梳理一遍,最后在自己脑袋中形成一个闭环,这样源码的核心内容和主流程就基本上理解了。

Nacos服务注册与发现源码剖析

Nacos核心功能点

服务注册:Nacos Client会通过发送REST请求的方式向Nacos Server注册自己的服务,提供自身的元数据,比如ip地址、端口等信息。Nacos Server接收到注册请求后,就会把这些元数据信息存储在一个双层的内存Map中。 

服务心跳:在服务注册后,Nacos Client会维护一个定时心跳来持续通知Nacos Server,说明服务一直处于可用状态,防止被剔除。默认5s发送一次心跳。

服务健康检查:Nacos Server会开启一个定时任务用来检查注册服务实例的健康情况,对于超过15s没有收到客户端心跳的实例会将它的healthy属性置为false(客户端服务发现时不会发现),如果某个实例超过30秒没有收到心跳,直接剔除该实例(被剔除的实例如果恢复发送心跳则会重新注册)

服务发现:服务消费者(Nacos Client)在调用服务提供者的服务时,会发送一个REST请求给Nacos Server,获取上面注册的服务清单,并且缓存在Nacos Client本地,同时会在Nacos Client本地开启一个定时任务定时拉取服务端最新的注册表信息更新到本地缓存

服务同步:Nacos Server集群之间会互相同步服务实例,用来保证服务信息的一致性。

Nacos服务端原理

图片

Nacos客户端原理

图片

其实从以上的两张图中我们就能够找到突破口了,其实核心内容就集中在nacos-console、nacos-naming、nacos-config,这几个模块中。那么知道了这些,现在我们就来进行Nacos的源码下载,然后我们具体分析。

Nacos源码下载

我们本次需要通过Nacos来运行Nacos,所以下载地址为:https://github.com/alibaba/nacos,我们需要通过Idea来进行下载,下载完成之后是这样子的

图片

设置单机运行

windows版本

-Dnacos.standalone=true -Dnacos.home=C:\\nacos

mac版本

-Dnacos.standalone=true -Dnacos.home=/Users/xxx/SoftWare/nacos

看下每个模块的意义

图片

启动Nacos-Server端

MySQL配置

图片

SQL文件,需要在自己的MySQL中执行

图片

启动结果:

图片

Nacos客户端服务注册源码剖析

我们从Nacos-Client开始说起,那么说到客户端就涉及到服务注册,我们先了解一下Nacos客户端都会将什么信息传递给服务器,我们直接从Nacos Client项目的NamingTest说起,代码注释写在了图片上面。

图片

其实这就是客户端注册的一个测试类,它模仿了一个真实的服务注册进Nacos的过程,包括NacosServer连接、实例的创建、实例属性的赋值、注册实例,所以在这个其中包含了服务注册的核心代码,仅从此处的代码分析,可以看出,Nacos注册服务实例时,包含了两大类信息:Nacos Server连接信息和实例信息。

Nacos Server连接信息

Nacos Server连接信息,存储在Properties当中,包含以下信息:

  • Server地址:Nacos服务器地址,属性的key为serverAddr;

  • 用户名:连接Nacos服务的用户名,属性key为username,默认值为nacos;

  • 密码:连接Nacos服务的密码,属性key为password,默认值为nacos;

实例信息

注册实例信息用Instance对象承载,注册的实例信息又分两部分:实例基础信息和元数据。

实例基础信息包括

  • instanceId:实例的唯一ID;

  • ip:实例IP,提供给消费者进行通信的地址;

  • port:端口,提供给消费者访问的端口;

  • weight:权重,当前实例的权重,浮点类型(默认1.0D);

  • healthy:健康状况,默认true;

  • enabled:实例是否准备好接收请求,默认true;

  • ephemeral:实例是否为瞬时的,默认为true;

  • clusterName:实例所属的集群名称;

  • serviceName:实例的服务信息;

Instance类包含了实例的基础信息之外,还包含了用于存储元数据的metadata(描述数据的数据),类型为HashMap,从当前这个Demo中我们可以得知存放了两个数据:

  • netType:顾名思义,网络类型,这里的值为external,也就是外网的意思;

  • version:版本,Nacos的版本,这里是2.0这个大版本。

除了Demo中这些“自定义”的信息,在Instance类中还定义了一些默认信息:

图片

preserved.heart.beat.interval:心跳间隙的key,默认为5s,也就是默认5秒进行一次心跳;preserved.heart.beat.timeout:心跳超时的key,默认为15s,也就是默认15秒收不到心跳,实例将会标记为不健康;preserved.ip.delete.timeout:实例IP被删除的key,默认为30s,也就是30秒收不到心跳,实例将会被移除;preserved.instance.id.generator:实例ID生成器key,默认为simple;

至于实例ID生成器key,可以参考文章 互联网大厂的分布式ID解决方案(上)。

NamingService接口

上面的demo中,实例注册是通过NameService进行注册,NameService是通过一个工厂来生成的,我们可以先看下NameService接口。

  • 服务实例注册

    void registerInstance(...) throws NacosException;
  • 服务实例注销

    void deregisterInstance(...) throws NacosException;
  • 获取服务实例列表

    List getAllInstances(...) throws NacosException;
  • 查询健康服务实例

    List selectInstances(...) throws NacosException;
  • 查询集群中健康的服务实例

    List selectInstances(....List clusters....)throws NacosException;
  • 使用负载均衡策略选择一个健康的服务实例

    Instance selectOneHealthyInstance(...) throws NacosException;
  • 订阅服务事件

    void subscribe(...) throws NacosException;
  • 取消订阅服务事件

    void unsubscribe(...) throws NacosException;
  • 获取所有(或指定)服务名称

    ListView getServicesOfServer(...) throws NacosException;
  • 获取所有订阅的服务

    List getSubscribeServices() throws NacosException;
  • 获取Nacos服务的状态

    String getServerStatus();
  • 主动关闭服务

    void shutDown() throws NacosException

在这些方法中提供了大量的重载方法,应用于不同场景和不同类型实例或服务的筛选,所以我们只需要在不同的情况下使用不同的方法即可。

NamingService的实例化是通过NamingFactory类和上面的Nacos服务信息,从代码中可以看出这里采用了反射机制来实例化NamingService,具体的实现类为NacosNamingService:

public static NamingService createNamingService(Properties properties) throws NacosException {    try { Class driverImplClass = Class.forName("com.alibaba.nacos.client.naming.NacosNamingService"); Constructor constructor = driverImplClass.getConstructor(Properties.class); return (NamingService) constructor.newInstance(properties);    } catch (Throwable e) { throw new NacosException(NacosException.CLIENT_INVALID_PARAM, e);    }}

NacosNamingService的实现

在示例代码中使用了NamingService的registerInstance方法来进行服务实例的注册,该方法接收两个参数,服务名称和实例对象。这个方法的最大作用是设置了当前实例的分组信息。在这里设置了默认的分组为“DEFAULT_GROUP”。至于分组等信息后续会进一步扩充。

@Overridepublic void registerInstance(String serviceName, Instance instance) throws NacosException {    registerInstance(serviceName, Constants.DEFAULT_GROUP, instance);}

紧接着调用的registerInstance方法如下,这个方法实现了两个功能:

第一,检查心跳时间设置的对不对(心跳默认为5秒)

第二,通过NamingClientProxy这个代理来执行服务注册操作

@Overridepublic void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {    NamingUtils.checkInstanceIsLegal(instance);//检查心跳    clientProxy.registerService(serviceName, groupName, instance);//通过代理执行服务注册操作}

通过clientProxy我们发现NamingClientProxy这个代理接口的具体实现是有NamingClientProxyDelegate来完成的,这个可以从NacosNamingService构造方法中来看出。

public NacosNamingService(Properties properties) throws NacosException {
init(properties);
}

初始化在init方法中

private void init(Properties properties) throws NacosException {    ValidatorUtils.checkInitParam(properties);    this.namespace = InitUtils.initNamespaceForNaming(properties);    InitUtils.initSerialization();    InitUtils.initWebRootContext(properties);    initLogName(properties);
this.changeNotifier = new InstancesChangeNotifier(); NotifyCenter.registerToPublisher(InstancesChangeEvent.class, 16384); NotifyCenter.registerSubscriber(changeNotifier); this.serviceInfoHolder = new ServiceInfoHolder(namespace, properties); this.clientProxy = new NamingClientProxyDelegate(this.namespace, serviceInfoHolder, properties, changeNotifier);//在这里进行了初始化,并看出使用的是NamingClientProxyDelegate来完成的}

NamingClientProxyDelegate中的实现

根据上方的分析和源码的阅读,我们可以发现NamingClientProxy调用registerService实际上调用的就是NamingClientProxyDelegate的对应方法:

@Overridepublic void registerService(String serviceName, String groupName, Instance instance) throws NacosException {    getExecuteClientProxy(instance).registerService(serviceName, groupName, instance);}

真正调用注册服务的并不是代理实现类,而是根据当前实例是否为瞬时对象,来选择对应的客户端代理来进行请求的:

如果当前实例为瞬时对象,则采用gRPC协议(NamingGrpcClientProxy)进行请求,否则采用http协议(NamingHttpClientProxy)进行请求。默认为瞬时对象,也就是说,2.0版本中默认采用了gRPC协议进行与Nacos服务进行交互。

private NamingClientProxy getExecuteClientProxy(Instance instance) {    return instance.isEphemeral() ? grpcClientProxy : httpClientProxy;}

NamingGrpcClientProxy中实现

关于gRPC协议(NamingGrpcClientProxy),我们后续在做展开,我们主要关注一下registerService方法实现,这里其实做了两件事情

  1. 缓存当前注册的实例信息用于恢复,缓存的数据结构为ConcurrentMap