«

Nacos源码学习计划-Day12-集群-CAP原则和Raft协议和Distro协议理论前提

ZealSinger 发布于 阅读:92 技术文档


Nacos的内核设计中,一开始的目标就是尽可能减少用户部署和运维的成本,即用户只需要一个程序包就能迅速的部署和使用Nacos单机服务和集群服务(来自阿里官方电子书《Nacos架构与原理》)。但是我们知道,Nacos本身是一个需要数据存储的一个组件,也就是说Nacos内部实现数据存储。如果是单机服务还好说,在Nacos内部内嵌关系型数据即可,但是在集群环境下,需要考虑集群节点之间的数据同步以及数据一致性,为了解决这个问题,也就导致我们不得不的引入共识算法,通过算法来保障各个节点之间的数据的一致性。

那么对于分布式集群服务的数据一致性的,自然就逃脱不了CAP理论,根据CAP理论,一个系统只能在同一时刻为CP系统或者AP系统,这是因为CA两者是无法共存的。Nacos作为一个AP和CP双特性的系统,能作到AP和CP模式的切换,这个就和上述说到的Nacos内核中对于数据一致性协议的设计了,Nacos底层通过Raft协议(实现CP特性)和Distro协议(实现AP特性)会在单个集群中同时运行 CP 协议以及 AP 协议

CAP理论

CAP理论的简单理解就是由C A P 三个特性

从上面三个特性中可以看到,其中分区一致性,因为网络环境的不稳定,集群内部出现问题肯定是有几率发生的,所以解决分区一致性问题,成为了分布式集群系统必须解决的问题。

那么当集群中出现分区问题:例如ABC组成一个集群,A和集群BC之间出现了断连,此时访问A

如果A能正常相应,那么是满足可用性的,但是因为ABC已经不在一个集群中,A的数据自然不会立马同步到BC,也就是说明集群内部的节点之间的数据不一致,那么就违背了一致性原则; 同理,如果A不能正常访问,那么为了保证集群内部的数据是一致的,那么只能让BC提供服务,就导致A是不可用的,这就违背了可用性原则;

所以CAP理论总的而言就是,一个分布式集群系统,特性一般为一致性,可用性,分区一致性三个维度,但是只能满足CP或者AP,不可能同时满足CAP三个维度

Nacos设计双特性的背景

为什么双特性

这个其实要从Nacos的使用场景出发,我们知道,Nacos最大的两个功能模块是服务注册 和 配置管理

对于服务注册模块的角度而言,主要功能为统一管理注册上来的微服务,让微服务之间可以感知对方的存在,微服务之间的相互调用必须通过注册中心进行服务发现,因为对于服务注册中心的组件的可用性提出了很高的要求,需要在任何场景下,尽可能保证服务注册发现中心的对外提供服务的能力,所以,为了保证高可用性,强一致性的协议就没那么适用了,强一致性共识算法中能否对外提供服务是有要求的,一般会要求集群可用节点数量过半,如果集群中不可用节点数量超过一半整个算法就会直接罢工(例如Zookeeper),所以这里我们肯定是设计为AP系统 那么是不是满足CP说明数据一致性不重要呢?倒也不是,可以看到,注册服务肯定也是需要保证一定的数据一致性的,我们可以使用折衷的思想,不能保证整个服务运行过程中的数据一致性,我们可以保证最终一致性,所以Nacos在这方面使用的是最终一致性算法Distro协议

特别注意:以上内容是针对于Nacos的临时实例(非持久化服务 需要客户端主动上报服务实例心跳进行续约)而言。
而对于 Nacos 服务发现注册中的持久化服务,因为所有的数据都是直接使用调用 Nacos
服务端直接创建,因此需要由 Nacos 保障数据在各个节点之间的强⼀致性,故而针对此类型的服务
数据,选择了强⼀致性共识算法来保障数据的⼀致性。
也就是说Distro协议是专门针对临时实例的最终一致性(AP)的分布式协议,也说明非临时实例是CP

对于配置管理模块的角度而言,配置是Nacos服务端统一进行的创建和管理,必须保证大部分节点都是配置了此数据才能认定配置保存成功/成功配置,如果无法保证所有节点的配置,就会导致服务的情况都不一样甚至可能会导致启动失败,这个问题是很严重的,所以从这里可以知道,作为配置管理模块,需要强一致性,自然而然就是需要使用强一致性的共识算法。

为什么是Raft协议和Distro协议

