1. jar包指定配置文件

    –spring.config.location=/ect/server/application.yml

    优先级, --spring.config.location大于jar包中的配置

  2. spring boot 的项目

  3. 通信都是使用Rest API通信

  4. 注册的单位是实例, 最小单元是实例

  5. 单集群的nacos 的功能

    • 服务发现
    • 心跳检测
    • 服务注册
  6. 实例: isemphere: true /false

    • 临时实例 :AP架构(注册中心)
    • 永久实例 :CP架构(配置中心)
  7. 服务注册的请求方式:

    • 1.x版本使用http / https请求
    • 2.x版本及以后: grpc请求
  8. AutoConfigreAfter ( {xxxx.class ,xxx.class}) 在在这写配置加载之后再加载 顺序

  9. 实例注册请求发送时机:

    Spring 监听器ApplicationListener 监听WenInit 初始化完成后 发送请求;

  10. spring boot web 容器

    • tomcat : 默认
    • undertow
    • jetty
  11. nacos注册表结构

  • Nacos 注册表结构的核心是一个双层 Map 结构,具体如下:
    • 最外层 Map:

      • Key: nameSpace (命名空间)
      • Value: 一个 Map
    • 第二层 Map: ConcurrentHashMap

      • Key: ServiceName (组::服务名称)
      • Value: Service 对象,该对象包含了一些关键属性。
    • Service 对象的属性:

      • clusterMap: 一个 Map,用于管理不同的集群
        • Key: 集群名称 (clusterName)
        • Value: 一个 Set,包含集群内的 Instance 对象集合
    • Instance 对象:

      • 表示具体的服务实例,包含实例的 IP 地址、端口、权重、健康状态等信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
+----------------------------------------------------------+
| NameSpace |
| (最外层 Map) |
| +-----------------------------------------------------+ |
| | groupName::ServiceName (组::服务名称) | |
| | +-------------------------------------------------+ | |
| | | clusterMap (集群管理) | | |
| | | +---------------------------------------------+ | | |
| | | | clusterName (集群名称) | | | |
| | | | +-----------------------------------------+ | | | |
| | | | | InstanceSet (实例集合) | | | | |
| | | | | +-----------------------------------+ | | | | |
| | | | | | Instance (实例) | | | | | |
| | | | | | - IP 地址 | | | | | |
| | | | | | - 端口 | | | | | |
| | | | | | - 权重 | | | | | |
| | | | | | - 健康状态 | | | | | |
| | | | | +-----------------------------------+ | | | | |
| | | | +-----------------------------------------+ | | | |
| | | +---------------------------------------------+ | | |
| | +-------------------------------------------------+ | |
| +-----------------------------------------------------+ |
+----------------------------------------------------------+

一 Nacos 的核心功能

通信机制

    1. 1.x版本中, Nacos 的服务端和客户端之间的通信是通过Restful风格的请求通信的
    2. 2.x版本之后, 改用了grpc的方式

服务注册

naocs 服务客户端,在启动时, 会根据配置文件中的nacos 的服务端配置信息, 向服务端发送一个请求, 将客户端的元数据发送给服务端, 比如: ip \ port \ 服务名 等信息.

服务端接收到数据后, 会将客户端的元数据存储在一个双层map中

服务注册的时间: 在客户端启动时, nacos客户端使用了spring的监听器ApplicaitonListener,监听WebServerInitializedEvent 是事件, 当web服务器启动后, 发送一个请求给服务端注册服务.

发送的请求地址 : Http: nacosip:nacos端口号/nacos/v1/ns/instance 发送一个Post请求, 带上请求头和请求参数 : 元数据 \ Ip \ 端口 \ 服务名 \ namspace 等

服务心跳检测

在客服端服务注册成功之后, Nacos 的客户端会维护一个定时任务 , 来持续的发送请求给服务端 ,告诉服务端自己还活着 ,默认是每5秒发送一次, 防止服务在注册中心被剔除

服务的健康检查

