Nacos源码学习计划-Day12-集群-CAP原则和Raft协议和Distro协议理论前提
ZealSinger 发布于 阅读:92 技术文档
那么对于分布式集群服务的数据一致性的,自然就逃脱不了CAP理论,根据CAP理论,一个系统只能在同一时刻为CP系统或者AP系统,这是因为CA两者是无法共存的。Nacos作为一个AP和CP双特性的系统,能作到AP和CP模式的切换,这个就和上述说到的Nacos内核中对于数据一致性协议的设计了,Nacos底层通过Raft协议(实现CP特性)和Distro协议(实现AP特性)会在单个集群中同时运行 CP 协议以及 AP 协议
CAP理论
CAP理论的简单理解就是由C A P 三个特性
-
C Consistency 一致性,集群中各个节点中的数据保持一致
-
A Availability 可用性,集群环境中,每个请求都能在规定的时间内获得合理的响应
-
P Partition Tolerance 分区一致性,当整个集群内部网络出现问题,整个集群依旧能对外提供服务
从上面三个特性中可以看到,其中分区一致性,因为网络环境的不稳定,集群内部出现问题肯定是有几率发生的,所以解决分区一致性问题,成为了分布式集群系统必须解决的问题。
那么当集群中出现分区问题:例如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协议中,为每个集群节点定义了三种状态
-
Follower 追随者:默认状态,集群中的节点一开始默认都处于Follower状态
-
Candidate 候选者:当某集群节点开始发其投票选举Leader的时候,首先会投票给自己,这个时候就会从Follower状态变化为Candidate状态。每个节点都可以成为Candidate,并且只有处于Condidate状态的节点才具备被选举权。
-
Leader 领导者:当集群中某个节点获得了大多数票选之后,就会晋升为Leader状态。同一时刻只能由一个Leader,负责协调和管理集群中的其他节点。
在Raft协议中,只有Leader节点有权利处理客户端数据的请求,如果不是Leader的节点但收到了请求,也会转移到Leader节点上进行数据处理,Leader 节点和 Follower 节点之间会有心跳机制(AppendEntries RPC 携带心跳包or数据包)
数据同步/读写流程
数据同步流程大致如下:数据的写入一共有两个状态:uncommit、commit
-
第一步:日志追加(uncommit)Leader 接收客户端请求后,先将请求封装为日志条目,追加到自己的日志中(此时状态为
uncommit),然后通过AppendEntries RPC向所有 Follower 同步该日志。 -
第二步:多数确认后提交(commit)当 Leader 收到多数 Follower(超过半数)的 “日志已复制” 确认后,会将该日志条目标记为
commit,并应用到自己的状态机(执行日志对应的操作)。 -
第三步:通知 Follower 提交Leader 在后续的
AppendEntries RPC(可能是心跳,也可能是新日志同步)中,会携带自己的 “commit 索引”(已提交日志的最大索引)。Follower 收到后,会将自己日志中 “索引≤commit 索引” 且未提交的日志标记为commit,并应用到状态机。
若部分 Follower 未成功复制日志(如网络故障、节点宕机),Raft 通过以下机制保证最终一致:
-
Leader 会持续重试向这些 Follower 发送
AppendEntries RPC(包含缺失的日志条目),直到成功复制(即使这些节点暂时离线,重启后 Leader 也会根据其日志状态同步缺失数据)。 -
由于 Leader 已通过多数派确认提交了日志,即使少数节点暂时不一致,也不影响 “已提交日志的一致性”(客户端已收到成功响应),后续通过重试同步即可让所有节点最终一致。
-
如果Follower迟迟没有收到AppendEntries RPC心跳包,则会认为Leader节点宕机从而将自己状态修改为Candidate并且开启重新选举Leader
Raft 的核心是 “Leader 日志复制→多数确认提交→通过索引同步所有节点”,通过 “任期号 + 日志索引” 和 “持续重试” 保证一致性
选举Leader流程
Raft协议选举流程的实现有点类似我们生活中的投票活动,遵循少数服从多数的原则
Raft 初始化时,所有节点均为 Follower,各自维护一个随机的 Election Timeout(称之为选举超时 150-300ms)。超时后,节点转为 Candidate,先给自己投一票,再向其他节点发送RequestVote RPC请求投票。这一随机机制是为了避免多个节点同时超时,导致 “分裂投票”(Split Vote)。
例如当前集群中存在ABC三个节点,其中C的Election Timeout最少,那么即C最先转化为Candidate状态并且给自己投一票,然后给AB发送RequestVote RPC请求投票
-
如果AB此时还没结束自己的
Election Timeout,那么会立马被打断,重置计时器,当前选举不再被触发,处理请求后就会选举一个Leader,之后接收到的都是Leader的心跳,也会重置计时器保证本轮次中不会再触发选举 -
如果AB此时已经结束了自己的
Election Timeout,因为结束Election Timeout后会给自己投一票,在一个完整的选举过程中每个节点只会允许投一票,所以会拒绝C的投票请求
当接收到了某个节点的RequestVote RPC之后,会根据如下两个决策进行投票
-
自己在当前任期(Term=1)是否还没投过票(Raft 规定一个节点在一个任期内只能投一次票,避免重复投票);
-
C 的日志是否 “至少和自己一样新”(即 C 的最后日志任期≥自己的,或任期相同则索引≥自己的,确保 Leader 的日志是最完整的)
当节点收获超过半数的投票,则会晋升为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 节点。

