Nacos原理解析

2,291 阅读12分钟

使用之前的疑问

  1. 服务端如何启动?server启动的时候一般会绑定对应的RequestHander,知道Server端如何启动的,追下去就可以找到Server在接收到请求后的处理流程
  2. 客户端如何启动?一般来说可能就是建立连接,然后开启一些任务
  3. 客户端和服务端通信方式?每个客户端建立几个连接?长连接?短连接?如果中途服务端发生变动(重启/宕机等情况)该如何处理?
  4. 配置推送的流程?如果失败该如何处理?服务端在重试期间,客户端一直等待吗?如果一台server失败,会不会切换到其它的server?
  5. 服务注册流程?
  6. 数据如何储存?

Server端启动

  1. 功能上:Nacos既支持配置中心,又支持注册中心,并且注册中心根据是否是临时节点来判断用AP模式还是CP模式,从这点来看,功能是比较强大的;
  2. 运维上:Nacoa支持配置中心和注册中心独立部署,也支持配置中心和注册中心合并部署到一个进程,完全看自己需求,从这一点看,部署是比较灵活的;

不管是配置中心还是注册中心,在Nacos中对应的都是springboot工程,部署比较方便,但从我的角度来看,这也会带来一些却缺点,比如说Nacos的启动流程变得更混乱了。通过阅读代码,发现Nacos的核心功能启动都是基于@PostConstruct注解来触发的,在整个功能中,有非常多的@PostConstruct注解,分布在不同的模块和包下面。通过Spring的特性来触发Nacos核心功能的启动,这本来也是没什么问题的,但对于想要快速了解Nacos启动流程的同学来说,还是挺不方便的。因为这些注解是分散的,想要知道它的启动流程,你需要知道有哪些@PostConstruct注解,然后具体去了解每个@PostConstruct注解对应什么样的逻辑,这样才能形成整体概念。

image.png

image.png

以下是2.0.0-ALPHA.2版本中,不同模块下的@PostConstruct注解列表

1. console      com.alibaba.nacos.console.config.ConsoleConfig 

2. cmdb         com.alibaba.nacos.cmdb.memory.CmdbProvider  

3. config       com.alibaba.nacos.config.server.auth.ExternalPermissionPersistServiceImpl
4. config       com.alibaba.nacos.config.server.auth.ExternalRolePersistServiceImpl
5. config       com.alibaba.nacos.config.server.auth.ExternalUserPersistServiceImpl
6. config       com.alibaba.nacos.config.server.controller.HealthController
7. config       com.alibaba.nacos.config.server.filter.CurcuitFilter
8. config       com.alibaba.nacos.config.server.service.capacity.CapacityService
9.  config       com.alibaba.nacos.config.server.service.capacity.GroupCapacityPersistService
10. config      com.alibaba.nacos.config.server.service.capacity.TenantCapacityPersistService
11. config      com.alibaba.nacos.config.server.service.datasource.LocalDataSourceServiceImpl  
12. config      com.alibaba.nacos.config.server.service.dump.EmbeddedDumpService   
13. config      com.alibaba.nacos.config.server.service.dump.ExternalDumpService
14. config      com.alibaba.nacos.config.server.service.repository.embedded.EmbeddedStoragePersistServiceImpl
15. config      com.alibaba.nacos.config.server.service.repository.embedded.StandaloneDatabaseOperateImpl
16. config      com.alibaba.nacos.config.server.service.repository.extrnal.ExternalStoragePersistServiceImpl

17. core        com.alibaba.nacos.core.cluster.remote.ClusterRpcClientProxy
18. core        com.alibaba.nacos.core.remote.AbstractRequestFilter
19. core        com.alibaba.nacos.core.remote.BaseRpcServer
20. core        com.alibaba.nacos.core.remote.ClientConnectionEventListener
21. core        com.alibaba.nacos.core.remote.ConnectionManager

22. istio       com.alibaba.nacos.istio.mcp.NacosMcpServer
23. istio       com.alibaba.nacos.istio.mcp.NacosMcpService

