Raft 学习笔记1 - 领导人选举和日志复制
Raft 学习笔记1 - 领导人选举和日志复制
图片来自 John Ousterhout 的 Raft user study 系列课程
Raft 算法的背景是复制状态机,关于复制状态机可以看我写的这篇博客
Paxos 算法存在的问题
Raft 是为了解决 Paxos 算法的这些问题而提出的
- 晦涩难懂
- 论文里描述的 Multi-Paxos 没有说清楚许多实现细节
- 大部分设计者应用 Paxos 时都是理解 Paxos 后,在它的基础上做修改,最后的系统建立在一套没有经过证明的算法之上
因此在设计 Raft 时,作者将可理解性作为一个重要的设计目标
- 问题分解
- Raft 被分解为领导人选举、日志复制、安全性和成员变更几个部分
- 减小状态的数量,简化状态空间
Raft 算法
领导人选举
两种可能的实现共识的手段
- 对称的,无领导人的
- 所有服务器的角色都是相同的
- 客户端可以和任何一个服务器通信
- 非对称的,基于领导人的
- 任何时候都只有一个服务器做决定,其他服务器听从领导人的决策
- 客户端只和领导人通信
Raft 选择了领导人模型
- 将问题分解为两种场景
- 正常运转(Normal Operation)
- 领导人交替
- 简化了稳定情况下的状态,因为不会发生 Paxos 中提议者之间的冲突
- 比对称模型效率更高
任何时候,服务器只能是以下三种角色之一
- 领导人
- 处理客户端请求,负责日志复制
- 任何时候,集群里都只能有一个领导人
- 候选人
- 完全被动的,不主动发起 RPC,只回应收到的 RPC
- 跟随者
- 从中选举出新的领导人
RPC
节点之间通过 RPC 通信
- RequestVote
- 由候选人在选举期间发出
- AppendEntries
- 复制日志
- 如果日志条目内容为空,就作为心跳包使用
任期
Raft 将时间划分为一段段任期
- 每个任期开始都会进行一次选举
- 选举出领导人后就进入正常运转阶段
- 如果某次选举没选举出领导人,该任期立即结束,进入下一个任期,重新进行选举
- 每个节点都会保存当前任期的值
- 作为逻辑时间使用
- 用来分辨出哪些信息是过期的
节点通信的报文都会带上节点的任期号
- 如果节点发现自己的任期号小于收到报文的任期号
- 将自己的任期号更新
- 如果节点发现请求的任期号是过期的
- 直接拒绝这次请求
- 如果候选人或领导人发现自己的任期号过期了
- 转换为跟随者状态
何时开始新一轮选举
- 服务器启动时都是跟随者
- 跟随者希望从领导人或候选人收到 RPC 报文
- 领导人必须定期发送心跳包给所有节点以维持权威,并阻止新选举的发起
- 如果服务器超过 electionTimeout 没有收到 RPC
- 猜测领导人已经宕机
- 开始新的一轮选举
- 一般超时时间是 100-500ms
选举流程
- 增加任期
- 转变为候选人
- 给自己投票
- 给其他所有节点发出 RequestVote RPC,不断重试直到
- 收到大多数选票
- 成为领导人
- 发送 AppendEntries 心跳包给其他所有服务器,以建立起自己的权威
- 从其他领导人那里收到 RPC 且对方的任期号不小于自己的任期号
- 回到追随者状态
- 没有人赢得选举(超过 electionTimeout,没有上面两种情况发生)
- 增加任期,开始新一轮选举
- 收到大多数选票
处理旧的领导人
旧的领导人可能并不是真正的宕机了
- 可能由于网络问题暂时的与集群断开连接,导致集群中的其他节点选出了新的领导人
- 等到旧的领导人重新恢复连接后,会尝试复制日志项并提交
- 但这时他已经不可能成功提交日志项了,因为选举过程会导致多数节点任期号提升
- 等他收到来自其他节点的 RPC 后,就会提升自己的任期号,并转换为追随者
选举算法要求
安全性
每个任期只能有最多一个获胜者
- 每个节点在某个任期内只能投票一次
- 这个状态要持久化到磁盘上,防止崩溃后重启又投票一次
- 投票给第一个发给他 RequestVote 的服务器
- 保证只能有一个候选人获得多数选票
活性
最终总是会有一个候选人胜出
假设大家的超时时间一样,就可能出现,多个候选人瓜分了选票,没能选出领导人,然后所有候选人都超时,在同一时间又都发起新一轮选举,这样又继续选举失败的可能性就会很高
- Raft 的做法是让候选人在
[T, 2T]
之间随机选择超时时间,通常T = electionTimeout
- 这样就能保证有一个节点在其他节点之前超时,并发起选举,这样他赢得选举的几率就很大
- 这种做法在
T >> broadcastTime
的情况下表现得很好- broadcastTime 是指一个服务器并行的发送 RPCs 给集群中的其他服务器并接收响应的平均时间
- 那个最先超时的服务器就能有充足的时间发起投票并从他们那里获得选票
日志复制
日志结构
日志项的结构是 (index, term, command)
- 所有日志的任期是单调递增的
- 命令的结构取决于客户端和集群的协议,与共识模块无关
日志应该被存储在持久化存储中,防止服务器宕机
正常运转状态下的日志复制
- 客户端发送命令给领导人
- 领导人将命令放到自己的日志里
- 发送 AppendEntries RPC 给所有跟随者
- 当多数跟随者确认已经复制成功后,该日志项就被认为是已提交的(committed)
- 领导人将命令提交给他的状态机,并将结果返回给客户端
- 领导人会维护最后一个已提交日志的索引(commitIndex),并将它放到 AppendEntries RPC 中,提醒追随者将已提交的命令提交给状态机
- 如果跟随者崩溃或运行缓慢,领导人会不断重发 AppendEntries 直到所有节点都成功的复制了日志
日志匹配原则
🔑 如果两个日志在某一相同索引位置日志条目的任期号相同,那么我们就认为这两个日志从头到该索引位置之间的内容完全一致
AppendEntries 一致性检查
通过 AppendEntries 报文执行一致性检查
- AppendEntries 报文中会包含前一条日志的索引和任期(prevLogIndex 和 prevLogTerm)
- 跟随者确认了自己上一条日志的索引和任期与 AppendEntries 报文一致的情况下,才有可能接受这条请求,否则会直接拒绝这条请求
领导人交替时的日志复制
当新领导人开始工作时,他可能会面临的情况是集群中留下了很多上一任领导人的遗留日志,比如说某条日志只复制给了集群里的部分节点(没有达到半数),但此时领导人宕机了,对于下一任领导人来说,这些日志就是必须要处理的,如果领导人频繁的宕机,可能导致系统中产生大量的遗留日志
下一任领导人可以选择在上台后,可以选择用一个阶段清理这些遗留日志,但 Raft 并没有选择这样做,理由是如果新领导人上台后,有部分节点正处于故障状态,新领导人就没法在清理阶段中处理故障节点中的遗留记录,除非等到他们重新上线,当然这是不可接受的
Raft 的做法是新领导人上台后直接进入正常运转的状态,因此就需要保证那些已经复制给多数节点的日志不能错误的被覆盖掉,比如下图中的画红框的这部分日志,我们现在描述的 Raft 协议是无法满足这点的,因此要在选举的流程上做一些修改
日志复制的安全性要求
🔑 如果某条日志项已经被提交给状态机了,那么其他节点在提交该日志项给状态机时,不能提交一个不同的值
在 Raft 中要保证日志复制的安全性,就要保证如果某个领导人认为某个日志项是已提交的,那么后续选出的领导人必须是拥有这条日志项的
- 领导人绝不会覆盖他自己日志里的日志项,这就保证了已提交日志项不会被修改
- 这种限制是通过修改选举流程保证的
- 但是选举的时候我们是没法判断哪些日志项是已提交的
- 因此我们希望选出的领导人是最可能拥有所有已提交日志项的候选人
修改后的选举流程:
- 候选人会在 RequestVote RPC 中携带最后一条日志的索引和任期(lastLogIndex 和 lastLogTerm)
- 如果候选人的日志更复杂,那么收到 RequestVote 的节点就会投票给该候选人,否则拒绝投票,日志的复杂度是指
- 候选人的任期比较新
- 任期相同,候选人的最后一条日志索引更大
示例 1 中, S4 和 S5 是无法被选为任期 3 的领导人的,因此前 4 条已提交日志是安全的
示例 2 中,S5 由于某种原因,产生了多条自己的日志,如果 S1 此时宕机,S5 是可以选举成为任期 5 的领导人的,这会导致已提交的日志 3 被覆盖
为了消除示例 2 里的这种情况,Raft 处理先前任期的日志时,并不是采取通过计数(统计已经复制该日志的节点数量)的方式判断它是否已提交,等到提交了一条本任期内的日志,根据日志匹配原则,先前任期里的日志也会间接地被提交
经过这种修改后,示例 2 中,S1 在日志 4 提交之前是不会提交日志 3 的,等到日志 4 被提交了,就保证了 S 不会被选举为领导人,避免了不一致的情况出现
解决领导人变更造成的不一致问题
领导人变更可能导致跟随者的日志和新领导人的日志之间存在不一致的内容
新领导人必须使得跟随者日志内容和自己保持一致
- 填补缺失的日志
- 删除无关的日志
为了领导人会为每个跟随者维护一个 nextIndex 变量
- 也就是下来该将哪一条日志复制给该追随者
- 但是领导人一开始是不知道自己的日志和跟随者的日志之间有什么差异
- 先将所有的 nextIndex 初始化为领导人最后一条日志的索引
- 通过 AppendEntries 一致性检查实现和跟随者的同步,每次一致性检查失败(报文被跟随者拒绝)
- 递减 nextIndex
- 重新发送 AppendEntries 请求
当跟随者覆盖掉某条不一致的日志项后,直接删除掉后续所有的日志项
本文来自博客园,作者:路过的摸鱼侠,转载请注明原文链接:https://www.cnblogs.com/ljx-null/p/15940921.html