• SpringCloud源码学习笔记3——Nacos服务注册源码分析


    系列文章目录和关于我

    一丶基本概念&Nacos架构#

    1.为什么需要注册中心#

    • 实现服务治理、服务动态扩容,以及调用时能有负载均衡的效果。

      如果我们将服务提供方的ip地址配置在服务消费方的配置文件中,当服务提供方实例上线下线,消费方都需要重启服务,导致二者耦合度过高。注册中心就是在二者之间加一层,实现解耦合。

      image-20230408103559163

    • 健康检查和服务摘除:主动的检查服务健康情况,对于宕机的服务将其摘除服务列表

    2.Nacos 的架构#

    nacos_arch.jpg

    • Naming Service :注册中心,提供服务注册,注销,管理
    • Config Service:配置中心,Nacos 配置中心为服务配置提供了编辑、存储、分发、变更管理、历史版本管理等功能,并且支持在实例运行中,更改配置。
    • OpenAPI:nacos对外暴露的接口,Provider App(服务提供者)就是调用这里的接口,实现将自己注册到nacos,Consumer App(服务消费者)也是使用这里的接口拉去配置中心中的服务提供者的信息。

    3.nacos数据模型#

    image-20230408120650010

    二丶nacos注册中心简单使用#

    我们使用nacos作为注册中心,只需要下载nacos提供的jar包并运行启动nacos服务,然后在服务提供者,消费者中引入spring-cloud-starter-alibaba-nacos-discovery,并配置spring.cloud.nacos.discovery.server-addr=nacos服务启动的地址,即可在nacos可视化界面看到:

    image-20230408155841722

    那么服务是如何注册到nacos的昵?

    三丶服务注册源码分析#

    当我们服务引入spring-cloud-starter-alibaba-nacos-discovery,便可以实现自动进行注册,这是因为在spring.facotries中自动装配了NacosServiceRegistryAutoConfiguration

    SpringBoot源码学习1——SpringBoot自动装配源码解析+Spring如何处理配置类的

    image-20230408160900201

    1.NacosServiceRegistryAutoConfiguration 引入了哪些类#

    点进NacosServiceRegistryAutoConfiguration 源码中,发现它注入了一下三个类

    1.1.NacosServiceRegistry#

    image-20230408161455326

    image-20230408161622634

    • ServiceInstance 表示的是服务发现中的一个实例

      这个接口定义了类似于getHost,getIp获取注册实例host,ip等方法,是springcloud定义的规范接口

    • Registration一个标记接口,ServiceRegistry这里面的R泛型就是Registration

      是springcloud定义的规范接口

    • ServiceRegistry 服务注册,定义如何向注册中心进行注册,和取消注册

      这个接口定义了register服务注册,deregister服务取消注册等方法,入参是Registration。它是springcloud定义的规范接口。

    spring cloud 定义了诸多规范接口,无论是服务注册,还是负载均衡,让其他中间件实现
    
    • NacosServiceRegistry nacos服务注册接口,实现了ServiceRegistry,定义了如何注册,如何取消注册,维护服务状态等。

    1.2.NacosRegistration#

    image-20230408162843995

    image-20230408162701079

    NacosRegistrationRegistration的实现类,象征着一个Nacos注册中心的服务,也就是我们自己写的springboot服务

    1.3.NacosAutoServiceRegistration#

    image-20230408163607142

    image-20230408163114101

    • AutoServiceRegistration一个标记接口,表示当前类是一个自动服务注册类
    • AbstractAutoServiceRegistration 实现了ApplicationListener,监听WebServerInitializedEvent web服务初始化结束事件,在ApplicationListener#onApplicationEvent中进行服务注册
    • NacosAutoServiceRegistration使用NacosServiceRegistryNacosRegistration的注册到nacos注册中心

    一通分析之后,可以看到NacosAutoServiceRegistration 是最核心的类,它负责监听事件,调用NacosServiceRegistry,将服务注册到注册中心。

    2.AbstractAutoServiceRegistration 监听事件进行注册#

    此类是SpringCloud提供的模板类,让市面上众多注册中心中间件实现它,快速接入SpringCloud生态。

    image-20230408164650713

    2.1 WebServerInitializedEvent 从何而来#

    AbstractAutoServiceRegistration想响应WebServerInitializedEvent ,那么WebServerInitializedEvent 是哪儿发出的昵?

    WebServerStartStopLifecycle#start方法

    image-20230408165846649

    image-20230408165805172

    WebServerStartStopLifecycle实现了Lifecycle,在spring容器刷新结束的时候,会使用LifecycleProcessor调用所以Lifecycle#start,从而发送ServletWebServerInitializedEvent(WebServerInitializedEvent子类)推送事件

    Reactive的springboot上下文则是由WebServerStartStopLifecycle推送ReactiveWebServerInitializedEvent事件,原理一样,如下图
    

    image-20230408171748437

    2.2 NacosAutoServiceRegistration如何进行服务注册#

    AbstractAutoServiceRegistration在响应事件后,会调用bind方法,进而调用register进行服务注册,这里就会调用到NacosAutoServiceRegistration#register

    image-20230408171934715

    那么到底如何进行服务注册?

    image-20230408172157911

    可以看到直接调用NacosServiceRegistry#register(NacosRegistration)进行服务注册

    3.NacosServiceRegistry 服务注册#

    image-20230408172550692

    可以看到这里使用NamingServiceInstance进行注册

    • NamingService,nacos框架中的类,负责服务注册和取消注册
    • Instance,nacos框架中的类,定义一个服务,记录ip,端口等信息
    可以看到nacos有自己一套东西,脱离springcloud,也可以使用,这就是松耦合
    

    下面我们看下NamingService是如何进行服务注册的image-20230408173117526

    • 如果是临时实例,会使用ScheduledThreadPoolExecutor,每5秒发送一次心跳,发送心跳即请求nacos注册中心/instance/beat接口

    • 然后调用NamingProxy 进行服务注册

      image-20230408173758235

      最终底层通过Http请求的方式,请求nacos服务的/nacos/v1/ns/instance

    image-20230408174719447

    4.nacos注册中心如何处理服务注册的请求#

    上面一通分析,我们直到了springboot服务是如何启动的时候,自动进行服务注册的,如何进行服务注册的,但是nacos服务端是如何响应注册请求的的昵

    image-20230408174916201

    • 从请求中拿实例信息

      image-20230408175100283

      主要包含上述这些字段。

    • ServiceManager#registerInstance

      服务注册的逻辑主要在addInstance方法中

      image-20230408180419438

      首先根据待注册服务的namespaceId命名中间idserviceName服务名称ephemeral是否临时服务构建出一个key,由于我们是一个临时实例,key最终为com.alibaba.nacos.naming.iplist.ephemeral + namespaceId ## + serviceName

      然后调用ConsistencyService一致性协议服务#put进行注册,这里和Nacos支持AP,CP架构有关,后续我们分析到一致性协议再补充。

      这里会调用到DelegateConsistencyServiceImpl(一致性协议门面)他会根据key中的是临时实例,还是非临时实例,选择协议,最终选择到DistroConsistencyServiceImpl,继续调用put方法

      image-20230408181110820

      image-20230408181215306

      可以看到DistroConsistencyServiceImpl(Distro一致性协议服务)会同步到nacos集群中的其他实例,这部分我们后续分析,我们重点看下onPut,看看nacos服务到底如何注册。

      image-20230408181806440

      image-20230408182847588

      至此服务注册请求结束了,只是将注册请求信息包装成了任务加入到Notifier的任务队列中。

    5.nacos 服务注册表结构#

    在看怎么处理阻塞队列中的任务前,我们看下nacos的注册表结构

    image-20230408190341273

    对应ServiceManager中的serviceMap属性

    /**
     * key 是命名空间
     * value 是 分组名称和Service服务的map
     *
     */
    private final Map<String, Map<String, Service>> serviceMap = new ConcurrentHashMap<>();
    
    //Service结构如下
    //集群和集群对象组成map
    private Map<String, Cluster> clusterMap = new HashMap<>();
    
    //Cluster 中的属性记录所有实例Instance的集合
    
    

    6.nacos服务注册异步任务队列处理注册任务#

    上面分析到最终服务注册请求被包装放到Notifier的任务队列中。我们看下任务队列的任务在哪里被拿出来消费。

    Notifier实现了Runnable,在DistroConsistencyServiceImpl中使用@PostConstruct将它提交到了调度线程池中。

    image-20230408183237936

    也就是说会有一个单线程调用Notifier#run

    image-20230408183406413

    image-20230408184903851

    后续会调用到Service#onChange,其updateIPs方法会更新实例的ip地址

    // 这里 instances 里面就包含了新实例对象
    // ephemeral 为 ture,临时实例
    public void updateIPs(Collection<Instance> instances, boolean ephemeral) {
    
        // clusterMap 对应集群的Map
        Map<String, List<Instance>> ipMap = new HashMap<>(clusterMap.size());
        // 把集群名字都放入到ipMap里面,value是一个空的ArrayList
        for (String clusterName : clusterMap.keySet()) {
            ipMap.put(clusterName, new ArrayList<>());
        }
    
        // 遍历全部的Instance,这个List 包含了之前已经注册过的实例,和新注册的实例对象
        // 这里的主要作用就是把相同集群下的 instance 进行分类
        for (Instance instance : instances) {
            try {
              
                // 判断客户端传过来的是 Instance 中,是否有设置 ClusterName
                if (StringUtils.isEmpty(instance.getClusterName())) {
                    // 如果没有,就给ClusterName赋值为 DEFAULT
                    instance.setClusterName(UtilsAndCommons.DEFAULT_CLUSTER_NAME);
                }
    
                // 判断之前是否存在对应的 ClusterName,如果没有则需要创建新的 Cluster 对象
                if (!clusterMap.containsKey(instance.getClusterName())) {
                    // 创建新的集群对象
                    Cluster cluster = new Cluster(instance.getClusterName(), this);
                    cluster.init();
                    // 放入到集群 clusterMap 当中
                    getClusterMap().put(instance.getClusterName(), cluster);
                }
    
                // 通过集群名字,从 ipMap 里面取
                List<Instance> clusterIPs = ipMap.get(instance.getClusterName());
                // 只有是新创建集群名字,这里才会为空,之前老的集群名字,在方法一开始里面都 value 赋值了 new ArrayList对象
                if (clusterIPs == null) {
                    clusterIPs = new LinkedList<>();
                    ipMap.put(instance.getClusterName(), clusterIPs);
                }
    
                // 把对应集群下的instance,添加进去
                clusterIPs.add(instance);
            } catch (Exception e) {
            }
        }
    
        // 分好类之后,针对每一个 ClusterName ,写入到注册表中
        for (Map.Entry<String, List<Instance>> entry : ipMap.entrySet()) {
            // entryIPs 已经是根据ClusterName分好组的实例列表
            List<Instance> entryIPs = entry.getValue();
            
            // 对每一个 Cluster 对象修改注册表  ->updateIps
            clusterMap.get(entry.getKey()).updateIps(entryIPs, ephemeral);
        }
    
    }
    

    针对每一个集群分别进行Cluster#updateIps

    public void updateIps(List<Instance> ips, boolean ephemeral) {
    
        // 先判断是否是临时实例
        // ephemeralInstances 临时实例
        // persistentInstances 持久化实例
        // 把对应数据先拿出来,放入到 新创建的 toUpdateInstances 集合中
        Set<Instance> toUpdateInstances = ephemeral ? ephemeralInstances : persistentInstances;
    
        // 先把老的实例列表复制一份 , 先复制一份新的
        //写时复制,先复制一份
        HashMap<String, Instance> oldIpMap = new HashMap<>(toUpdateInstances.size());
        for (Instance ip : toUpdateInstances) {
            oldIpMap.put(ip.getDatumKey(), ip);
        }
    
        //省略了同步到其他nacos服务的代码。。。
        
        // 最后把传入进来的实例列表,重新初始化一个 HaseSet,赋值给toUpdateInstances
        toUpdateInstances = new HashSet<>(ips);
        
        // 判断是否是临时实例
        if (ephemeral) {
            // 直接把之前的实例列表替换成新的
            ephemeralInstances = toUpdateInstances;
        } else {
            persistentInstances = toUpdateInstances;
        }
    }
    

    image-20230408182638959

  • 相关阅读:
    【300+精选大厂面试题持续分享】大数据运维尖刀面试题专栏(九)
    JavaScript入门⑦-DOM操作大全
    关于wukong-kong项目在树莓派启动后只运行一次卡死的问题的解决方法
    算法---矩阵中战斗力最弱的 K 行(Kotlin)
    【算法集训专题攻克篇】第二十五篇之树状数组
    排序算法-快速排序
    华为OD机试真题- 阿里巴巴找黄金宝箱(IV)-2023年OD统一考试(B卷)
    SSM 医院在线挂号系统
    肝了30天,终于整出这份Java面试九大核心专题,收割4个大厂offer
    解决springboot2.6和swagger冲突的问题
  • 原文地址:https://www.cnblogs.com/cuzzz/p/17299053.html