24. naming      com.alibaba.nacos.naming.cluster.ServerListManager
25. naming      com.alibaba.nacos.naming.cluster.ServerStatusManager
26. naming      com.alibaba.nacos.naming.consistency.ephemeral.distro.DistroConsistencyServiceImpl
27. naming      com.alibaba.nacos.naming.consistency.ephemeral.distro.DistroHttpRegistry
28. naming      com.alibaba.nacos.naming.consistency.ephemeral.distro.v2.DistroClientComponentRegistry
29. naming      com.alibaba.nacos.naming.consistency.persistent.raft.RaftCore
30. naming      com.alibaba.nacos.naming.consistency.persistent.raft.RaftPeerSet
31. naming      com.alibaba.nacos.naming.consistency.persistent.raft.RaftConsistencyServiceImpl
32. naming      com.alibaba.nacos.naming.core.DistroMapper
33. naming      com.alibaba.nacos.naming.core.ServiceManager
34. naming      com.alibaba.nacos.naming.misc.GlobalConfig
35. naming      com.alibaba.nacos.naming.misc.SwitchManager
36. naming      com.alibaba.nacos.naming.monitor.PerformanceLoggerThread
  1. 加载配置文件
  2. 启动日志处理器
  3. 启动ServerMemberManager
  4. 启动MemberLookup
  5. 启动gRPC server端
  6. 启动Distro协议,与注册中心的AP模式相关
  7. 启动raft协议,与注册中心的CP模式相关

集群节点管理

Nacos服务端启动可以分为单机模式和集群模式:单机模式主要是方便我们调试,我们可以通过添加-Dnacos.standalone=true启动参数指定Nacso以单机模式启动;集群模式要分几种情况。在集群模式下,不管是AP模式还是CP模式,一般都需要知道有那些server列表,以便于server之间进行通信,那在Nacao中该如何知道有那些server节点呢?Nacos中提供了相关APILookupFactoryMemberLookupLookupFactory是一个更高层次的API,便于我们快速获取/切换MemberLookup,重点关注MemberLookupMemberLookup在Nacos中国有3种实现:

  1. StandaloneMemberLookup: 对应Nacos单机模式,这个类核心实现就是获取本地的IP 端口
  2. FileConfigMemberLookup: 从/${user.home}/nacos/conf/cluster.conf中获取server列表
  3. AddressServerMemberLookup: 从独立的地址服务器上获取server列表

1、如果只是简单测试,可以通过单机模式启动,此时对应的是StandaloneMemberLookup

-Dnacos.standalone=true

2、以集群模式启动,每个server对应一台机器,通过配置文件的方式管理server节点信息

1. 启动参数添加 -Dnacos.member.list=IP1:8848,IP2:8848,IP3:8848
2. 或者在这文件文件中配置`/Users/luoxy/nacos/conf/cluster.conf`,每个IP:端口 对应一行记录

3、以集群模式启动,3个server节点部署在同一台机器,但是通过不同端口区分,注意这时候有一些限制

-Dserver.port=8848
-Dnacos.home=/Users/luoxy/nacos8848
-Ddebug=true
-Dnacos.member.list=172.16.120.249:8848,172.16.120.249:8858,172.16.120.249:8868 

必须要指定`nacos.home`参数, 因为默认是用户目录,如果此时三个server节点在同一台机器上启动,此时jraft对应的数据目录会有冲突,导致后面的两个节点启动失败

Client端启动

主要就是与server建立连接

配置中心

NacosFactory#createConfigService => new NacosConfigService => new ClientWorker => new ServerListManager => ConfigRpcTransportClient

注册中心

NacosFactory#createNamingService => new NacosNamingService => new NamingClientProxyDelegate => new NamingGrpcClientProxy => RpcClientFactory#createClient => GrpcSdkClient#start

连接管理

1、连接

  1. ConnectionBasedClient: 长连接,针对2.x版本
  2. IpPortBasedClient:针对1.x版本

2、连接管理器

  1. ConnectionBasedClientManager: 管理ConnectionBasedClient
  2. EphemeralIpPortClientManager: 管理IpPortBasedClient,针对临时节点
  3. PersistentIpPortClientManager: 管理IpPortBasedClient,针对永久节点

依赖关系如下

image.png

  1. 连接在创建或断开的时候,会执行ClientConnectionEventListenerRegistry#notifyClientxxx方法,进而通知ConnectionBasedClientManager#clientxx方法,更新对应的client缓存
  2. ConnectionBasedClientManager、EphemeralIpPortClientManager在初始化的时候,会启动一个定时任务,用于检查client是否已过期,默认过期时间30s,5s检查一次,为什么PersistentIpPortClientManager不开启这样一个任务呢?因为前两者针对临时节点,而PersistentIpPortClientManager这对永久节点。

