文章目录
- Raft算法基本概念和准则
- 节点模型
- 领袖选举
- 领袖任期
- 强领袖
- 日志复制
- 领袖选举
- 心跳和选举定时器
- 选举步骤
- 候选人状态变化
- 日志复制
- 日志结构
- 领袖承担接收同步日志任务
- 日志的复制流程
- 日志提交
- 日志一致性保证
- 日志复制安全性校验
- 日志压缩和快照
- 可用性与时序
- 集群异常情况综合分析
- Follower异常
- Leader异常
Raft算法基本概念和准则
节点模型
一般情况下,分布式系统中存在如下两种节点关系模型
对称节点模型: 所有节点都是平等的 不存在主节点 。 客户端可以与任意节点进行交互 。
非对称节点模型: 基于选主模型,只有主节点拥有决策权 。 任意时刻有且仅有一 个主节点,客户端只与主节点进行交互 。
Raft 算法采用的是非对称节点关系模型 。一共包含如下 3 类角色
Leader (领袖)
Candidate (候选人)
Follower (群众)
领袖选举
如果当前没有领袖,那么有节点超过选举超时时间,就触发选举,一旦某位候选人得到了半数以上群 众的选票,就出任那一届(Term)的领袖
领袖 产生后,就昭告天下,结束选举 。 除领袖之外的所有候选人又都回到了群众的身份 并接受领袖的领导。
领袖任期
每一个任期 的开始都是一次领导人的选举 。
如果一个候选人赢得了选举,那么它就会在该任期的剩余时 间内担任领导人。
如果任期内一次选举没成功,本次任期将以没有选出领导人而结束。待有节点超过选举超时时间,会开始一 次新的选举,任期随之更新
每个 Raft 节点都在本 地 维护一个当前任期值,触发这个数字变化(增加)主要有两个场景:开始选举和与其他节点交换信息 。 当节点之间进行通信时,会相互交换当前的任期号 。以任期号大的节点数据为准。
强领袖
由于分布式系统中节点之间无法做到在任意时刻完全同步, 因 此不同的 Raft 节点可能会在不同的时刻感知到任期的切换 。 甚至在出现网络 分区或节点异常的情况下,某个节点可能会感知不到一次选举或者一个完整的 任期 。 所以 Raft 强制使用较新的 Term 更新旧的 Term
日志复制
在强领袖的模式下,leader负责接收客户端请求,并产生wal日志同步到其他节点,所以raft协议的重点就是解决好领袖选举及日志同步的问题。
领袖选举
Raft 通过选举一个权力至高无上的领导人,来维护节点间复制日志的一致性 。领导人从客户端接收日志条目,再 把 日志条目复制 到其他节点,并且在保证安全性(日志被复制到了大多数节点)的前提下,告诉其他服务 器将日志条目应用到它们的状态机中 。 强领导人的存在大大简化了复制日志的 管理 。
心跳和选举定时器
每个Raft 节点都有一个选举定时器, 所有的 Raft节点最开始以 Follower角色运行时 都会启动这个选举定时器。
Leader 在任期内必须定期向集群内的其他节点广播心跳包,昭告自己的存 在。Follower每次收到心跳包后将自己的选举定时器清零。
如果 Follower选举定时器超时(规定的一个选举超时时间周期内, Leader 没有发给 Follower 心跳包或者客观原因数据包被延迟或被丢弃了), Follower 就认为 Leader 已经不存在,于是会发起一次新的选举。
Leader 广播心跳的周期必须要短于选举定时器的超时时间,否 则会频繁地发生选举
选举步骤
如果一个 Follower 决定开始参加选举,那么它会执行如下步骤 :
1 )将自己本地维护的当前任期号( current term id )加 l 。
2 )将自己的状态切换到候选人( Candidate ),并为自己投票 。每 个候选人的第一张选票来自于他自己 。
3 )向其所在集群中的其他节点发送 RequestVote RPC ( RPC 消息会携带 “ current term id ”值),要求它们投票给自己
每人一票先到先得
同一任期,一个 Raft 节点最多只能为一个候选 人投票,按照先到先得的原则,投给最早来拉选票 的候选人
候选人状态变化
一个候选人有三种状态迁移的可能性
1 )得到大多数节点的选票,成为 Leader 。
2 )发现其他节点赢得了选举,主动切换回 Follower。
3 )过了一段时间后,发现没有人赢得选举,重新发起一次选举
收到心跳信息:
当一个候选人在等待其他人的选票时,收到来自其他节点的心跳包。这个候 选人会对比心跳包和自己保存的两个任期号,如果心跳包中的大,则承认该领导人合法,并且主动将自己的状态切换回Follower ;反之,认为该“领导人”不合法,拒绝此次 RPC心跳包 ,并且返 回当前较新的那个任期号,以便让“领导人”意识到自己的任期号已经过时了,该节点将继续保持候选人状态不变。
重新发起选举:
如果多个 Follower 在同一时刻都成了候选人,那么选票可能会被多个候选人平分,没人得票过半,就会选举失败。此时每个候选人自增任期号( Term++)并且发起新 一 轮的拉选票活动。
收到投票邀请:
接收方处理逻辑如下:
1 )如果 term < currentTerm ,即 RPC 包中任期 term 的值小于接收方本 地维护的 term ( currentTerm )值, 则返 回( currentTerm, false ),提醒调用方 其 term 过时了, 并且明确地告诉这位候选人这张选票不会投给他;否则执行如下逻辑
2 )如果之前没把选票投给任何人(包括自己)或者已经把选票投给当前候 选人了,并且候选人的日志和自己的日志一样新,则返回( term, true ),表示在 这个任期,选票都投给这位候选人 。 如果之前已 经把选票投给其他人了,那么,返回 ( term, false )
日志复制
日志结构
日志由有序编号的日志条目组成 。 每一个日志条目一般均包 含三个属性 : 整数索引( log index )、 任期号( term )和指令( command ) 。 每个 条目所包含的整数索引即该条目在日志文件中的槽位 。 term 是指日志被领导人创 建时所在的任期号,对应到图 中就是每个方块中的数字,用于检测在不同 的服务器上日志的不一致性问题。 指令即用于被状态机执行的外部命令(对应 到图中就是 x ← 3,y ← l 等) 。
领袖承担接收同步日志任务
一旦某个领导人赢得了选举, 那么它就会开始接收客户端的请求。领导人将把这条指 令作为一条新的日志条目加入它的日志文件中,
然后并行地向其他 Raft 节点发 起 AppendEntries RPC ,要求其他节点复制这个日志条目 。
当这个日志条目被 “安全”地复制(完成半数以上节点的复制)之后, Leader 会将 这条日志应用( apply ,即执行该指令)到它的状态机中,然后向客户端返回执 行结果,并通知其他节点应用此条日志。
如果 Follower 发生错误,运行缓慢没有及时响应 AppendEntries RPC, 或者发生了网络丢包的问题,那么领导人会无限地重试 AppendEntries RPC ,直到所有的追随者最终存储了和 Leader 一样的日 志条目 。
只有日志条目被复制到半数以上的节点上,这个条 目才能被提交 。 上图 中的 7 号条目在其中 3 个节点( 一共是 5 个节点)上均有复制,所以 7 号条目是可被提交的;但条目 8 只在其中 2 个节 点上有复制,因此 8 号条目不是可被提交的 。
日志的复制流程
1 )客户端向 Leader发送写请求。
2) Leader 将写请求解析成操作指令追加到本地日志文件中。
3) Leader 为每个 Follower广播 AppendEntries RPC。
4) Follower通过一致性检查,选择从哪个位置开始追加 Leader 的日志条目。
5 )一旦日志项完成半数以上节点的复制, Leader就将该日志条目对应的指令应用(apply) 到本地状态机,并向客户端返回操作结果,表示数据提交成功。
6) Leader 后续通过 AppendEntries RPC将已经成功(在大多数节点上)提 交的日志项告知 Follower。
7)Follower 收到提交的日志项之后,将其应用至本地状态机。
Raft 日志条目有两个操作,提交( commit)和应用( apply ),应用必须发生在提交之后,即某个日志条目只有被提交之后才 能被应用到本地状态、机上。
日志提交
领导人记录被提交日志条目的最大索引值,并将这个索引值包含在 AppendEntries RPC 中, 并且这个索引值会包含在他向其他节点发送的 AppendEntries RPC 包中,其他节点也能通过领导人的心跳包获悉某个日志条目的提交情况 。 一旦 Follower 得知某个日志条目已经 被提交,那么它会将该条日志应用至本地的状态机
日志一致性保证
Raft算法中,约定如果在不同节点的两个日志条目有着相同的索引和任期号,则它们所存储 的命令是相同的 ,且它们之前的 所有条目都是完全一样 的。如何保证这一点?
1、领导人在一个任期里在给定的一个日志索引位 置上最多创建一条日志条目,同时该条目在日志文件中的槽位永远也不会改变
2、AppendEntries RPC 有一个简单的一致性检查。 领导人在发送一个 AppendEntries RPC 消息同步日志条目时,会把这些新日志条目之前一个槽位的日志条目的任期号和索引位置带上。 Follower 在它的 日志文件中没有找到相同的任期号和索引的日志, 它就会拒绝该 AppendEntries RPC 。 保证不会随便加日志。具体过程如下:
Follower一致性检查过程
当一个新的 Leader 被选举 出 来时,它的日志与其他的 Follower 的日志可 能是不一样 的 。如下图
a 和 b 表示 Follower 丢失一些日志条目 的场景 。
c 和 d 表示 Follower 可能多 出来一些未提交的条目的场景 。
e 和 f 表示上述两种情况都有的场景。
在 Raft算法中,Leader通过强制 Follower 复制它的日志来处理日志不一致的问题。Follower上的冲突日志会被覆盖。
Leader会找到Follower 与它的日志条目不一致的位置。然后让它删除之后的冲突日志。如何查找?
Leader会为每一个 Follower 维护了一个 nextlndex ,它表示领导人将要发送给该Follower的下一条日志条目的索引。当一个Leader 赢得选举时,它会假设每个 Follower 上的日志都与自己的保持一致,于是先将 nextlndex 初始化为它最新 的日志条目索引数+l 。Leader发送 AppendEntries RPC 时,它携带了( term_id,next-Index-I )二元组信息, termid 即nextindex-1这个槽位的日志条目的 term。
Follower接收到 AppendEntriesRPC 消息后, 会进行一致性检查, 搜索自己的日志文件中是否存在这样的日志条目,如果不存在,就向Leader 返回 AppendEntries RPC 失 败。这就意味着Follower 发 现自己的日志与领导人的不 一 致。在失败之后,领导人会将nextlndex 递减 ( nextlndex -- ),然后 重 试 AppendEntriesRPC,直到A ppendEntriesRPC 返回成功为止。这才表明在nextlndex 位置的日志条目中领导人与追随者的 保持一致。
Follower上 nextlndex 位 置之前的日志条目将全部保留,之后(与 Leader 有冲突)的日志条目全部删除,并且从 该位置起追加 Leader 上在 nextlndex 位置之后的所有日志条目。
日志复制安全性校验
Raft 算法是强领导人模型, 因此必须保证被选为leader的节点的日志全面、正确,且已提交记录不会被覆盖。
保证新Leader上包含所有已提交日志
Raft算法在选举过程中比较日志文件 中 最后一个条目 的索引和任期 号,节点不给索引号比自己低的人投票,保证只有索引值更大的节点才能被选为领袖,如果一个节点被选为Leader表明它的日志比集群中过半的节点都要高。而且leader同步日志时只有过半节点已同步才会提交,这就可以保证,之前任期已 提交的所有日志条目都已经在Leader上了。
保证已提交的日志不会被覆盖
一条已经被复制到过半节点的日志条目,也依然 有可能会被未来的领导人覆盖掉。如下图:
时刻 a, Sl 是任期 2 的领导人并且向部分节点( s 1 和 S2 )复制了 2 号位置的日志条目,然后宕机。
时刻 b, S5 获得了 S3 、 S4 和自己的选票赢得了选举,成了 3 号任期的领导人,并且在 2 号位置上写人了一条任期号为 3 的日志条目 。 在新日志条目复制到其他节点之 前, S5 若机了
时刻 c,Sl 重启,并且通过 S2、S3、S4和自己的选票赢得了选举,成了 4号任期的领导人,并且继续向S3 复制2号位置的日志。此时,任期2的日志 条目已经在大多数节点上完成了复制,并提交。
时刻d,SI 发生故障, S5 通过 S2、S3、的选票再次成为领导人,任期号为5 。然后 S5用自己的本地日志覆盖了其他节点上的日志。
即使日志条目被半数以上的节点写盘(复制) 了,也并不代表它已经被提交( commited )到Raft 集群了,因为一旦某条日 志被提交,那么它将永远没法被删除或修改。
因此领导人 无法单纯地依靠之前任期的日志条目信息判断它的提交状态,还要增加一个限制:Raft算法要求 Leader在当前任期至少有一条日志被提交,即被超过半数 的节点写盘。(否则当前任期不会被增加到下一任期?)
如上图 中e描述的那样,Sl 作为 Leader ,在崩溃之前,将3号位置 的日志(任期号为4)在大多数节点上复制了一条日志条目,那么即使这时·Sl 若机了, S5 也不可能赢得选举。这样已提交的日志就不会被覆盖。
日志压缩和快照
Raft 节点上的日志记录不可能无限制地增 加下 去 。 一方 面目志记录会对节点的存储空间造成压力,另一方面当 Raft 节点重启 时需要花 费大量 的时间进行日志回放( rep lay ),进而影 响 系统 的可用性 。Raft使用快照进行日志压缩,系统的全部状态都以快照的形式写入持久化存储 , 然后删 除那个时间点之前的全部日志 ,如下图
为了支持快照后第一个日志条目的 AppendEntries RPC 一致性检查,Raft 快照元数据中会存储被快照取代的最后一个日志条目的索引位置和对 应的任期号。
一般Raft 节点都是独 立 创建快照的,但是 Leader 偶尔也需要向一些落后的 Follower 发送快照。这种情况通常发生在 Leader因为做快照删除了还未发送给 Follower 的日志条目的情况下。
Raft 算法的 InstallSnapshotRPC 实现了 Leader 和 Follower 之间发送和接收快照文件的过程。当有些快照文件过大时,需要对其进行分块传输。
可用性与时序
领导人选取是 Raft 算法中对时序要求最多的地方。 只有当系统环境满足以 下时序要求时, Raft 算法才能选举并且保持一个稳定的领导人存在:
broadcastTime << electionTimeout << MTBF
broadcastTime 指的是 一 个节点向集群中其他节点发送 RPC ,并且收到它们响应的平均时间 (一般几毫秒-几十毫秒),最好比electionTimeout 小一个数量级
electionTimeout 就是在上文中多次出现 的选举超时时间 (一般几十到几百毫秒) 最好比MTBF 少一个数量级
MTBF 指的是单个节点发生故障的平均时间间隔 一般按月年来计算,远大于electionTimeout
因此由上得出只要合理配置electionTimeout 就可以满足时序要求.
集群异常情况综合分析
Raft 集群的异常情况可以分为两大类:领导人异常和追随者/候 选人异常 。
Follower异常
如果一个追 随者或者候选人崩溃了,那么领导人在这之后发送给他们的 RequestVote RPC 和 AppendEntries RPC 就会失败 。 Raft 算法通过领导人无限的重试来应对这些失败, 直到故障的节点重启并处理了这些 RPC 为止 。 如果一个节点在收到 RPC 之后 但在响应之前就崩溃了,那么它会在重启之后再次收到同一个 RPC 。 因为 Raft 算法中 的 RPC 都是幕等的,因 此不会有什么问题 。
Leader异常
Raft协议数据提交的全过程,如下图:
数据的流向只能从 Leader 节点( L) 向 Follower 节点( F)转移。当 Client 向 集群 Leader节点提交数据时, Leader 节点接收到的数据处于未 提交状态( Uncommitted),接着 Leader节点会并发向所有 Follower 节点复制数据并等待接收响应,在确保集群中至少有超过半数的节点已经接收到数据之后, 再向 Client 确认数据已接收。一旦Leader节 点向 C lient 发出数据接收ACK 响应之后, 即 表明此时数据状态进入已提交( Committed )状态, Leader节点会再次向 Follower 节点发送通知,告知该数据状态已提交。
领导人可能会在任意阶段崩溃,下面将逐一示范 Raft算法 在各个场景下是如何保障数据一致性的
client数据到达 Leader 前
client数据提交失败,重新提交,不会影响一致性。
数据到达 Leader 节点,但未复制到 Follower节点
如果在这个阶段 Leader 出现故障,此时数据属于未提交状态,那么 Client 不会收到 ACK ,会认为超时失败,然后发起重试。Follower 节点上没有该 数据,重新选主后Client 重试重新提交可成功。原来的 Leader 节点恢复之后将 作为Follower 加人集群,重新从当前任期的新 Leader处同步数据,与Leader 数据强制保持一致。
数据到达 Leader 节点,成功复制到 Follower 的部分节点上
时数据在 Follower 节点处于未提交状 态( Uncommi忧ed )且不一致,那么 Raft 协议要求投票只能投给拥有最新数据 的节点 。 所以拥有最新数据的节点会被选为 Leader ,再将数据强制同步到 Follower ,数据不会丢失并且能够保证最终一致
数据到这 Leader 节点,成功复制到 Follower 的所有节点上,但还未 向 Leader 晌应接收
在这个阶段 Leader 出现故障,虽然此时数据在 Follower 节点处于未提 交状态( Uncommitted ),但也能保持一致,那么重新选出 Leader 后即可完成数 据提交,由于此时客户端不知到底有没有提交成功,因此可重试提交。 针对这 种情况, Raft 要求 RPC 请求实现幕等’性, 完成重试保持一致。
数据到达 Leader 节点,成功复制到 Follower的所有或大多数节点 上,数据在 Leader 上处于己提交状态,但在 Follower 上处于未提交状态
用户收到了ack 因此不会重试,Follower重新选主后,自动保持数据一致。
数据到达Leader 节点,成功复制到Follower的所有或大多数节点 上,数据在所有节点都处于己提交状态,但还未 响应 Client
如果在这个阶段 Leader 出现故障,此时集群内部数据其实已经是一致的, 那么 Client 重复重试基于幕等策 略对一致性无影响 。
网络分区导致的脑裂情况,出现双 Leader
网络分区将原先的 Leader 节点和 Fo llower 节点分隔开, Follower 收 不到 Leader 的心跳将发起选 举产生新 的 Leader。 这时就产生了双 Leader , 原先的 Leader 独自在一个区,向它提交数据不可能复制到大 多数节点上,所以永远都 是提交不成功 (client永远都超时)。 向新的 Leader 提交数据可以提交成功,网络恢复后旧的 Leader 发现集群中有更新任期( Term )的新 Leader , 则 自动降级为 Follower 并从新 Leader 处同 步数据达成集群数据一致。