写点什么

Nacos 源码系列—关于服务注册的那些事

作者:牧小农
  • 2022 年 5 月 05 日
  • 本文字数:9889 字

    阅读完需:约 32 分钟

Nacos源码系列—关于服务注册的那些事

点赞再看,养成习惯,微信搜索【牧小农】关注我获取更多资讯,风里雨里,小农等你,很高兴能够成为你的朋友。项目源码地址:公众号回复 nacos,即可免费获取源码

简介

首先我们在看 Nacos 源码之前,要先想想为什么我们要读源码?是为了装杯?还是为了在心仪的女神面前给她娓娓道来展示自己的代码功底?当然不全是!


这都不是我们读源码的最终目的。作为一名技术人,上面的都是浮云,真正激励我们的应该是能够提升我们技术功底和整体技术大局观。此乃大道也!闲言少叙,接下来我们就来看一看,看源码究竟有什么好处


  • 提升技术功底: 当我们去看源码的时候,能够学习源码里面优秀的设计思想,还含有设计模式和并发编程技术,解决问题的思路,能够知其所以然

  • 新技术学习能力: 当我们看多了源码,对于一个新技术或者框架的掌握速度会有大幅度提升,能根据经验或官网资料快速掌握底层的实现,技术更新迭代也可以更快的入手

  • 快速解决问题能力: 遇到问题,尤其是框架源码的问题,能够更快速的定位

  • 面试获取更高成功率: 现在出去面试,一般中高级一点的,都会问到框架源码级别的实现,如果能够说出来,可以提升面试的成功率和薪资待遇,源码面试是区别程序员水平另一面镜子

  • 认识更多圈子: 多活跃开源社区,熟读源码后多思考,发现问题或需求主动参与开源技术研发,与圈内大牛成为为朋友

阅读源码的方法

  1. 搭建入门 demo: 我们可以先看一下官网提供的文档,搭建 Demo,快速掌握框架的基本使用

  2. 看核心代码: 对于初次看源码的同学,不要太过于关注源码的细枝末节,先把主要核心流程梳理出来,找到其入口,分析静态代码,如果遇到问题,可以进行断点调试。

  3. 绘图和笔记: 梳理好核心功能后,可以用流程图记录下来,好记性不如烂笔头,同时对关键的源码部分可以进行备注,分析参数的变化。同时要善于用 Debug,来观看源码的执行过程

  4. 复习总结: 当我们把框架的所有功能点的源码都分析完成后,回到主流程在梳理一遍,最后在自己脑袋中形成一个闭环,这样源码的核心内容和主流程就基本上理解了。


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 的源码下载下来,下载地址:https://github.com/alibaba/nacos



我们将源码下下来以后,导入到 idea 中


proto 编译


当我们导入成功以后,会出现程序包com.alibaba.nacos.consistency.entity不存在的错误提示,这是因为 Nacos 底层的数据通信会基于 protobuf 对数据做序列化和反序列化,需要先将 proto 文件编译为对应的 Java 代码。



最简单的 不安装任何的东西 idea2021.2 已经捆绑安装了这个。



可以通过mvn copmpile来在 target 自动生成他们。



Nacos 缺少 Istio 依赖问题解决


我们只需要在文件根目录下执行以下命令即可:


mvn clean package -Dmaven.test.skip=true -Dcheckstyle.skip=true
复制代码


做完以上两步,我们就可以启动 Nacos 的了


启动 Nacos


首先我们找到 nacos-console这个模块,这个就是我们的管理后台,找到它的启动类,因为 Nacos 默认为集群启动,所以我们要设置它为单机启动,方便演示


设置命令:


-Dnacos.standalone=true -Dnacos.home=E:\test\nacos



启动成功后,账号密码:nacos/nacos



到这里我们 Nacos 的源码启动就完成了。

开启源码

我们先从客户端服务的注册开始说起,我们可以先想一想如果 Nacos 客户端要注册,会把什么信息传递给服务器?这里我们可以看到在 nacos-client下的NamingTest有这么一些信息