有关于心跳的问题: 1、客户端发送心跳:在http客户端中,注册服务的时候,会创建一个心跳任务,每间隔5s向server发一次心跳;在gRPC中,不会创建心跳任务,此时根据TCP的连接状态来刷新clients,比如删除失效的clients

  1. 服务端接收心跳:gRPC客户端不会发送心跳,所以服务端也不会接收到心跳;针对Http客户端发送的心跳

配置更新

image.png

为了快速了解发布一个配置的整体流程,这里直接通过管控台藏创建一个配置

  1. 请求后台管控台的http接口: /v1/cs/configs,对应Controller为ConfigController
  2. 通过外部存储服务更新配置,这里对应的是PersistService的实现类 ExternalStoragePersistServiceImpl,最终对应的就是执行SQL写库
  3. 发布ConfigDataChangeEvent事件:将事件推送到BlockingQueue,然后依次通知所有的订阅者。注意这里的订阅者不是指客户端监听,而是指在Server端的订阅者,也就是Server端收到ConfigDataChangeEvent事件后接下来的处理,主要是观察者模式
  4. 执行订阅者对应的notify方法
  5. 记录日志并返回结果

那么,在Server端有那些订阅者呢?通过调试源码,发现可以通过静态方法NotifyCenter#registerSubscriber注册订阅者,核心订阅者如下:

  1. RpcConfigChangeNotifier: 对应gRPC客户端,用于处理ConfigDataChangeEvent事件,将该事件通知客户端
  2. LongPollingservice: 对应Http客户端,用于处理LocalDataChangeEvent事件,将该事件通知客户端
  3. AsyncNotifyService: 针对处理ConfigDataChangeEvent事件,用于通知其它server节点配置发生了变更,同时更新本server的dump缓存

服务注册

服务发现有两种模式 AP/CP

AP模式

image.png

AP模式有一个特点: 注册的节点类型为临时节点;数据不会持久化,使用了Distro协议,即: 临时数据的一致性协议

Properties properties = new Properties();
// Nacos 的服务中心的地址
properties.put(PropertyKeyConst.SERVER_ADDR, "localhost:8848");
NamingService nameService = NacosFactory.createNamingService(properties);


Instance instance = new Instance();
instance.setIp("127.0.0.1");
instance.setPort(8009);
instance.setMetadata(new HashMap<>());
instance.setEphemeral(true);
nameService.registerInstance("nacos-api", instance);

