• 聊聊 Nacos


    在这里插入图片描述

    Nacos架构

    在这里插入图片描述

    • Provider APP:服务提供者
    • Consumer APP:服务消费者
    • Name Server:通过VIP(Virtual IP)或DNS的方式实现Nacos高可用集群的服务路由
    • Nacos Server:Nacos服务提供者,里面包含的Open API是功能访问入口,Conig Service、Naming Service 是Nacos提供的配置服务、命名服务模块。Consitency Protocol是一致性协议,用来实现Nacos集群节点的数据同步,这里使用的是Raft算法(Etcd、Redis哨兵选举)
    • Nacos Console:控制台

    一、注册中心的原理

    • 服务实例在启动时注册到服务注册表,并在关闭时注销
    • 服务消费者查询服务注册表,获得可用实例
    • 服务注册中心需要调用服务实例的健康检查API来验证它是否能够处理请求

    在这里插入图片描述

    二、SpringCloud完成注册的时机

    在Spring-Cloud-Common包中有一个类org.springframework.cloud. client.serviceregistry .ServiceRegistry ,它是Spring Cloud提供的服务注册的标准。集成到Spring Cloud中实现服务注册的组件,都会实现该接口。
    在这里插入图片描述
    该接口有一个实现类是NacoServiceRegistry

    SpringCloud集成Nacos的实现过程:

    在spring-clou-commons包的META-INF/spring.factories中包含自动装配的配置信息如下:
    在这里插入图片描述
    其中AutoServiceRegistrationAutoConfiguration就是服务注册相关的配置类:
    在这里插入图片描述
    在AutoServiceRegistrationAutoConfiguration配置类中,可以看到注入了一个AutoServiceRegistration实例,该类的关系图如下所示。
    在这里插入图片描述

    可以看出, AbstractAutoServiceRegistration抽象类实现了该接口,并且最重要的是NacosAutoServiceRegistration继承了AbstractAutoServiceRegistration。

    看到EventListener我们就应该知道,Nacos是通过Spring的事件机制继承到SpringCloud中去的。

    AbstractAutoServiceRegistration实现了onApplicationEvent抽象方法,并且监听WebServerInitializedEvent事件(当Webserver初始化完成之后) , 调用this.bind ( event )方法。

    在这里插入图片描述
    最终会调用NacosServiceREgistry.register()方法进行服务注册。
    在这里插入图片描述
    在这里插入图片描述

    三、NacosServiceRegistry的实现

    在NacosServiceRegistry.registry方法中,调用了Nacos Client SDK中的namingService.registerInstance完成服务的注册。
    在这里插入图片描述
    跟踪NacosNamingService的registerInstance()方法:
    在这里插入图片描述

    • 通过beatReactor.addBeatInfo()创建心跳信息实现健康检测, Nacos Server必须要确保注册的服务实例是健康的,而心跳检测就是服务健康检测的手段。

    • serverProxy.registerService()实现服务注册

    心跳机制:

    在这里插入图片描述

    从上述代码看,所谓心跳机制就是客户端通过schedule定时向服务端发送一个数据包 ,然后启动-个线程不断检测服务端的回应,如果在设定时间内没有收到服务端的回应,则认为服务器出现了故障。Nacos服务端会根据客户端的心跳包不断更新服务的状态。

    注册原理:

    Nacos提供了SDK和Open API两种形式来实现服务注册。

    Open API:

    curl -X POST 'http://127.0.0.1:8848/nacos/v1/ns/instance?serviceName=nacos.naming.serviceName&ip=20.18.7.11&port=8080'
    
    • 1

    SDK:
    在这里插入图片描述

    这两种形式本质都一样,底层都是基于HTTP协议完成请求的。所以注册服务就是发送一个HTTP请求:
    在这里插入图片描述

    对于nacos服务端,对外提供的服务接口请求地址为nacos/v1/ns/instance,实现代码咋nacos-naming模块下的InstanceController类中:
    在这里插入图片描述

    • 从请求参数汇总获得serviceName(服务名)和namespaceId(命名空间Id)
    • 调用registerInstance注册实例

    在这里插入图片描述

    • 创建一个控服务(在Nacos控制台“服务列表”中展示的服务信息),实际上是初始化一个serviceMap,它是一个ConcurrentHashMap集合
    • getService,从serviceMap中根据namespaceId和serviceName得到一个服务对象
    • 调用addInstance添加服务实例

    在这里插入图片描述
    在这里插入图片描述

    • 根据namespaceId、serviceName从缓存中获取Service实例
    • 如果Service实例为空,则创建并保存到缓存中

    在这里插入图片描述

    • 通过putService()方法将服务缓存到内存
    • service.init()建立心跳机制
    • consistencyService.listen实现数据一致性监听

    service.init ( ) 方法的如下图所示,它主要通过定时任务不断检测当前服务下所有实例最后发送心跳包的时间。如果超时,则设置healthy为false表示服务不健康,并且发送服务变更事件。在这里请大家思考一一个问题,服务实例的最后心跳包更新时间是谁来触发的?实际上前面有讲到, Nacos客户端注册服务的同时也建立了心跳机制。

    在这里插入图片描述

    putService方法,它的功能是将Service保存到serviceMap中:
    在这里插入图片描述

    继续调用addInstance方法把当前注册的服务实例保存到Service中:
    在这里插入图片描述

    总结:

    • Nacos客户端通过Open API的形式发送服务注册请求
    • Nacos服务端收到请求后,做以下三件事:
      1. 构建一个Service对象保存到ConcurrentHashMap集合中
      2. 使用定时任务对当前服务下的所有实例建立心跳检测机制
      3. 基于数据一致性协议服务数据进行同步

    服务提供者地址查询

    Open API:
    在这里插入图片描述

    SDK:
    在这里插入图片描述

    InstanceController中的list方法:
    在这里插入图片描述

    • 解析请求参数
    • 通过doSrvIPXT返回服务列表数据

    在这里插入图片描述
    在这里插入图片描述

    • 根据namespaceId、serviceName获得Service实例
    • 从Service实例中基于srvIPs得到所有服务提供者实例
    • 遍历组装JSON字符串并返回

    Nacos服务地址动态感知原理

    可以通过subscribe方法来实现监听,其中serviceName表示服务名、EventListener表示监听到的事件:
    在这里插入图片描述

    具体调用方式如下:
    在这里插入图片描述

    或者调用selectInstance方法,如果将subscribe属性设置为true,会自动注册监听:
    在这里插入图片描述

    在这里插入图片描述

    Nacos客户端中有一个HostReactor类,它的功能是实现服务的动态更新,基本原理是:

    • 客户端发起时间订阅后,在HostReactor中有一个UpdateTask线程,每10s发送一次Pull请求,获得服务端最新的地址列表

    • 对于服务端,它和服务提供者的实例之间维持了心跳检测,一旦服务提供者出现异常,则会发送一个Push消息给Nacos客户端,也就是服务端消费者

    • 服务消费者收到请求之后,使用HostReactor中提供的processServiceJSON解析消息,并更新本地服务地址列表

    四、集群环境:分布式的前提

    如果是 Nacos 集群环境,客户端会随机选择一个 Nacos 节点发起注册。

    搭建好一套Nacos 集群环境

    为了讲解客户端是如何注册到 Nacos 集群环境的底层原理,我在本地搭建了一个 Nacos 集群环境,有 3 个 Nacos 服务,它们的 IP 相同,端口号不同。

    192.168.10.197:8848
    192.168.10.197:8858
    192.168.10.197:8868
    
    • 1
    • 2
    • 3

    然后服务 A 和服务 B 都是配置了 Nacos 集群的 IP 和 端口号的,配置如下所示

    spring.cloud.nacos.discovery.server-addr
      =192.168.10.197:8848,192.168.10.197:8858,192.168.10.197:8868
    
    • 1
    • 2

    整体的结构如640 (1)下图所示,服务 A 和 服务 B 都往 Nacos 集群进行注册。

    但是里面有一个问题:服务 A 注册时,是向所有 Nacos 节点发起注册呢?还是只向其中一个节点发起注册?如果只向一个节点注册,要向哪个节点注册呢?

    答案:在 Client 发起注册之前,会有一个后台线程随机拿到 Nacos 集群服务列表中的一个地址。

    Nacos 为什么会这样设计?

    • 这其实就是一个负载均衡的思想在里面,每个节点都均匀的分摊请求。
    • 保证高可用,当某个节点宕机后,重新拿到其他的 Nacos 节点来建立连接。

    接下来我们看下服务 A 是怎么随机拿到一个 Nacos 节点的。

    随机节点

    我们来看下客户端是如何随机选择一个节点的,流程图如下:

    640 (2)

    那么如何找到这些代码逻辑呢?思路是怎么样的?

    我们之前讲过,RpcClient 会发起 request 请求,用的是和 Nacos 建立 currentConnection 连接来发起调用,代码如下:

    // 发起调用
    response = this.currentConnection.request(request, timeoutMills);
    
    • 1
    • 2

    这个 currentConnection 是客户端和 Nacos 集群中的某个节点建立的连接,我们找下它在哪里赋值的。代码如下:

    // 拿到 Nacos 节点信息
    serverInfo = recommendServer.get() == null ? nextRpcServer() : recommendServer.get();
    // 连接 Nacos 节点
    connectToServer = connectToServer(serverInfo);
    // 赋值 currentConnection
    this.currentConnection = connectToServer;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    而连接的信息是通过参数 serverInfo 传进去的,所以我们再看下 serverInfo 在哪里赋值的。

    这个 nextRpcServer() 方法里面会拿到一个随机的 Nacos 地址

    // 一个 int 随机数,范围 [0 ~ Nacos 个数)
    currentIndex.set(new Random().nextInt(serverList.size()));
    // index 自增 1
    int index = currentIndex.incrementAndGet() % getServerList().size();
    // 返回 Nacos 地址
    return getServerList().get(index);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    小结:客户端生成一个随机数,然后通过这个随机数从 Nacos 服务列表中拿到一个 Nacos 服务地址返回给客户端,然后客户端通过这个地址和 Nacos 服务建立连接。Nacos 服务列表中的节点都是平等的,随机拿到的任何一个节点都是可以用来发起调用的。

    五、路由转发

    5.1 发起和转发请求的流程

    为了演示发起注册的流程,我在这里模拟了一个注册请求。

    用的是 curl 命令,对 Nacos 节点(127.0.0.1:8848)发起注册请求:

    curl -X POST 'http://127.0.0.1:8848/nacos/v1/ns/instance?serviceName=nacos.naming.serviceName&ip=20.18.7.11&port=8080'
    
    • 1

    请求 URL:/nacos/v1/ns/instance

    请求参数

    • serviceName=nacos.naming.serviceName
    • ip=20.18.7.11
    • port=8080’

    之前我们讲到,Nacos 的有多个节点可以分别处理请求,当节点发现这个请求不是属于自己的,就会进行转发。

    如下图所示:

    服务 A 随机选择一个 Nacos 节点(图中为 Nacos1)发起注册请求,请求参数中包含了实例信息,Nacos 1 根据实例信息 hash + 取模拿到正确的节点,如果不属于自己,则将请求转发给其他节点(图中为 Nacos2)

    640 (3)

    步骤如下:

    • ① Nacos 节点从客户端发起的 request 中拿到客户端的实例信息生成 distroTag,如 IP + port 或 service name。
    • ② Nacos 根据 distroTag 生成 hash 值。
    • ③ 用 hash 值对 Nacos 节点数进行取余,拿到余数,比如 0、1、2、3。
    • ④ 根据余数从 Nacos 节点列表中拿到指定的节点地址。

    5.2 路由转发源码分析

    入口文件是 DistroFilter.java:

    naming/src/main/java/com/alibaba/nacos/naming/web/DistroFilter.java
    
    • 1

    请求会先到 DistroFilter 类的 doFilter() 方法,拿到正确的节点地址后,将请求转发出去。

    获取需要转发节点地址的代码如下:

    // 找到 Nacos 集群中的目标节点
    final String targetServer = distroMapper.mapSrv(distroTag);
    
    // mapSrv 方法会先 hash,然后再取模,responsibleTag的值类似这样:"20.18.7.11:8080"
    int index = distroHash(responsibleTag) % servers.size();
    
    // distroHash 方法里面会对 客户端的 ip+port 字符串或者服务名字符串 进行 hash
    Math.abs(responsibleTag.hashCode() % Integer.MAX_VALUE);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    不论是自己处理注册请求还是转发给其他节点来处理,都会把实例信息存储起来,那么是如何进行存储的?

    六、处理请求

    Nacos 目前有两个版本,v1 和 v2,如果是 v1,则是 instanceController 来处理注册请求,否则用 instanceControllerV2。本篇我们只讲解 v1 版本是怎么处理请求的。

    先上流程图:

    640 (5)

    添加实例信息的流程

    测试用的发起注册的命令:

    curl -X POST 'http://127.0.0.1:8858/nacos/v1/ns/instance?serviceName=nacos.naming.serviceName&ip=20.18.7.11&port=8080'
    
    • 1

    核心代码就是这个:

    图片服务端注册实例的方法

    首先有一个 synchronized 锁,然后执行 put 操作将临时的实例信息存放起来,所以重点看下 这个 consistencyService.put() 方法做了什么事情。

    先看下源码:

    onPut(key, value);
    // 开启 1s 的延迟任务,将数据同步给其他 Nacos 节点
    distroProtocol.sync(new      DistroKey(key,KeyBuilder.INSTANCE_LIST_KEY_PREFIX),DataOperation.CHANGE,
                    DistroConfig.getInstance().getSyncDelayMillis());
    
    • 1
    • 2
    • 3
    • 4

    这里面做了三件事情:

    • ① 将实例信息存放到内存缓存 ConcurrentHashMap 里面。
    • ② 添加一个任务到 BlockingQueue 队列里面,这个任务就是将最新的实例列表通过 UDP 的方式推送给所有客户端(服务实例),这样客户端就拿到了最新的服务实例列表。没想到吧,计算机网络的知识终于用上了~
    • ③ 开启 1s 的延迟任务,将数据通过给其他 Nacos 节点。

    注意:针对第二点和第三点,属于 Distro 一致性协议的一部分,里面的内容还比较多,我们放到下一讲专门来讲。

    一条注册请求的核心流程640 (4)

    参考

    blog.csdn.net/cold___play/article/details/108032204

  • 相关阅读:
    最好用的Boost.Asio:现代C++网络编程
    Python中aiohttp和aiofiles模块的安装
    2024年网络安全/黑客自学路线图
    测试开发技能实践-搭建ELK日志管理系统
    单源最短路径
    小侃设计模式(四)-原型模式
    方舟开服务器游戏基础管理设置
    计算机组成原理(二)
    数据结构之链表
    985、211毕业一多年,面试多次八家大厂,四面拿美团offer(Java后端)
  • 原文地址:https://blog.csdn.net/agonie201218/article/details/126124632