Raft协议没什么太好说的,当时的工业生产中,使用最多的强一致性共识算法就是Raft协议,Raft协议不仅容易理解,而且还有很多成熟的工业方案,例如蚂蚁金服JRaft,Zookeeper的ZAB算法,Consul的Raft,百度的BRaft,Apache Ratis。因为Nacos本身是Java技术栈,在加上都是阿里的生态且不引入新的中间件,最终选择了JRaft,并且JRaft支持RaftGroup,为后续Nacos的多数据分片带来了可能性

Distro算法是阿里自研的一个最终一致性协议,业界里面常用的是Gossip协议(redis集群中使用),Eureka内部的同步算法。而 Distro 算法是集 Gossip 以及 Eureka 协议的优点并加以优化而出来的,对于原生的 Gossip,由于随机选取发送消息的节点,也就不可避免的存在消息重复发送给同⼀节点的情况,增加了网络的传输的压力,也给消息节点带来额外的处理负载,而 Distro 算法引入了权威 Server 的概念,每个节点负责⼀部分数据以及将自己的数据同步给其他节点,有效的降低了消息冗余的问题。

Nacos实现CP特性-Raft协议

在Raft协议中,为每个集群节点定义了三种状态

在Raft协议中,只有Leader节点有权利处理客户端数据的请求,如果不是Leader的节点但收到了请求,也会转移到Leader节点上进行数据处理,Leader 节点和 Follower 节点之间会有心跳机制(AppendEntries RPC 携带心跳包or数据包)

数据同步/读写流程

数据同步流程大致如下:数据的写入一共有两个状态:uncommitcommit

若部分 Follower 未成功复制日志(如网络故障、节点宕机),Raft 通过以下机制保证最终一致:

Raft 的核心是 “Leader 日志复制→多数确认提交→通过索引同步所有节点”,通过 “任期号 + 日志索引” 和 “持续重试” 保证一致性

选举Leader流程

Raft协议选举流程的实现有点类似我们生活中的投票活动,遵循少数服从多数的原则

Raft 初始化时,所有节点均为 Follower,各自维护一个随机的 Election Timeout(称之为选举超时 150-300ms)。超时后,节点转为 Candidate,先给自己投一票,再向其他节点发送RequestVote RPC请求投票。这一随机机制是为了避免多个节点同时超时,导致 “分裂投票”(Split Vote)。

例如当前集群中存在ABC三个节点,其中C的Election Timeout最少,那么即C最先转化为Candidate状态并且给自己投一票,然后给AB发送RequestVote RPC请求投票

当接收到了某个节点的RequestVote RPC之后,会根据如下两个决策进行投票

当节点收获超过半数的投票,则会晋升为Leader节点,Leader 产生后,通过定期发送心跳(AppendEntries RPC)维持 leadership,阻止其他节点触发选举(心跳会重置节点的Election TimeOut)

若 Leader 下线,Follower 因超时未收到心跳,会转为 Candidate 重新发起选举,且新选举会进入新的任期(Term)(任期是单调递增的整数,用于标识选举轮次)

当多个 Candidate 同时发起选举(如 4 节点中 A 和 B 同时成为 Candidate,各得 2 票,均未达 3 票的多数),此时所有 Candidate 的选举会失败,且它们的 Election Timeout 计时器会重新随机重置(仍在 150-300ms 范围内)。由于新的超时时间是随机的,必然有一个 Candidate 会先超时,再次发起RequestVote RPC。此时其他节点尚未超时,会优先给先发起请求的 Candidate 投票(遵循 “先到先得” 和 “日志完整性” 原则),从而避免再次分裂,最终选出 Leader

从上述可以总结如下几点
1:整个集群如果是刚开始,即每隔节点都没有任何先前的数据日志,Raft的初始化其实就是给所有节点分配个随机数,随机数越低的优先当Leader
2:当存在数据日志的时候,就会考虑数据日志的最新程度,数据越新的节点越容易当Leader

 

脑裂问题

集群节点有 5 个,节点 B 是 Leader,但是由于发生了网络分区问题,节点 A、B 可以相互通信,可是节点 C、D、E 不能和 Leader 进行通信,那么节点 C、D、E 将会重新进行 Leader 选举,最终节点 C 也成为了 Leader。此时,在原本一个集群下,就会产生两个 Leader 节点。

如果此时两个客户端连接,一个连到了CDE所在集群,一个连到了AB所在集群,第一个客户端请求到了节点 B,但是由于它只有一个 Follower 节点,达不到半数以上的要求,所以节点 B 的数据一直处于不会提交的状态,数据也不会写入成功第二个客户端请求到了节点 C,它是有两个 Follower 节点,有半数以上支持,所以它是能够写入成功的。