Nacos的服务端也会维护一个定时任务, 通过判断当前时间和实例最后一次的检测时间, 如果超过了15秒, 就将该实例的健康状态改为false ,默认是true, 如果超过了30 秒, 则直接将该服务从nacos中剔除

服务发现

服务端在注册到nacos后, 在调用服务注册中心的服务时, 会发送请求给服务端, 拉取服务端中所注册的服务清单,并且缓存在客户端本地,同时会开启一个定时任务,定时从服务端拉取最新服务清单

所以在第一次调用时, 速度会相对慢有点 , 后续主要通过查找缓存中的服务列表来调用, 靠定时任务来定时更新缓存中的服务列表

Nacos服务注册表结构

  • 双Map结构:HashMap<String,CurrentHashMap<String,Service>>

    • 第一层是一个HashMap ,Key 是 nameSpace, Value 是一个CurrentHashMap<Service,>

    • 第二层的CurrentHashMap的 Key 是 GroupName::ServiceName, Value 是 Service 对象

      Group本身没有单独的一个层级, 通常是直接携带在ServiceName后面,在查找时就可以判断属于哪个组了

    • Service对象: Service 对象中又有一个custerMap 集群map

    • custerMap的 Key 是custerName , Value 是一个Cluster 对象

    • 在Cluster 对象中, 存在一个set集合, 里面存放的元素就是实例Instance对象 , Instance 对象中的属性存放着客户端传的元数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
+----------------------------------------------------------+
| NameSpace |
| (最外层 Map) |
| +-----------------------------------------------------+ |
| | GroupName::ServiceName (服务名称) | |
| | +-------------------------------------------------+ | |
| | | clusterMap (集群管理) | | |
| | | +---------------------------------------------+ | | |
| | | | clusterName (集群名称) | | | |
| | | | +-----------------------------------------+ | | | |
| | | | | InstanceSet (实例集合) | | | | |
| | | | | +-----------------------------------+ | | | | |
| | | | | | Instance (实例) | | | | | |
| | | | | | - IP 地址 | | | | | |
| | | | | | - 端口 | | | | | |
| | | | | | - 权重 | | | | | |
| | | | | | - 健康状态 | | | | | |
| | | | | +-----------------------------------+ | | | | |
| | | | +-----------------------------------------+ | | | |
| | | +---------------------------------------------+ | | |
| | +-------------------------------------------------+ | |
| +-----------------------------------------------------+ |
+----------------------------------------------------------+

二 OpenFeign + Nacos 调用流程

  1. Nacos客户端在启动时,根据服务注册机制将服务注册到Nacos注册中心.
  2. nacos客户端会通过心跳检测机制定时在Nacos的服务端更新当前实例的健康状态, 防止被Nacos服务端剔除, Nacos服务端则根据当前时间和实例最后一次的报告时间做对比, 如果超过了15秒,则将该实例的健康状态标记为false, 如果超过了30 秒, 则直接将该服务剔除掉
  3. nacos客户端通过服务发现机制, 拉取Nacos服务端中的服务列表, 然后根据Ribbon的负载均衡机制, 选择一个服务的ip和端口号进调用

三 Nacos服务端具体处理注册流程

1. 功能 :

Nacos在进行服务注册时, 主要的工作就是将实例添加到注册表中对应服务的对应Cluster下的set中, 里面存放着当前服务下所有集群下的所有服务

2. **主要实现: **

思想: Nacos在进行处理时 , 采用同步加异步思想,

2.1 在同步中做的事

  1. 解析客户端注册服务时发送的请求中的数据

  2. 如果该服务是第一次注册, 首先会先创建一个空的Service对象

    1
    2
    createEmptyService(namespaceId, serviceName, instance.isEphemeral());
    Service service = getService(namespaceId, serviceName);
  3. 构建一个双层Map结构, 将Service放进去

  4. 初始化该服务

    在初始化该服务时,

    1. 使用定时任务的线程池开启了一个定时任务, 对实例做心跳检测,

      心跳检测: 通过判断实例的最后一次的报告时间 和当前时间的差值, 如果大于15秒 则将该实例的健康状态改为false. 如果不大于 ,则不变为true ,

      如果大于 30 秒, 则将该实例从服务中移除 , 如果不大于就不移除

  5. 做完这些后, 将该实例对象 存放在一个临时的Map中.

  6. 最后再将实例对象从临时map取出, 添加到阻塞队列中, task.add(Pair.with)

    该阻塞队列为LinedBlockingQueue阻塞队列, 默认大小为1024 * 1024