Thread.sleep(Integer.MAX_VALUE);
  1. gRPC客户端与Server端建立连接
  2. 客户端向Server端发送请求(RpcClient#request)
  3. 服务端接收请求。其实在接收请求之前,还是有一个问题需要先弄清楚:服务端如何开启?我们必须要知道这个,因为一般在开启Server的时候,会绑定上对应的RequestHandler,如果知道了这个,我们在调试的时候就可以方便的断点,通过断点就可以快速的掌握整个流程。所以在Nacos中,服务端是如何开启的?核心如下:BaseRpcServer@PostConstruct注解修饰,在启动server的时候绑定了这样的hander GrpcRequestAcceptor -> InstanceRequestHandler
  4. 先进入GrpcRequestAcceptor#request方法;然后进入RequestHandler#handleRequest方法,在这个过程中,会先处理权限问题;然后进入InstanceRequestHandler#handle方法,在该方法中会判断是注册服务还是销毁服务,这里以注册服务为例;然后进入EphemeralClientOperationServiceImpl#registerInstance方法,EphemeralClientOperationServiceImpl是ClientOperationService的一个实现类,同时PersistentClientOperationServiceImpl也是ClientOperationService的一个实现类,分布对应AP和CP模式
  5. 执行AbstractClient#addServiceInstance方法,更新Map缓存,然后发布一个ClientEvent.ClientChangedEvent事件
  6. 发布一个ClientOperationEvent.ClientRegisterServiceEvent事件
  7. 发布一个MetadataEvent.InstanceMetadataEvent事件,然后返回客户端

主干流程看起来很简单,其实还有一些核心的逻辑隐藏在接收到事件之后的处理中,比如说,这里只是更新了一个server节点的缓存,那该如何将数据同步到其它server节点,这样一想就会发现还有很多问题。

在一次服务注册中,上面看是发布了3个事件,其实一共有4个事件,在有些事件的监听者逻辑中,又会发布新事件,我们来看看涉及到的这几个事件分别是干嘛的

  1. ClientEvent.ClientChangedEvent: 该事件对应的监听者是DistroClientDataProcessor。那DistroClientDataProcessor是什么时候注册的呢?DistroClientComponentRegistry#doRegister@PostConstruct修饰了,在该方法中会注册DistroClientDataProcessorDistroClientDataProcessor收到ClientEvent.ClientChangedEvent事件后,会通过DistroProtocol将数据变更同步到其它server节点,这样可以解答我们刚刚说的第一个问题。具体如何同步,我们下面再细分析
  2. ClientOperationEvent.ClientRegisterServiceEvent: 该事件对应的监听者是ClientServiceIndexesManager,这是一个spring beanClientServiceIndexesManager收到ClientOperationEvent.ClientRegisterServiceEvent事件后,会先更新这个缓存ConcurrentMap<Service, Set<String>> publisherIndexes,然后再发布一个ServiceEvent.ServiceChangedEvent事件。publisherIndexes具体干嘛用的,后面再分析
  3. MetadataEvent.InstanceMetadataEvent: 该事件对应的监听者是NamingMetadataManager,这是一个spring beanNamingMetadataManager收到MetadataEvent.InstanceMetadataEvent事件后,看逻辑也是为了更新两个缓存的信息expiredMetadataInfosserviceMetadataMap
  4. ServiceEvent.ServiceChangedEvent: 该事件对应的监者是NamingSubscriberServiceV2Impl,这是一个spring beanNamingSubscriberServiceV2Impl收到ServiceEvent.ServiceChangedEvent事件后,会向PushDelayTaskExecuteEngine中丢一个任务,意图目前不太清楚

还是有一些地方不太理解,比如这几个缓存的用途

Distro协议

AP模式底层就是基于Distro协议来实现的,Distro 协议被定位为 临时数据的一致性协议,即:不需要把数据存储到磁盘或者数据库,临时数据通常和服务器保持一个session会话, 该会话只要存在,数据就不会丢失。

CP模式

CP模式有一个特点: 注册的节点类型为非临时节点;数据会持久化

Properties properties = new Properties();
// Nacos 的服务中心的地址
properties.put(PropertyKeyConst.SERVER_ADDR, "localhost:8848");
NamingService nameService = NacosFactory.createNamingService(properties);


Instance instance = new Instance();
instance.setIp("127.0.0.1");
instance.setPort(8009);
instance.setMetadata(new HashMap<>());
instance.setEphemeral(false);
nameService.registerInstance("nacos-api", instance);

Thread.sleep(Integer.MAX_VALUE);

结果发现,注册的还是临时节点,自己本地调试的结果,以及和官方确认的结果:长连接模式下无法使用CP模式。

但是可以通过Http的方式进行持久节点的注册,如下:

curl -X POST 'http://127.0.0.1:8848/nacos/v1/ns/instance?port=8848&healthy=true&ip=11.11.11.11&weight=1.0&serviceName=nacos.test.3&encoding=GBK&namespaceId=n1&ephemeral=false'

但此时报错了,报错信息如下(说明一下当前server部署情况: 单节点启动;启动参数 -Dnacos.standalone=true -Ddebug=true)

caused: java.util.concurrent.ExecutionException: com.alibaba.nacos.consistency.exception.ConsistencyException: com.alibaba.nacos.core.distributed.raft.exception.NoLeaderException: The Raft Group [naming_persistent_service_v2] did not find the Leader node;caused: com.alibaba.nacos.consistency.exception.ConsistencyException: com.alibaba.nacos.core.distributed.raft.exception.NoLeaderException: The Raft Group [naming_persistent_service_v2] did not find the Leader node;caused: com.alibaba.nacos.core.distributed.raft.exception.NoLeaderException: The Raft Group [naming_persistent_service_v2] did not find the Leader node

看起来和leader选举有关,初步判断是server部署单节点有有关

CP 模式的核心实现基于开源jraft: www.sofastack.tech/projects/so…

// 发送心跳 curl -v -X PUT "http://localhost:8848/nacos/v1/ns/instance/beat?serviceName=xxx"

IpPortBasedClient ClientBeatCheckTaskV2

InstanceBeatCheckTaskInterceptorChain

HealthCheckEnableInterceptor HealthCheckResponsibleInterceptor InstanceEnableBeatCheckInterceptor ServiceEnableBeatCheckInterceptor

InstanceBeatCheckTask UnhealthyInstanceChecker ExpiredInstanceChecker

配置管理

有这样一个问题,数据存在DB,假如一个集群中有3个server节点,客户端1连接了server1,客户端2连接了server2,当客户端1写了一个配置时,客户端2要怎么监听到这个配置的变化?或者说,修改了某个配置,在不同server节点之间,如何同步?

1、客户端1向nacos推送一条配置,因为此时客户端1是与server1建立的长连接(gRPC),所以该请求会请求到server1 2、server1收到请求,首先更新数据库,然后更新本地Dump文件,然后发布一个ConfigDataChangeEvent事件,该事件会被AsyncNotifyService接收 3、AsyncNotifyService接收事件后,首先获取集群下的所有server节点,然后基于ConfigDataChangeEvent事件,为每个server节点封装一个NotifySingleRpcTask对象,最后再合并成AsyncRpcTask对象交给线程池执行(如果是http请求,这里对应的是NotifySingleTaskAsyncTask) 4、AsyncRpcTask的执行逻辑:拿到NotifySingleRpcTask,判断里面的server节点是不是当前节点,如果是则跟新本地dump文件;否则通过gRPC请求通知其它server节点 5、如果通知其它server的过程中出错了怎么办?会一直重试,不过重试延迟时间会随着重试次数的增加而增加,直到重试次数达到6 6、其它server收到server1发出的通知:GrpcRequestAcceptor -> ConfigChangeClusterSyncRequestHandler,执行本地dump操作,即将数据库里的信息查询出来,更新到缓存文件 7、dump操作:先从数据库查出最新的值,然后更新本地缓存文件,然后发布一个LocalDataChangeEvent事件,RpcConfigChangeNotifierLongPollingService都会对该事件监听。收到监听事件之后,找到所有监听该key的listener(即 client),然后给客户端发通知,客户端收到通知后,再根据key找到对应的listeners,然后依次执行,这样就完成了配置的监听 8、server是怎么启动的呢?一开始一直没找到gRPC server是在哪里启动的,最后发现在BaseRpcServer类中,有一个方法被@PostConstruct注解修饰,也就是说在这个方法里面完成了server的启动 9、为什么要本地dump文件呢?主要是为了降低数据库的读压力,

服务发现

服务发现有两种模式 AP/CP

1、AP模式下,注册的是临时节点,此时不会涉及到持久化,数据就在一个Map中,然后各个server之间的数据同步通过广播的方式进行同步,失败就重试 2、CP模式下,注册的是持久节点,基于Raft算法

配置中心

有关于连接

  1. 两种方式:Http和gRPC,gRPC要2.0版本之后才提供的,我们只讨论gRPC这种情况
  2. 连接只有在客户端真正使用的时候才会创建,比如ConfigService初始化的时候不会创建,在推送配置的时候才会创建
  3. 在当前版本(2.0.0-ALPHA.2)中,每个客户端只会建立一个gRPC连接,但是看代码的意图,在后来的版本中,有可能一个客户端支持多个连接,然后将taskId分配到不同的连接上(ClientWorker#ensureRpcClient)
  4. 开始建立连接之前,会先获取所有的server列表,然后选者第一个开始建立连接,如果成功则返回;如果失败就重试,重试的时候会选择下一个server,最多重试3次(RpcClient#start)
  5. 如果步骤4重试3次之后还是没连上该怎么处理?在步骤4中,除了建立连接,还会启动两个后台线程:一个线程用于处理连接失败的情况;一个线程用于处理连接成功后的回调

有关于请求

  1. 客户端发送推送请求到指定的server,如果失败就重试,在请求不超时的条件下,最多重试3ci。假如说请求的时候,客户端和该server的长连接异常断开了(比如server节点下线等情况),这时候客户端会通过上面说到的后台任务,重新与可用server建立连接,保证连接可用
  2. server端收到请求,先更新数据库,然后更新本地缓存文件,然后进行通知,这里的通知包括两部分:客户端和服务端。客户端主要针对那些监听者;服务端就是指其它的server节点,告诉他们配置变更了
  3. 客户端收到配置变更通知,即执行客户端的listener逻辑;服务端收到配置变更通知,更新本地dump文件,然后触发给监听的客户端发送通知,客户端收到通知,执行listener逻辑

有关于存储

  1. 数据库:持久化
  2. 本地缓存,就是一个Map中,减轻数据的读压力(ConfigCacheService#dump)

参考链接

  1. Distro协议:cloud.tencent.com/developer/a…