Nacos作为注册中心有几个核心功能点
- 服务注册:Nacos Client会通过发送REST请求的方式向Nacos Server注册自己的服务,提供自身的元数据,比如ip地址、端口等信息。 Nacos Server接收到注册请求后,就会把这些元数据信息存储在一个双层的内存Map中。
- 服务心跳:在服务注册后,Nacos Client会维护一个定时心跳来持续通知Nacos Server,说明服务一直处于可用状态,防止被剔除。默认 5s发送一次心跳。
- 服务同步:Nacos Server集群之间会互相同步服务实例,用来保证服务信息的一致性。
- 服务发现:服务消费者(Nacos Client)在调用服务提供者的服务时,会发送一个REST请求给Nacos Server,获取上面注册的服务清 单,并且缓存在Nacos Client本地,同时会在Nacos Client本地开启一个定时任务定时拉取服务端最新的注册表信息更新到本地缓存。
- 服务健康检查:Nacos Server会开启一个定时任务用来检查注册服务实例的健康情况,对于超过15s没有收到客户端心跳的实例会将它的 healthy属性置为false(客户端服务发现时不会发现),如果某个实例超过30秒没有收到心跳,直接剔除该实例(被剔除的实例如果恢复发送 心跳则会重新注册)
客户端
服务注册Client
从client包入手通过Springboot的自动配置过程找到配置入口NacosDiscoveryAutoConfiguration其中NacosAutoServiceRegistration实现了ApplicationListener监听了服务启动初始化事件:
跟踪start方法,最终会调用ServiceRegistry接口的register方法,追溯该成员变量的来源它便是配置入口装配的NacosServiceRegistry:
它会把将服务的信息封装成实例Instance,然后添加心跳任务同时注册实例(心跳任务暂不展开,先看注册)
(接着看Restful风格API类com.alibaba.nacos.naming.controllers.InstanceController#register -->前往目录中的服务注册)
服务发现1
跟踪NacosNamingService#getAllInstances方法主要调用得是HostReactor#getServiceInfo方法,会根据serviceName获取到所有实例,首次先通过API 更新到缓存中:
然后添加一个定时更新任务
接着我们跟踪InstanceController#list API --> 前往目录中的服务发现2
服务端
服务注册Server
会先构建空的Service对象放入服务注册表中(Map<namespace, Map<group::serviceName, Service>> serviceMap,再添加实例
在添加实例方法中构建Cluster然后添加新实例(通过写时复制,实例列表为共享资源存在线程安全问题,添加场景为读多写少所以采取写时复制的方式并发也更高),Nacos默认(AP)实例都为临时实例(并发高)所以跳转到DistroConsistencyServiceImpl的put实现:
put方法进行了三个操作:
- 将实例列表封装成Datum对象放入dataStore中
- 添加一个CHANGE行为的任务到notifier中
- 添加一个任务到taskDispatcher中
DistroConsistencyServiceImpl.Notifier
它是一个Runnable实现类,内部维护了一个阻塞队列tasks,addTask时通过services(ConcurrentHashMap)去重后添加任务,由run方法取出处理。(那它什么时候运行呢,DistroConsistencyServiceImpl初始化时便创建了一个守护线程来执行;这是一种生产者消费者模式的思想运用)
继续看run方法,取出任务后通过service的唯一key取出实例列表调用onChange方法,既dataStore用于生产者消费者之间传递数据
onChange方法最终就是更新实例集合
(Nacos是通过copyOnWrite的方式更新注册表,而Eureka是通过多级缓存Map定时同步的方式,时效性较差)
服务同步
TaskDispatcher.TaskScheduler
它也是一个Runnable实现类,任务由前面注册时添加,运行方式与Notifier类似,默认当数据量达到1000或同步间隔超过2秒时提交异步的同步任务。
(同步任务就是通过Api DistroController#onSyncDatum推送实例数据给集群成员,成员最终又通过DistroConsistencyServiceImpl的onPut方法写入实例)
健康检查
心跳健康监测:回忆一下服务注册时addBeatInfo添加了一个心跳任务,任务请求得就是以下方法:
实际执行的任务:ClientBeatProcessor.run():
问:该方法更新了一个LastBeat字段,那在哪使用到该字段呢?
答:在ServiceManager#registerInstance注册实例时调用了createEmptyService#方法初始化Service,其中调用的putServiceAndInit 下service.init() 方法创建的健康监测任务中会使用。
服务发现2
InstanceController#list API 调用的doSrvIPXT 方法:
最终调用了Cluster#allIPs() 方法把Cluster分组下所有临时和持久的实例返回给客户端。
CAP原则与BASE原则
P:分布式系统多个节点间必然是需要同步数据的,当出现网络等不确定因素无法同步数据时(分区),应该使系统依然能够对外提供服务(容错)。(如果不保证P的话就跟单机系统没区别了)
Nacos的CP
Nacos对于临时实例只存在内存中(即AP),而持久化实例会进行文件存储实现类Raft协议;Raft和ZAB协议都是分布式一致性协议Paxos的简化,两者类似,主要包括两部分:1.leader选举(半数以上节点投票同意),2.集群写入数据同步(两阶段提交)。
ServiceManager#addInstance注册实例时,如果是非临时实例会执行RaftConsistencyServiceImpl的put方法,实际调用RaftCore#signalPublish 方法:
(从节点也是先写磁盘再更新内存,更新任务最终也是调用onChange方法更新实例集合)
Leader节点选举
RaftCore#init方法被@PostConstruct修饰会在服务启动时执行:
MasterElection任务设置了一个开始选举时间,当选举时间<=0时开始投票,然后调用了sendVote 方法:(节点的开始选举时间随机,为了让时间短的节点更容易成为leader)
/vote接口中选举者的选举周期少于等于自己的,则投票给自己,否则重置选举时长并投给选举者,最后返回自身节点对象。最后统计票数决定leader:
(目前只是把leader选出来了,那在哪里通知其他节点谁是leader呢?在init方法所添加的心跳任务中。)
主从节点数据同步
HeartBeat任务:只有leader才能发送的心跳任务,实际调用的sendBeat 方法:
(只是把key同步给从节点,那实例数据怎么同步呢?由从节点筛选好需要处理的key后主动获取同步。)
/beat接口实际调用了RaftCore#receivedBeat 方法,做三个事情,1是在收到心跳数据时变更为从节点:
2是对比Service的key进行筛选处理:
3是通过/raft/datum API接口批量同步batch中差异key对应的实例数据:
(最后会清理掉无效的key)