@Ignorepublic class NamingTest { @Test public void testServiceList() throws Exception { //Nacos Server连接信息 Properties properties = new Properties(); //Nacos服务器地址 properties.put(PropertyKeyConst.SERVER_ADDR, "127.0.0.1:8848"); //连接Nacos服务的用户名 properties.put(PropertyKeyConst.USERNAME, "nacos"); //连接Nacos服务的密码 properties.put(PropertyKeyConst.PASSWORD, "nacos"); //实例信息 Instance instance = new Instance(); //实例IP,提供给消费者进行通信的地址 instance.setIp("1.1.1.1"); //端口,提供给消费者访问的端口 instance.setPort(800); //权重,当前实例的权限,浮点类型(默认1.0D) instance.setWeight(2); Map<String, String> map = new HashMap<String, String>(); map.put("netType", "external"); map.put("version", "2.0"); instance.setMetadata(map);
//关键代码 创建自己的实例 NamingService namingService = NacosFactory.createNamingService(properties); namingService.registerInstance("nacos.test.1", instance); ThreadUtils.sleep(5000L); List<Instance> list = namingService.getAllInstances("nacos.test.1"); System.out.println(list); ThreadUtils.sleep(30000L); // ExpressionSelector expressionSelector = new ExpressionSelector(); // expressionSelector.setExpression("INSTANCE.metadata.registerSource = 'dubbo'"); // ListView<String> serviceList = namingService.getServicesOfServer(1, 10, expressionSelector); }}
复制代码


上面就是客户端注册的一个测试类,模仿了真实的服务注册到 Nacos 的过程,包括 NacosServer 连接、实例的创建、实例属性的赋值、注册实例,所以在这个其中包含了服务注册的核心代码,从这里我们可以大致看出,它包含了两个类的信息: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:实例的服务信息;


元数据:


元数据类型为 HashMap,从当前 Demo 我们能够看到的数据只有两个


  • netType:网络类型,这里设置的值为 external(外网)

  • version Nacos 版本,这里为 2.0


除此之外,我们在Instance类中还可以看到一些默认信息,这些方法都是通过 get 方法进行提供的


  //心跳间隙的key,默认为5s,也就是默认5秒进行一次心跳    public long getInstanceHeartBeatInterval() {        return getMetaDataByKeyWithDefault(PreservedMetadataKeys.HEART_BEAT_INTERVAL,                Constants.DEFAULT_HEART_BEAT_INTERVAL);    }
//心跳超时的key,默认为15s,也就是默认15秒收不到心跳,实例将会标记为不健康; public long getInstanceHeartBeatTimeOut() { return getMetaDataByKeyWithDefault(PreservedMetadataKeys.HEART_BEAT_TIMEOUT, Constants.DEFAULT_HEART_BEAT_TIMEOUT); }
//实例IP被删除的key,默认为30s,也就是30秒收不到心跳,实例将会被移除; public long getIpDeleteTimeout() { return getMetaDataByKeyWithDefault(PreservedMetadataKeys.IP_DELETE_TIMEOUT, Constants.DEFAULT_IP_DELETE_TIMEOUT); }
//实例ID生成器key,默认为simple; public String getInstanceIdGenerator() { return getMetaDataByKeyWithDefault(PreservedMetadataKeys.INSTANCE_ID_GENERATOR, Constants.DEFAULT_INSTANCE_ID_GENERATOR); }
复制代码


为什么要说这个呢?从这些参数中我们就可以了解到,我们服务的心跳间隙是多少以及超时时间,传递什么参数配置什么参数,以此来了解我们的实例是否健康。同时我们也可以看到一个比较关键且核心的类,是真正创建实例的类 ——NamingService


NamingService


NamingService 是 Nacos 对外提供的一个统一的接口,当我们点进去查看,可以看到大概一下几个方法,这些方法提供了不同的重载方法,方便我们用于不同的场景。



//服务实例注册void registerInstance(...) throws NacosException;
//服务实例注销void deregisterInstance(...) throws NacosException;
//获取服务实例列表List<Instance> getAllInstances(...) throws NacosException;
//查询健康服务实例List<Instance> selectInstances(...) throws NacosException;
//查询集群中健康的服务实例List<Instance> selectInstances(....List<String> clusters....)throws NacosException;
//使用负载均衡策略选择一个健康的服务实例Instance selectOneHealthyInstance(...) throws NacosException;
//订阅服务事件void subscribe(...) throws NacosException;
//取消订阅服务事件void unsubscribe(...) throws NacosException;
//获取所有(或指定)服务名称ListView<String> getServicesOfServer(...) throws NacosException;
//获取所有订阅的服务List<ServiceInfo> getSubscribeServices() throws NacosException; //获取Nacos服务的状态String getServerStatus(); //主动关闭服务void shutDown() throws NacosException;
复制代码


NamingService 的实例化是通过NacosFactory.createNamingService(properties);实现的,内部源码是通过反射来实现实例化过程


 NamingService namingService = NacosFactory.createNamingService(properties); 
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); } }
复制代码


接下来我们就来看一看NamingService的具体实现