假如这个时候网络突然恢复了,5 个节点都可以相互通信了,在这个时候两个 Leader 都会相互发送心跳,但是节点 B 会发现节点 C 的 Term 比自己大,所以它会认节点 C 为 Leader,自己自动退位成为 Follower 节点。

image-20251110205832150

Nacos实现AP特性-Distro协议

Distro 协议是 Nacos 社区自研的⼀种 AP 分布式协议,是面向临时实例设计的⼀种分布式协议,其保证了在某些 Nacos 节点宕机后,整个临时实例处理系统依旧可以正常工作。作为⼀种有状态的中间件应用的内嵌协议,Distro 保证了各个 Nacos 节点对于海量注册请求的统⼀协调和存储。

对于Distro协议的相关的数据同步等操作,我们之前的文章都是按照临时实例进行分析的,其实我们前面的集群新增节点数据同步,节点健康状态变更同步的逻辑,其实就很好体现了Distro协议的设计。

数据读写 / 同步流程:分片存储 + 异步收敛

Distro 协议针对服务发现中的非持久化实例(临时实例,依赖客户端心跳续约)设计,数据存储在内存中,核心流程围绕 “分片管理 + 异步同步” 展开。

1. 数据分片:节点自主管理部分数据

2. 写入流程:本地优先,异步扩散

客户端写入(如注册实例)时:

  1. 路由到主节点:客户端通过哈希计算找到服务对应的主节点,发送写入请求(HTTP/GRPC)。

  2. 本地写入:主节点收到请求后,先写入本地内存注册表(无需等待其他节点确认),立即返回成功给客户端(保证低延迟)。

  3. 异步批量同步:主节点将数据变更放入本地队列,定期(默认 500ms)批量同步给集群其他节点(非主节点),同步方式为 “增量推送”(仅发送变更数据)。

3. 读取流程:本地读取,容忍短暂不一致

客户端读取(如查询服务实例列表)时:

  1. 可向任意节点发起请求(无需路由到主节点),节点直接返回本地内存中的数据。

  2. 若本地数据与主节点存在短暂差异(如同步尚未完成),客户端可能读到旧数据,但后续通过异步同步会自动修正(最终一致)。

这个地方我们在实例查询相关的文章中说过一点,新版本中Nacos客户端查询的时候会直接查询的当前节点的最新数据而不是客户端本地的缓存,所以不会出现差异。但是这里我们又说会出现,需要注意这里是两个部分
客户端X ---- 集群服务A ----- 集群其余节点other
A中会包集群中其余节点的信息,X和A之间不出意外的话是不会差异的,但A和集群其余节点间的数据同步需要等待别的节点发起同步请求,所以A和other之间是会存在短暂的差异的,需要靠异步任务进行核对与同步,所以这里才说会有短暂的差异

 

4. 数据同步与收敛:Gossip + 定期校验

为保证最终一致性,Distro 设计了两层同步机制:

选举流程:无 “Leader” 设计,节点平等

Distro 协议是去中心化的,不存在 “Leader” 角色,因此没有传统意义上的 “选举流程”。所有节点地位平等,均可独立处理读写请求,核心原因如下:

  1. 数据分片自治:每个节点仅负责部分分片的主节点工作,无需全局领导者协调。

  2. 无统一决策依赖:写入操作由主节点本地处理后异步扩散,无需多数派确认,因此不需要 “Leader” 统一协调投票。

  3. 故障自动容错:若某个节点(如分片主节点)宕机,客户端会通过重试路由到其他节点(其他节点会临时接管该分片的读写,待原主节点恢复后自动同步数据)。

脑裂问题处理:分区容忍优先,数据自动合并

脑裂(网络分区)是分布式系统中集群被分割为多个独立子网的场景。Distro 协议针对脑裂的处理完全体现 “AP 特性”:

1. 分区内可用:优先保证可用性

当集群分裂为多个分区(如 P1、P2):

2. 分区恢复后:数据自动合并收敛

当网络恢复,分区重新合并:

3. 为何无需担心 “多 Leader 冲突”?

由于 Distro 无 Leader,脑裂时不会出现 “多个 Leader 各自决策” 的问题。每个分区内的节点仅基于本地数据工作,冲突由后续同步机制自动解决,避免了强一致性协议中 “脑裂导致数据永久不一致” 的风险。

编程 Java 项目