使用之前的疑问
- 服务端如何启动?server启动的时候一般会绑定对应的
RequestHander
,知道Server端如何启动的,追下去就可以找到Server在接收到请求后的处理流程 - 客户端如何启动?一般来说可能就是建立连接,然后开启一些任务
- 客户端和服务端通信方式?每个客户端建立几个连接?长连接?短连接?如果中途服务端发生变动(重启/宕机等情况)该如何处理?
- 配置推送的流程?如果失败该如何处理?服务端在重试期间,客户端一直等待吗?如果一台server失败,会不会切换到其它的server?
- 服务注册流程?
- 数据如何储存?
Server端启动
- 功能上:Nacos既支持配置中心,又支持注册中心,并且注册中心根据是否是临时节点来判断用AP模式还是CP模式,从这点来看,功能是比较强大的;
- 运维上:Nacoa支持配置中心和注册中心独立部署,也支持配置中心和注册中心合并部署到一个进程,完全看自己需求,从这一点看,部署是比较灵活的;
不管是配置中心还是注册中心,在Nacos中对应的都是springboot工程,部署比较方便,但从我的角度来看,这也会带来一些却缺点,比如说Nacos的启动流程变得更混乱了。通过阅读代码,发现Nacos的核心功能启动都是基于@PostConstruct
注解来触发的,在整个功能中,有非常多的@PostConstruct
注解,分布在不同的模块和包下面。通过Spring的特性来触发Nacos核心功能的启动,这本来也是没什么问题的,但对于想要快速了解Nacos启动流程的同学来说,还是挺不方便的。因为这些注解是分散的,想要知道它的启动流程,你需要知道有哪些@PostConstruct
注解,然后具体去了解每个@PostConstruct
注解对应什么样的逻辑,这样才能形成整体概念。
以下是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
- 加载配置文件
- 启动日志处理器
- 启动ServerMemberManager
- 启动MemberLookup
- 启动gRPC server端
- 启动Distro协议,与注册中心的AP模式相关
- 启动raft协议,与注册中心的CP模式相关
集群节点管理
Nacos服务端启动可以分为单机模式和集群模式:单机模式主要是方便我们调试,我们可以通过添加-Dnacos.standalone=true
启动参数指定Nacso以单机模式启动;集群模式要分几种情况。在集群模式下,不管是AP模式还是CP模式,一般都需要知道有那些server列表,以便于server之间进行通信,那在Nacao中该如何知道有那些server节点呢?Nacos中提供了相关APILookupFactory
、MemberLookup
:LookupFactory
是一个更高层次的API,便于我们快速获取/切换MemberLookup
,重点关注MemberLookup
。MemberLookup
在Nacos中国有3种实现:
StandaloneMemberLookup
: 对应Nacos单机模式,这个类核心实现就是获取本地的IP 端口FileConfigMemberLookup
: 从/${user.home}/nacos/conf/cluster.conf
中获取server列表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、连接
- ConnectionBasedClient: 长连接,针对2.x版本
- IpPortBasedClient:针对1.x版本
2、连接管理器
- ConnectionBasedClientManager: 管理ConnectionBasedClient
- EphemeralIpPortClientManager: 管理IpPortBasedClient,针对临时节点
- PersistentIpPortClientManager: 管理IpPortBasedClient,针对永久节点
依赖关系如下
- 连接在创建或断开的时候,会执行
ClientConnectionEventListenerRegistry#notifyClientxxx
方法,进而通知ConnectionBasedClientManager#clientxx
方法,更新对应的client缓存 ConnectionBasedClientManager、EphemeralIpPortClientManager
在初始化的时候,会启动一个定时任务,用于检查client是否已过期,默认过期时间30s,5s检查一次,为什么PersistentIpPortClientManager
不开启这样一个任务呢?因为前两者针对临时节点,而PersistentIpPortClientManager
这对永久节点。
有关于心跳的问题: 1、客户端发送心跳:在http客户端中,注册服务的时候,会创建一个心跳任务,每间隔5s向server发一次心跳;在gRPC中,不会创建心跳任务,此时根据TCP的连接状态来刷新clients,比如删除失效的clients
- 服务端接收心跳:gRPC客户端不会发送心跳,所以服务端也不会接收到心跳;针对Http客户端发送的心跳
配置更新
为了快速了解发布一个配置的整体流程,这里直接通过管控台藏创建一个配置
- 请求后台管控台的http接口:
/v1/cs/configs
,对应Controller为ConfigController
- 通过外部存储服务更新配置,这里对应的是
PersistService的实现类 ExternalStoragePersistServiceImpl
,最终对应的就是执行SQL写库 - 发布
ConfigDataChangeEvent
事件:将事件推送到BlockingQueue
,然后依次通知所有的订阅者。注意这里的订阅者不是指客户端监听,而是指在Server端的订阅者,也就是Server端收到ConfigDataChangeEvent
事件后接下来的处理,主要是观察者模式 - 执行订阅者对应的notify方法
- 记录日志并返回结果
那么,在Server端有那些订阅者呢?通过调试源码,发现可以通过静态方法NotifyCenter#registerSubscriber
注册订阅者,核心订阅者如下:
- RpcConfigChangeNotifier: 对应gRPC客户端,用于处理
ConfigDataChangeEvent
事件,将该事件通知客户端 - LongPollingservice: 对应Http客户端,用于处理
LocalDataChangeEvent
事件,将该事件通知客户端 - AsyncNotifyService: 针对处理
ConfigDataChangeEvent
事件,用于通知其它server节点配置发生了变更,同时更新本server的dump缓存
服务注册
服务发现有两种模式 AP/CP
AP模式
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);
- gRPC客户端与Server端建立连接
- 客户端向Server端发送请求(RpcClient#request)
- 服务端接收请求。其实在接收请求之前,还是有一个问题需要先弄清楚:服务端如何开启?我们必须要知道这个,因为一般在开启Server的时候,会绑定上对应的
RequestHandler
,如果知道了这个,我们在调试的时候就可以方便的断点,通过断点就可以快速的掌握整个流程。所以在Nacos中,服务端是如何开启的?核心如下:BaseRpcServer
被@PostConstruct
注解修饰,在启动server的时候绑定了这样的handerGrpcRequestAcceptor -> InstanceRequestHandler
- 先进入
GrpcRequestAcceptor#request
方法;然后进入RequestHandler#handleRequest
方法,在这个过程中,会先处理权限问题;然后进入InstanceRequestHandler#handle
方法,在该方法中会判断是注册服务还是销毁服务,这里以注册服务为例;然后进入EphemeralClientOperationServiceImpl#registerInstance
方法,EphemeralClientOperationServiceImpl是ClientOperationService的一个实现类,同时PersistentClientOperationServiceImpl也是ClientOperationService的一个实现类,分布对应AP和CP模式
- 执行
AbstractClient#addServiceInstance
方法,更新Map缓存,然后发布一个ClientEvent.ClientChangedEvent
事件 - 发布一个
ClientOperationEvent.ClientRegisterServiceEvent
事件 - 发布一个
MetadataEvent.InstanceMetadataEvent
事件,然后返回客户端
主干流程看起来很简单,其实还有一些核心的逻辑隐藏在接收到事件之后的处理中,比如说,这里只是更新了一个server节点的缓存,那该如何将数据同步到其它server节点,这样一想就会发现还有很多问题。
在一次服务注册中,上面看是发布了3个事件,其实一共有4个事件,在有些事件的监听者逻辑中,又会发布新事件,我们来看看涉及到的这几个事件分别是干嘛的
- ClientEvent.ClientChangedEvent: 该事件对应的监听者是
DistroClientDataProcessor
。那DistroClientDataProcessor
是什么时候注册的呢?DistroClientComponentRegistry#doRegister
被@PostConstruct
修饰了,在该方法中会注册DistroClientDataProcessor
。DistroClientDataProcessor
收到ClientEvent.ClientChangedEvent
事件后,会通过DistroProtocol
将数据变更同步到其它server节点,这样可以解答我们刚刚说的第一个问题。具体如何同步,我们下面再细分析 - ClientOperationEvent.ClientRegisterServiceEvent: 该事件对应的监听者是
ClientServiceIndexesManager
,这是一个spring bean
。ClientServiceIndexesManager
收到ClientOperationEvent.ClientRegisterServiceEvent
事件后,会先更新这个缓存ConcurrentMap<Service, Set<String>> publisherIndexes
,然后再发布一个ServiceEvent.ServiceChangedEvent
事件。publisherIndexes
具体干嘛用的,后面再分析 - MetadataEvent.InstanceMetadataEvent: 该事件对应的监听者是
NamingMetadataManager
,这是一个spring bean
。NamingMetadataManager
收到MetadataEvent.InstanceMetadataEvent
事件后,看逻辑也是为了更新两个缓存的信息expiredMetadataInfos
、serviceMetadataMap
- ServiceEvent.ServiceChangedEvent: 该事件对应的监者是
NamingSubscriberServiceV2Impl
,这是一个spring bean
。NamingSubscriberServiceV2Impl
收到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请求,这里对应的是NotifySingleTask
和AsyncTask
)
4、AsyncRpcTask
的执行逻辑:拿到NotifySingleRpcTask
,判断里面的server节点是不是当前节点,如果是则跟新本地dump文件;否则通过gRPC请求通知其它server节点
5、如果通知其它server的过程中出错了怎么办?会一直重试,不过重试延迟时间会随着重试次数的增加而增加,直到重试次数达到6
6、其它server收到server1发出的通知:GrpcRequestAcceptor -> ConfigChangeClusterSyncRequestHandler
,执行本地dump操作,即将数据库里的信息查询出来,更新到缓存文件
7、dump操作:先从数据库查出最新的值,然后更新本地缓存文件,然后发布一个LocalDataChangeEvent
事件,RpcConfigChangeNotifier
和LongPollingService
都会对该事件监听。收到监听事件之后,找到所有监听该key的listener(即 client),然后给客户端发通知,客户端收到通知后,再根据key找到对应的listeners,然后依次执行,这样就完成了配置的监听
8、server是怎么启动的呢?一开始一直没找到gRPC server是在哪里启动的,最后发现在BaseRpcServer
类中,有一个方法被@PostConstruct
注解修饰,也就是说在这个方法里面完成了server的启动
9、为什么要本地dump文件呢?主要是为了降低数据库的读压力,
服务发现
服务发现有两种模式 AP/CP
1、AP模式下,注册的是临时节点,此时不会涉及到持久化,数据就在一个Map中,然后各个server之间的数据同步通过广播的方式进行同步,失败就重试 2、CP模式下,注册的是持久节点,基于Raft算法
配置中心
有关于连接
- 两种方式:Http和gRPC,gRPC要2.0版本之后才提供的,我们只讨论gRPC这种情况
- 连接只有在客户端真正使用的时候才会创建,比如ConfigService初始化的时候不会创建,在推送配置的时候才会创建
- 在当前版本(2.0.0-ALPHA.2)中,每个客户端只会建立一个gRPC连接,但是看代码的意图,在后来的版本中,有可能一个客户端支持多个连接,然后将taskId分配到不同的连接上(ClientWorker#ensureRpcClient)
- 开始建立连接之前,会先获取所有的server列表,然后选者第一个开始建立连接,如果成功则返回;如果失败就重试,重试的时候会选择下一个server,最多重试3次(RpcClient#start)
- 如果步骤4重试3次之后还是没连上该怎么处理?在步骤4中,除了建立连接,还会启动两个后台线程:一个线程用于处理连接失败的情况;一个线程用于处理连接成功后的回调
有关于请求
- 客户端发送推送请求到指定的server,如果失败就重试,在请求不超时的条件下,最多重试3ci。假如说请求的时候,客户端和该server的长连接异常断开了(比如server节点下线等情况),这时候客户端会通过上面说到的后台任务,重新与可用server建立连接,保证连接可用
- server端收到请求,先更新数据库,然后更新本地缓存文件,然后进行通知,这里的通知包括两部分:客户端和服务端。客户端主要针对那些监听者;服务端就是指其它的server节点,告诉他们配置变更了
- 客户端收到配置变更通知,即执行客户端的listener逻辑;服务端收到配置变更通知,更新本地dump文件,然后触发给监听的客户端发送通知,客户端收到通知,执行listener逻辑
有关于存储
- 数据库:持久化
- 本地缓存,就是一个Map中,减轻数据的读压力(ConfigCacheService#dump)
参考链接
- Distro协议:cloud.tencent.com/developer/a…