//调用registerInstance方法namingService.registerInstance("nacos.test.1", instance);
复制代码


 @Override    public void registerInstance(String serviceName, Instance instance) throws NacosException {        //默认的分组为“DEFAULT_GROUP”         registerInstance(serviceName, Constants.DEFAULT_GROUP, instance);    }
复制代码


 @Override    public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {        //检查心跳时间是否正常        NamingUtils.checkInstanceIsLegal(instance);        //通过代理注册服务        clientProxy.registerService(serviceName, groupName, instance);    }
复制代码


心跳检测代码


   //心跳间隙超过限制 返回错误        if (instance.getInstanceHeartBeatTimeOut() < instance.getInstanceHeartBeatInterval()                || instance.getIpDeleteTimeout() < instance.getInstanceHeartBeatInterval()) {            throw new NacosException(NacosException.INVALID_PARAM,                    "Instance 'heart beat interval' must less than 'heart beat timeout' and 'ip delete timeout'.");        }
复制代码


通过代理注册服务,我们了解到clientProxy代理接口是通过NamingClientProxyDelegate来完成,我们可以在 init 构造方法中得出,具体的实例对象


  private void init(Properties properties) throws NacosException {        //使用NamingClientProxyDelegate来完成         this.clientProxy = new NamingClientProxyDelegate(this.namespace, serviceInfoHolder, properties, changeNotifier);    }
复制代码



NamingClientProxyDelegate 实现NamingClientProxyDelegate中,真正调用注册服务的并不是代理实现类,而且先判断当前实例是否为瞬时对象后,来选择对应的客户端代理来进行请求。


  @Override    public 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 中的实现


    @Override    public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {        NAMING_LOGGER.info("[REGISTER-SERVICE] {} registering service {} with instance {}", namespaceId, serviceName,                instance);        //数据的缓存        redoService.cacheInstanceForRedo(serviceName, groupName, instance);        //gRPC进行服务调用        doRegisterService(serviceName, groupName, instance);    }
复制代码


大体关系图如下所示:


Nacos 客户端在项目的应用

  1. 我们想要让某一个服务注册到 Nacos 中,首先要引入一个依赖:


        <dependency>            <groupId>com.alibaba.cloud</groupId>            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>        </dependency>
复制代码


  1. 在依赖中,去查看 SpringBoot 自动装配文件自动装配文件META-INF/spring.factories



  1. 通过 SpringBoot 的自动装配来加载EnableAutoConfiguration对应的类,这里我们可以看到很多有关于 Nacos 相关的类,怎么知道哪个是我们真正需要关心的类,服务在注册的时候走的是哪个,一般自动装配,我们都会找到带有“Auto”关键字的文件进行查看,然后在结合我们需要找的,我们是客户端注册服务,所以我们大体可以定位到NacosServiceRegistryAutoConfiguration这个文件



  1. 查看NacosServiceRegistryAutoConfiguration源码,在这里我们只需要关注最核心的nacosAutoServiceRegistration方法




而我们真正关心的只有三个类NacosAutoServiceRegistration类是注册的核心,我们来看一下它的继承关系



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


  1. 从上述内容中我们可以知道,Nacos 服务自动注册是从NacosServiceRegistryAutoConfiguration类开始的,并自动注册到NacosAutoServiceRegistration类中。


在下图中我们可以看到,主要是调用了 super 方法,所以我们继续查看该类的构造方法:AbstractAutoServiceRegistration


public class NacosAutoServiceRegistration    extends AbstractAutoServiceRegistration<Registration> {
public NacosAutoServiceRegistration(ServiceRegistry<Registration> serviceRegistry, AutoServiceRegistrationProperties autoServiceRegistrationProperties, NacosRegistration registration) { super(serviceRegistry, autoServiceRegistrationProperties); this.registration = registration; }
}
复制代码


AbstractAutoServiceRegistration实现了ApplicationListener接口,用来监听 Spring 容器启动过程中WebServerInitializedEvent事件,一般如果我们实现这个类的时候,会实现一个方法onApplicationEvent(),这个方法会在我们项目启动的时候触发



  @Override  @SuppressWarnings("deprecation")  public void onApplicationEvent(WebServerInitializedEvent event) {    bind(event);  }
复制代码


由此我们可以看到 bind 里面的这个方法


  @Deprecated  public void bind(WebServerInitializedEvent event) {  //获取 ApplicationContext对象    ApplicationContext context = event.getApplicationContext();    //判断服务的 Namespace    if (context instanceof ConfigurableWebServerApplicationContext) {      if ("management".equals(((ConfigurableWebServerApplicationContext) context).getServerNamespace())) {        return;      }    }    //记录当前服务的端口    this.port.compareAndSet(0, event.getWebServer().getPort());    //【核心】启动注册流程    this.start();  }
复制代码


start()方法调用register();方法来注册服务


  public void start() {    if (!isEnabled()) {      if (logger.isDebugEnabled()) {        logger.debug("Discovery Lifecycle disabled. Not starting");      }      return;    }
// only initialize if nonSecurePort is greater than 0 and it isn't already running // because of containerPortInitializer below //如果服务是没有运行状态时,进行初始化 if (!this.running.get()) { //发布服务开始注册事件 this.context.publishEvent(new InstancePreRegisteredEvent(this, getRegistration())); //【核心】注册服务 register(); if (shouldRegisterManagement()) { registerManagement(); } //发布注册完成事件 this.context.publishEvent(new InstanceRegisteredEvent<>(this, getConfiguration())); //服务状态设置为运行状态 this.running.compareAndSet(false, true); }
}
复制代码


NacosServiceRegistry.register()方法,如下所示:


@Override  public void register(Registration registration) { //判断ServiceId是否为空    if (StringUtils.isEmpty(registration.getServiceId())) {      log.warn("No service to register for nacos client...");      return;    } //获取Nacos的服务信息    NamingService namingService = namingService();  //获取服务ID和分组    String serviceId = registration.getServiceId();    String group = nacosDiscoveryProperties.getGroup();    //构建instance实例(IP/Port/Weight/clusterName.....)    Instance instance = getNacosInstanceFromRegistration(registration);
try { //向服务端注册此服务 namingService.registerInstance(serviceId, group, instance); log.info("nacos registry, {} {} {}:{} register finished", group, serviceId, instance.getIp(), instance.getPort()); } catch (Exception e) { if (nacosDiscoveryProperties.isFailFast()) { log.error("nacos registry, {} register failed...{},", serviceId, registration.toString(), e); rethrowRuntimeException(e); } else { log.warn("Failfast is false. {} register failed...{},", serviceId, registration.toString(), e); } } }
复制代码


NacosNamingService.registerInstance()方法,如下:


    @Override    public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {        //检查超时参数是否异常,心跳超时时间(15s)必须大于心跳间隙(5s)        NamingUtils.checkInstanceIsLegal(instance);        //拼接服务名,格式:groupName@@serviceName        String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);        //判断是否为临时实例,默认为true        if (instance.isEphemeral()) {            //临时实例,定时向Nacos服务发送心跳            BeatInfo beatInfo = beatReactor.buildBeatInfo(groupedServiceName, instance);            beatReactor.addBeatInfo(groupedServiceName, beatInfo);        }        //【核心】发送注册服务实例请求        serverProxy.registerService(groupedServiceName, groupName, instance);    }
复制代码


registerService中我们可以看到 Nacos 服务注册接口需要的完整参数


    public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {                NAMING_LOGGER.info("[REGISTER-SERVICE] {} registering service {} with instance: {}", namespaceId, serviceName,                instance);                final Map<String, String> params = new HashMap<String, String>(16);        //环境        params.put(CommonParams.NAMESPACE_ID, namespaceId);        //服务名称        params.put(CommonParams.SERVICE_NAME, serviceName);        //分组名称        params.put(CommonParams.GROUP_NAME, groupName);        //集群名称        params.put(CommonParams.CLUSTER_NAME, instance.getClusterName());        //当前实例IP        params.put("ip", instance.getIp());        //当前实例端口        params.put("port", String.valueOf(instance.getPort()));        //权重        params.put("weight", String.valueOf(instance.getWeight()));        params.put("enable", String.valueOf(instance.isEnabled()));        params.put("healthy", String.valueOf(instance.isHealthy()));        params.put("ephemeral", String.valueOf(instance.isEphemeral()));        params.put("metadata", JacksonUtils.toJson(instance.getMetadata()));                reqApi(UtilAndComs.nacosUrlInstance, params, HttpMethod.POST);            }
复制代码

补充

在这里我们会发现我们请求实例接口的地址为/nacos/v1/ns/instance,其实这个在官网中也有提供对应的地址给我们,并且是对应的




客户端注册流程图

总结

以上就是 Nacos 的客户端注册流程,阅读源码并没有我们想象中的那么难,道阻且长,行之将至,当你开始行动的时候,你就已经开始进步了,别管学多少,如果您对文中有疑问或者问题,欢迎在下方留言,小农看见了会第一时间回复,大家加油~

发布于: 刚刚阅读数: 3
用户头像

牧小农

关注

业精于勤荒于嬉,行成于思毁于随。 2019.02.13 加入

公众号【牧小农】

评论

发布
暂无评论
Nacos源码系列—关于服务注册的那些事_源码_牧小农_InfoQ写作社区