二次阅读Nacos源码——Nacos自动服务注册架构设计原理

文章目录

  • 1 问题背景
  • 2 前言(强烈推荐看此小节)
  • 3 自动服务注册总体设计
  • 4 哪里体现自动?
  • 5 客户端服务注册
  • 6 服务端服务注册
    • 6.1 serviceManager.registerInstance()
    • 6.2 consistencyService.put()
    • 6.3 DistroProtocol实例化
  • 7 Distro一致性算法

1 问题背景

第一次阅读Nacos源码是从源码启动单机运行nacos开始。时隔1年再次Nacos源码,会有更深刻的感受。写下此文章前,笔者阅读了Nacos官方发布的《Nacos 架构与原理》。

2 前言(强烈推荐看此小节)

  1. 如从未阅读过Nacos源码,推荐从源码启动单机运行nacos开始阅读,该文章所在的专栏也会进一步介绍。
  2. 如果阅读过Nacos源码,推荐再阅读Nacos官方发布的《Nacos 架构与原理》,可以从架构层面认识Nacos是怎样设计的。
  3. 本博客主要记录笔者二次阅读研究到的东西,或者说是理顺了某些逻辑。因此部分地方可能写得比较简略,部分地方写得比较详细。本博客在行文构思处还有待提高,看不懂本文或有疑惑的小伙伴可留言。
  4. 笔者以思维导图的方式从宏观角度归纳总结了关于Nacos服务注册Distro协议的设计机制思维导图,如果看不懂则可以先看Nacos辅服务注册总结思维导图。强烈建议先看前面2篇的思维导图,再回来看博客的源码级别讲解。

3 自动服务注册总体设计

  • 参与的角色:Nacos客户端(即服务提供者)、Nacos服务端(即Nacos集群)
  • 客户端的核心操作:服务注册、心跳上报
  • 服务端的核心操作:服务注册、健康检查
  • 路由选择:客户端随机选择服务端发送请求,服务端接收到“写”请求(服务注册、心跳上报)会判断自己是否负责处理该写请求
  • 服务端集群的一致性算法:Distro算法(网上很多人说Raft算法,但没说场景。Nacos确实有用Raft算法,但是在服务注册方面,临时服务基本都是采用Distro算法,Distro算法遵循AP,最终一致性,非强一致性)

4 哪里体现自动?

客户端启动后,会发布一个WebServerInitializedEvent。spring cloud的服务发现组件AbstractAutoServiceRegistration会监听该事件,底层会调用将客户端注册到Nacos服务端的逻辑。

5 客户端服务注册

底层实际是封装好一个客户端实例的数据发送HTTP请求给Nacos服务端

6 服务端服务注册

服务端处理服务注册的核心入口在com.alibaba.nacos.naming.controllers.InstanceController#register,其实还有其他入口,比如服务端集群做一致性,此处仅从最平常的入口切入研究

  1. parseInstance():从请求参数中解析出一个实例instance,其中有一个操作是instance.setLastBeat(System.currentTimeMillis());,服务端做健康检查的时候,会用当前时间戳-instance.getLastBeat判断实例是否健康
  2. serviceManager.registerInstance():注册服务的核心操作。ServiceManager更加关心Service服务层面,比如注册服务、注销服务;Service更加关心 Instance实例层面,比如更新实例。

6.1 serviceManager.registerInstance()

  1. createEmptyService():如果当前Nacos服务端没有当前要注册的服务,则创建一个服务。存储服务的容器是Map> serviceMap,他的key-value分别是Map(namespace, Map(groupName@@serviceName, Service)),第一个key对应的value是ConcurrentSkipListMap类型。
    1.1 创建完服务后会Service服务放入serviceMap容器
    1.2 健康检查 。启动一个带延迟事件的调度循环执行(线程池的应用),每5000毫秒检查客户端有无上报心跳。超过15秒没有上报,则标记客户端为不健康,再过15秒(即超过30秒)没有上报,则剔除该客户端。
    1.3 服务端一致性监听。把Service放入Map> listeners监听器。此处放了2次,临时实例、永久实例各放了一次,由key做区分。此处监听器涉及了观察者设计模式,降低一致性算法的耦合性,将服务端做一致性协议的能力下沉到Nacos内核模块,更加通用,更加低耦合。前面提到服务注册的入口不只在controller有,服务端集群做一致性的地方也会有,这就是一致性算法下沉到内核模块的作用。

  2. addInstance():添加实例到服务里面。
    2.1 KeyBuilder.buildInstanceListKey:构建一个Service的全局唯一标识,基本上所有地方都是靠这个key做逻辑处理。
    2.2 addIpAddresses():拿到Service下的所有实例,初始化clusterMap
    2.3 consistencyService.put()处理数据一致性。这个很核心,下面单独拿出来讲。

