Nacos Server Register and Discovery
Nacos Server Register and Discovery
前言
之前尝试去简单梳理了一下RocketMQ的两个模块的初始执行流程,感觉还是没有做到与客户端结合,很多点停留在表面,并没有深入地去思考其为什么要这么写,后续还要继续完善。因为之前的一个个人项目中用到了Zookeeper作为注册中心,就想深入地去看一下同为注册中心的Nacos,这篇文章参考了Nacos的官方文档,从其数据模型到实现逻辑一一展开。
Nacos 注册中心的设计原理
一、数据模型
在讲服务注册与发现的源码之前,我们先要对 Nacos 中的数据模型
有一个简单了印象。注册中心的核心数据是服务的名字和它对应的网络地址,其还包括了实例的健康状态、权限、权重等等信息。Nacos 采用的是一种服务-集群-实例
的分级模型,这样基本可以满足服务在所有场景下的数据存储和管理。
除此之外,Nacos 提供了四层的数据逻辑隔离模型
,用户账号对应的可能是⼀个企业或者独立的个体,这个数据⼀般情况下不会透传到服务注册中心。⼀个用户账号可以新建多个命名空间,每个命名空间对应 ⼀个客户端实例,这个命名空间对应的注册中心物理集群是可以根据规则进行路由的,这样可以让注册中心内部的升级和迁移对用户是无感知的,同时可以根据用户的级别,为用户提供不同服务级别的物理集群。再往下是服务分组
和服务名
组成的二维服务标识,可以满足接口级别的服务隔离。
Nacos 的数据模型很复杂,为了适配各种场景,其中存在非常多的属性,但在大多数场景下,我们可以忽略一些数据属性,只关注于我们场景需要的,如当我们不配置 Group 时,Nacos 会为我们分配一个默认 group。
二、Nacos 服务注册与发现源码解读
服务注册
既然是服务注册,那我们肯定要从客户端开始看起。这里用到了 SpringBoot 的自动装配,就不在多赘述了,每一个 Nacos 客户端都会引入这些启动类,顾名思义,NacosDiscoveryAutoConfiguration
应该就是总的入口了,我们继续看。
我们跟进来,就可以看到 NacosServiceRegistry
,很明显这个应该就是服务注册的入口了。
我们跟进入就会看到服务注册的核心方法,这里我们可以看到,Instance
其实就是服务的单个实例,其中包含了很多数据,此时我们就可从中初见 Nacos 的服务分级模型,Service - Cluster - Instance
,这里有些数据我们在配置的时候故意忽略了,但其都有一个默认初始值,也就是之前说的,只需要关注我们场景中需要的属性即可。
我们继续跟进去,在这里可以看到 addBeatInfo() 方法,顾名思义,这个应该就是客户端心跳机制
的实现了,但这里我们先不看,专注于服务注册,后续再展开。
这里就是客户端的服务注册核心逻辑,将 Instance 转为 Map,然后通过 HTTP 请求发送给 Nacos 服务端。
这个接口我们在 Nacos 官方文档中也可以看到。
此时 Nacos 客户端的服务注册的任务就完成了,我们接着看服务端是怎样处理的。我提前把 Nacos 2.3 的源码给拉下来了,进去之后你就会发现,Nacos 本质也是一个 Web 项目,其中 console
就是 Nacos 的核心启动入口。naming
就是服务注册与发现的核心模块,在里面我们就可以找到服务注册的 POST 接口。
这里应该很熟悉了,SpringBoot 项目的Controller。我们抓着主线看,在这里首先把客户端传的 Map 转换为 Instance,然后通过getInstanceOperator().registerInstance()
注册服务。
值得一提的是,在 Nacos2.0 后将是否持久化的数据抽象至服务级别,且不再允许⼀个服务同时存在持久化实例和非持久化实例,实例的持久化属性继承自服务的持久化属性。这是官方的说法,意味着一个服务下面的实例只能是临时的 or 持久的,一般默认是 ephemeral
,即临时实例。这里我们演示的是临时实例的注册逻辑,就看 EphemeralClientOperationServiceImpl
中 registerInstance
的具体实现。
在这里可以看到有两个对象,Client
和Service
。Service就是注册的服务,一般会维护命名空间、组名、服务名、临时客户端标识等信息,服务下面就是实例集合。Nacos在2.0 版本之后新增了Client模型,每个Client都有自身唯一的ClientId,一个gRPC长连接
对应一个Client。Client负责管理一个客户端的服务实例注册Publish和服务订阅Subscribe。
Client的创建及gRPC连接
这就是Client接口的具体方法,我们根据方法名大概就能猜出其作用,主要是维护客户端的一些信息。
我们进到 createIpPortClientIfAbsent 方法里,就可以看到其主要逻辑,这里用到了 ClientManager
,这是用来管理 Client的创建、获取、释放的接口,可以看到 ClientManager 也对实例类型做了不同的实现类。
我们这里就看看临时实例是怎么实现的,这里还有个 ClientFactory<IpPortBasedClient> clientFactory
,这个是创建 Client 的工厂,底层实现也会区分临时、持久实例等,就不再展开。clientConnected 方法接收 clientFactory 创建的 Client 对象后,就会初始化 Client 的心跳检查定时任务,再把其放在名为 clients 的Map中。
1 | ConcurrentMap<String, IpPortBasedClient> clients = new ConcurrentHashMap<>() |
Service的创建与注册
Service的创建其实没什么好说的,就是根据客户端的参数去 new 一个 Service 对象。
后续的操作是通过 clientOperationService.registerInstance
进行,ClientOperationService
接口也基于不同的服务进行了区分,我们直接看 EphemeralClientOperationServiceImpl
中对 registerInstance
的具体实现。
这里是先通过 ServiceManager
获取了一个单例的 Service 对象,维护了两个 Map,当我们需要的 Service 在Map中不存在时,就发布 ServiceMetadataEvent
事件,最终将其放到对应的Map中。
1 | //存储Service的单例 |
之后就通过 clientId 从 ClientManager 中获取对应的 Client,这一步其实是从 ClientManager 中的Map clients
中获取的,而且获取到的是 Client 的实现类 AbstractClient
。AbstractClient 负责存储当前客户端的服务注册表,即Service与Instance的映射关系
。对于单个客户端来说,同一个服务只能注册一个实例。
下面就是真正注册实例的逻辑,先把服务与实例的映射关系保存在 publishers
中,然后通过 NotifyCenter
发布一个 ClientChangedEvent
服务注册事件。
服务注册事件的处理
我们进到 NotifyCenter 的 publishEvent 方法中,可以看到其最终是通过 EventPublisher
将事件发布出去。EventPublisher 是事件发布的上层接口,上面定义了基本的方法,比如发布事件,通知订阅者处理事件等。服务注册实际上最终走的是 NamingEventPublisher
中的逻辑。
我们进到 NamingEventPublisher 中,就可以看到其主要逻辑,发布事件实际上是把事件存入 BlockingQueue
中,使用 BlockingQueue 有一个好处就是,对数据进行存取的时候,如果有数据就返回,没有数据就会让出 CPU 的控制权,而不会一直阻塞住线程。
1 | BlockingQueue<Event> queue |
我们回到 NamingEventPublisher 本身来看,可以看到其继承了 Thread
类,这样我们直接去看其 run 方法。
在 run 方法中,我们就可以看到,先是要等待所有事件订阅者初始化完成,内部是碰到没有初始化的直接调用 ThreadUtils.sleep(1000L) 方法阻塞线程。当所有订阅者都初始化完成后,就调用 handleEvents
去处理事件。
可以看到,handleEvents 方法中会轮训事件队列,当获取到一个 event 后,就进行处理事件。
拿到事件的类型,之后获取订阅了该类型的所有订阅者,通知每一个订阅者去处理该事件。
通过线程池或者直接 new 一个线程去执行订阅者的处理事件。
服务注册事件最终是通过 ClientServiceIndexesManager
去处理的,我们进到里面可以看到其订阅的事件。
我们看看 ClientServiceIndexesManager
是怎么处理服务注册事件的,handleClientDisconnect
方法是处理服务下线事件的,handleClientOperation
就是具体的客户端操作了。
可以看到大概是将客户端与服务的映射关系存入一个Map中,这里的 publisherIndexes
就是注册表
,然后发布了一个 ServiceChangedEvent
注册表变更事件。
1 | /** |
服务注册表变更事件最终是由 NamingSubscriberServiceV2Impl
进行处理的,之后就是进行通知订阅的客户端,这里是新建一个 PushDelayTask
延时任务,将当前服务的变更全量信息
推送给所有订阅了该事件的客户端去更新服务信息。