Nacos实现AP特性-Distro协议
Distro 协议是 Nacos 社区自研的⼀种 AP 分布式协议,是面向临时实例设计的⼀种分布式协议,其保证了在某些 Nacos 节点宕机后,整个临时实例处理系统依旧可以正常工作。作为⼀种有状态的中间件应用的内嵌协议,Distro 保证了各个 Nacos 节点对于海量注册请求的统⼀协调和存储。
对于Distro协议的相关的数据同步等操作,我们之前的文章都是按照临时实例进行分析的,其实我们前面的集群新增节点数据同步,节点健康状态变更同步的逻辑,其实就很好体现了Distro协议的设计。
数据读写 / 同步流程:分片存储 + 异步收敛
Distro 协议针对服务发现中的非持久化实例(临时实例,依赖客户端心跳续约)设计,数据存储在内存中,核心流程围绕 “分片管理 + 异步同步” 展开。
1. 数据分片:节点自主管理部分数据
-
分片规则:Distro 通过哈希算法(默认对服务名哈希)将服务实例数据分片,每个节点负责特定分片的 “主节点”(即该分片的读写主要由该节点处理)。
例:集群有 3 个节点(N1、N2、N3),服务名 “S1” 哈希后分配给 N2,则 N2 是 “S1” 的主节点,客户端对 “S1” 的实例注册 / 注销请求会优先路由到 N2。
-
优势:避免单节点负载过高,支持动态扩缩容(新增节点时重新分片,旧节点将部分数据迁移给新节点)。
2. 写入流程:本地优先,异步扩散
客户端写入(如注册实例)时:
-
路由到主节点:客户端通过哈希计算找到服务对应的主节点,发送写入请求(HTTP/GRPC)。
-
本地写入:主节点收到请求后,先写入本地内存注册表(无需等待其他节点确认),立即返回成功给客户端(保证低延迟)。
-
异步批量同步:主节点将数据变更放入本地队列,定期(默认 500ms)批量同步给集群其他节点(非主节点),同步方式为 “增量推送”(仅发送变更数据)。
3. 读取流程:本地读取,容忍短暂不一致
客户端读取(如查询服务实例列表)时:
-
可向任意节点发起请求(无需路由到主节点),节点直接返回本地内存中的数据。
-
若本地数据与主节点存在短暂差异(如同步尚未完成),客户端可能读到旧数据,但后续通过异步同步会自动修正(最终一致)。
这个地方我们在实例查询相关的文章中说过一点,新版本中Nacos客户端查询的时候会直接查询的当前节点的最新数据而不是客户端本地的缓存,所以不会出现差异。但是这里我们又说会出现,需要注意这里是两个部分
客户端X ---- 集群服务A ----- 集群其余节点other
A中会包集群中其余节点的信息,X和A之间不出意外的话是不会差异的,但A和集群其余节点间的数据同步需要等待别的节点发起同步请求,所以A和other之间是会存在短暂的差异的,需要靠异步任务进行核对与同步,所以这里才说会有短暂的差异
4. 数据同步与收敛:Gossip + 定期校验
为保证最终一致性,Distro 设计了两层同步机制:
-
实时异步同步:主节点数据变更后,通过异步批量推送扩散到其他节点(非阻塞,不影响主流程)。
-
定期全量校验:所有节点定期(默认 10 秒)向其他节点发送本地数据的 “校验和(Checksum)”,若发现某节点的 Checksum 与本地不一致,触发增量同步(拉取差异数据)。
这里其实就是节点变更同步那几节的内容 -
故障恢复同步:节点重启或新增时,通过
DistroLoadDataTask从任意一个健康节点同步全量数据快照(只需同步一个节点,快速完成初始化,后续通过校验机制补齐差异)。
选举流程:无 “Leader” 设计,节点平等
Distro 协议是去中心化的,不存在 “Leader” 角色,因此没有传统意义上的 “选举流程”。所有节点地位平等,均可独立处理读写请求,核心原因如下:
-
数据分片自治:每个节点仅负责部分分片的主节点工作,无需全局领导者协调。
-
无统一决策依赖:写入操作由主节点本地处理后异步扩散,无需多数派确认,因此不需要 “Leader” 统一协调投票。
-
故障自动容错:若某个节点(如分片主节点)宕机,客户端会通过重试路由到其他节点(其他节点会临时接管该分片的读写,待原主节点恢复后自动同步数据)。
脑裂问题处理:分区容忍优先,数据自动合并
脑裂(网络分区)是分布式系统中集群被分割为多个独立子网的场景。Distro 协议针对脑裂的处理完全体现 “AP 特性”:
1. 分区内可用:优先保证可用性
当集群分裂为多个分区(如 P1、P2):
-
每个分区内的节点仍可独立处理读写请求(无需跨分区通信)。例如,P1 中的节点继续接收客户端的实例注册,写入本地内存并在 P1 内同步;P2 中的节点同样正常工作。
-
此时不同分区的数据可能出现不一致(如 P1 新增了实例,P2 未收到),同区域内的调用不受银影响,但是跨区调用会受到影响(也是不能确保强一致性造成的)
2. 分区恢复后:数据自动合并收敛
当网络恢复,分区重新合并:
-
节点通过定期 Checksum 校验发现跨分区的数据差异,触发全量 / 增量同步。
-
对于冲突数据(如同一实例在 P1 被注销、在 P2 被注册),Distro 通过 “时间戳” 或 “版本号” 解决冲突(保留最新操作),最终所有节点数据达成一致。
3. 为何无需担心 “多 Leader 冲突”?
文章标题:Nacos源码学习计划-Day12-集群-CAP原则和Raft协议和Distro协议理论前提
文章链接:https://www.zealsinger.xyz/?post=37
本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明来自ZealSinger !
如果觉得文章对您有用,请随意打赏。
您的支持是我们继续创作的动力!
微信扫一扫
支付宝扫一扫