6.2 consistencyService.put()

put()方法有2个核心操作,一个是onPut(),更新自身服务端节点的实例数据,推送实例数据给Nacos客户端们。一个是distroProtocol.sync(),做Nacos服务端集群的数据一致性。

  1. onPut():有两个核心操作(1)更新当前Nacos服务端的Cluster中的临时实例Set集合的数据;(2)Service服务最新的Instances实例数据推送给订阅了该服务的Nacos客户端
    1.1填充datastore
    1.2notifier.addTask(key, DataOperation.CHANGE)发布change事件。notifier是一个Runable实现类,该类是DistroConsistencyServiceImpl的内部类,而DistroConsistencyServiceImpl有一个带有@PostConstruct注解的方法,说明Nacos服务端启动完成后,会回调该注解标注的方法,该方法会用线程池执行notifier任务。notifier任务实际就是调用监听器listener的onChange方法listener.onChange()onChange()方法实际上是将实例添加到Cluster中的Set ephemeralInstances临时实例Set集合。
    1.3 getPushService().serviceChanged(this)广播Service 被改变的消息,实际是封装UDP报文,发给订阅了该服务的Nacos客户端(客户端获取某个服务的所有实例时,该客户端被添加到订阅容器里面)。

总结:用了一个队列做解耦,并配合观察者设计模式,底层实际是把instance实例数据都存储到Cluster中的临时实例Set集合中。

  1. distroProtocol.sync():当前Nacos服务端节点上注册了一个服务实例,将最新的数据同步给Nacos服务端集群的其他节点
    2.1 构建DistroKey:含有目标Nacos服务端节点的地址。
    2.2 构建DistroDelayTask:含有DistroKey
    2.3 distroTaskEngineHolder.getDelayTaskExecuteEngine().addTask(distroKeyWithTarget, distroDelayTask):将key和task添加到一个类型是ConcurrentHashMap tasks的Map中。
    2.4 distroTaskEngineHolder.getDelayTaskExecuteEngine():返回一个Engine,该Engine实例化时,会用Executors.newScheduledThreadPool的线程池执行一个ProcessRunnable任务,该任务会拿到类型是ConcurrentHashMap tasks的Map中所有的key,然后根据key获取对应的Processor(此处拿到的时候DistroDelayTaskProcessor,该processor在delayTaskExecuteEngine实例化的时候就被set进去了),然后执行process()方法。此处用了策略模式processor()方法底层是构建了DistroSyncChangeTask
    2.5 构建DistroSyncChangeTask。他是一个Runnable实现类。实际是将数据发送给Nacos服务端集群的其他节点。待发送的Service数据是Datum结构的二进制序列化

6.3 DistroProtocol实例化

DistroConsistencyServiceImpl实例化的时候,会注入DistroProtocol,在DistroProtocol实例化的时候,会启动一个线程去拉取Nacos服务端集群其他节点上的服务数据

  1. DistroProtocol构造器会调用一个startDistroTask(),底层会用线程池执行一个DistroLoadDataTask任务,该任务底层是加载Nacos服务端集群的其他节点的数据,核心方法是load()

  2. load()
    2.1 如果只有自己一个服务端节点,那么就休眠1s,一直while循环扫描发现其他服务端节点。
    2.2 循坏等待distroComponentHolder.getDataStorageTypes()不为空(此处代码给出的日志打印是distro data storage register),果有数据则退出while循坏,否则休眠1s。distroComponentHolder.getDataStorageTypes()该值是在com.alibaba.nacos.naming.consistency.ephemeral.distro.DistroHttpRegistry#doRegister@PostConstruct注入的。
    2.3 loadAllDataSnapshotFromRemote():发送http请求,拿到远程服务器的所有数据(这些数据是二进制的数据,DistroData

7 Distro一致性算法

Nacos在服务注册方面采用了遵循AP定理的自研Distro一致性算法。该算法并不是由单独一处逻实现的,而是由8大机制实现的。详情可见Nacos Distro协议的设计机制

笔者总结Distro协议有8大机制:

  • 平等机制
  • 路由转发机制
  • 客服端心跳上报机制
  • 服务端健康检查机制
  • 本地读机制
  • 寻址机制
  • 新节点同步机制
  • 异步复制机制

你可能感兴趣的:(Java面试题笔记,Spring,Cloud,Alibaba,Nacos)