2.2 异步线程做的事(notifiyer)

  1. 异步线程notiyer 从阻塞队列出Pair对象, 使用Task.take()获取, 并将该实例存放到Cluster对象中 ,
  2. 更新Cluster对象中的数据的操作
  3. 进行各种实例的CRUD比对
  4. 得到最终要注册的实例对象
  5. 最后将该实例存放在在注册表中的该service下的Cluster 对象中set集合中

这样, 实例就相当于存放在了注册表中. 并且这个线程是在服务启动时一种存在的, Nacos使用的@PostConstrct注解和@Service 来让该线程在服务启动时就运行 .

对于这种需要一直执行的任务, 既需要让这个任务一直执行下去, 又要防止 这个任务大量的消耗CPU资源

我在Nacos中就学到了一种很好的解决办法, 它首先通过在该任务中加入while (true) 循环, 来确保该线程可以一直执行下去, 并且,为了防止该线程一直循环消耗资源, 所以Nacos在这里它使用的是一个阻塞队列, 当该任务 获取不实例对象时,会进入阻塞状态, 只有当新的实例对象被添加进来, 该线程才会继续执行. 这样就很好的解决了消耗CPU资源的问题. 然后,while 循环其实并不能百分百保证该线程能一直循环下去, 当该任务在执行的过程中发生了异常, 该任务也会停止 , 因此, 在这个任务的while循环中,还通过try-catch包围, 将异常直接处理, 这样, 就算发生了异常, 该任务也不会停止了.

注册表实列的多线程读取的问题和解决办法

❔问题

在对注册表中的实例的操作, 在Nacos中主要就两种,存和取, 这样就有一个问题, 如果有多个线程对该set集合进行操作, 一个线程读数据, 另外一个线程来存数据,

  1. 当写线程还没有写进去, 读线程就读完了, 就会造成数据不一致
  2. 在存的过程中,如果其他线程同时取, 那么读取到的数据就可能是错误的一个数据, 读写冲突问题

💡解决办法

对于这种多线程同时存储的问题, 一般的解决办法就是加锁 , 但是加锁的弊端太大了, 虽然加锁能很好的解决读写冲突问题数据不一致的问题. 但是,在Nacos源码中, 它并没有去使用加锁, 而是采用了写时复制的解决办法. 因为在Nacos的官网中也介绍了,Nacos 的TPS能达到13000 ,如果使用锁, 那么对于并发的性能就会很差, TPS很难达到13000, 采用写时复制, 虽然还是存在数据一致性问题, 但是确保了最终一致性, 而且还拥有很好的并发性能.

写时复制:

原理:

就是在一个容器中存在两种操作, 写和读时, 当只有读时 , 所有的读线程都是操作的同一个容器,

但是,如果是写操作, 它不会去操作原容器, 而是先将原容器中复制一份, 操作那个复制出的新容器, 当写操作完成后, 再将旧的容器替换成新的容器.

这样,就能很好的解决并发读写问题

但是:

虽然写时复制虽然读与读, 读与写之间没有冲突, 但是写于写之间还是会存在并发的冲突, 如果存在并发的环境, 还是得通过加锁来解决. 不过,在Nacos的服务注册中并不存在并发的写, 因为该任务只有一个线程, 并且是通过拿取阻塞队列中的数据来一个个写的.谁先存谁就先写

❓阻塞队列对堆积吗?

一般情况下不会堆积, Nacos本身处理这个逻辑就很快, 但是也可能发生人为的攻击 ,

程序员通过编写脚本, 一次性发送大量的注册请求, 除此之外,不会出现堆积问题