Raft 论文阅读
参考:
Raft Paper
一文搞懂Raft算法 - xybaby
Raft Demo
Raft 实现汇总
Raft 为什么是更易理解的分布式一致性算法
空挡)
Raft协议实现之etcd(一):基本架构
Raft 协议 - buttercup
【raft】学习二:etcd/raft raft数据结构学习_从未想放弃的博客-CSDN博客_raft readstates
Raft协议详解 - 知乎 (zhihu.com)
Raft协议原理详解 - 知乎 (zhihu.com)
0. Abstract
Raft 是用来进行管理replicated log的共识协议。他能产生和paxos一样的效果和效率。之所以更加容易理解,是将共识的关键元素(问题)进行了分解以及进行了状态的简化。
1. Intro
Raft协议进行了decomposition 分解如下,其中前两个是子问题。
- Leader election 领导者选举
- Log replication 日志复制
- safety 安全问题
进行了状态空间的简化如下,任何一个节点在任意时间只能处于以下三个状态中的一个:
- leader 领导者
- follower 追随者
- candidate 候选者
Raft 协议有以下三个特征:
- Strong leader :即Raft使用比其他共识算法更强的领导形式,例如,日志条目仅从leader流向其他服务器。
- Leader election :使用随机的timeout,仅仅增加少量的代价,来实现快速的选举(解决冲突)。
- Memberhship changes :两个集群合并的时候,即在成员更改的期间,集群正常运行?
最后,概括raft工作过程:raft会先选举出leader,leader完全负责replicated log的管理。leader负责接受所有客户端更新请求,然后复制到follower节点,并在“安全”的时候执行这些请求。如果leader故障,followes会重新选举出新的leader。
2. Replicated state machines
复制状态机。共识算法的实现一般是基于复制状态机。 简单来说:相同的初识状态 + 相同的输入 = 相同的结束状态。即:每个节点存储包含一系列命令的日志,然后状态机按照顺序执行这些命令,每个日志保存相同顺序的相同命令,每个状态机执行命令序列后,状态机的最终状态和输出序列都是相同的。
如何保证所有节点 get the same inputs in the same order
,使用replicated log是一个很不错的注意,log具有持久化、保序的特点,是大多数分布式系统的基石。
下图的解释:
- client 向服务器发起请求或者命令
- leader将客户端请求(command)封装到一个个log entry,并且将这些log entries复制(replicate)到所有follower节点
- 节点按相同顺序应用(apply)log entry中的command,得到一致的状态。
- 返回给客户端一致的正确结果
用于实际系统的共识算法通常具有以下特性:
- 确保在所有非拜占庭条件下的安全性(永远不会返回不正确的结果),包括网络延迟、分区和数据包丢失、复制和重新排序。
- 主要大多数机器不宕机,整个集群就具备可用性(available)。
- 不依赖于(物理)时间来确保日志的一致性
- 只要大多数集群响应了一轮RPC,命令就可以完成;
3. Paxos的优缺点
优点:Paxos确保了安全性和活跃性,并且支持集群成员关系的更改。该方法已被证明是正确的,在正常情况下是有效的。
缺点:第一个缺点是Paxos非常难以理解。在多个决策上达成共识的总体问题可以用其他更直接和明显的方式分解。Paxos的第二个缺点是它没有为构建实用系统提供良好的基础。
4. Design for understandability
设计raft的目标:必须为系统构建提供一个完整而实用的基础;必须在所有条件下都是安全的,并在典型的操作条件下可用;最重要的目标—方便理解。
用到了上面说的问题分解和状态简化。
5. The Raft consensus algorithm
Raft通过首先选举一个杰出的领导者来实现共识,然后让领导者完全负责管理复制的日志。leader从客户端接收日志条目,在其他服务器上复制它们,并告诉服务器什么时候将日志条目应用到它们的状态机是安全的。有一个leader可以简化对复制日志的管理。例如,领导者可以在不咨询其他服务器的情况下决定在日志中放置新条目的位置,数据以简单的方式从领导者流向其他服务器。leader可能出现故障或与其他服务器断开连接,在这种情况下,将选出新的leader。
基于leader方法,Raft将共识问题分解为三个相对独立的子问题:
- Leader election:当现有的领导者失败时,必须选择一个新的领导者
- Log replication:leader必须接受来自客户端的日志条目,并在集群中复制它们,迫使其他日志与它自己的日志一致
- Safety:首先能够保证状态机安全。raft还能保证以下属性正确:
- Election Safety: 一个任期内最多只能选出一名领导人
- Leader Append-Only: 领导者从不覆盖或删除日志中的条目;它只追加新条目。
- Log Matching: 如果两个日志包含具有相同索引和项的条目,那么在给定索引之前的所有条目中日志都是相同的。
- Leader Completeness: 如果在给定任期中提交了一个日志条目,那么该条目将出现在所有高编号任期的前导日志中
- State Machine Safety:如果某个服务器将给定索引上的日志条目应用到其状态机,那么其他服务器将不会对同一索引应用不同的日志条目。(意思就是说 一条日志一旦在任意服务器apply,在整个集群中他就是提交了的)。
5.1 Raft basics
状态转换
follower只响应来自其他服务器的请求。如果一个follower没有收到任何heartbeat,它就成为一个candidate并发起选举。获得整个选区多数票的candidate成为新的leader。leader通常会一直运作到失败为止。
可以看出所有节点启动时都是follower状态;在一段时间内如果没有收到来自leader的心跳,从follower切换到candidate,发起选举;如果收到majority票(含自己的一票)则切换到leader状态;如果发现其他节点比自己更新,则主动切换到follower。
总之,系统中最多只有一个leader,如果在一段时间里发现没有leader,则大家通过选举-投票选出leader。leader会不停的给follower发心跳消息,表明自己的存活状态。如果leader故障,那么follower会转换成candidate,重新选出leader。
Term
Raft将时间划分为任意长度的term。
term(任期)以选举(election)开始,然后就是一段或长或短的稳定工作期(normal Operation)。从上图可以看到,任期是递增的,这就充当了逻辑时钟的作用(如果候选人或领导人发现自己的任期已经过期,就会立即恢复到追随者状态。如果服务器接收到带有过期术语号的请求,它将拒绝该请求);另外,term 3展示了一种情况(split vote),就是说没有选举出leader就结束了,然后会发起新的选举。
RPC
Raft服务器通过远程过程调用(rpc)进行通信,基本的共识算法只需要两种类型的rpc:
- RequestVote RPC:候选人在选举期间发起
- AppendEntries RPC:领导者发起,用于复制日志条目并提供一种形式的心跳
后面还有用于传输快照的RPC,到时再说。
5.2 Leader election
Raft使用心跳机制(heartbeat mechanism)来触发领袖选举。当服务器启动时,它们开始都是追随者。只要服务器从领导者或候选人接收到有效的rpc,它就保持在追随者状态。领导者定期向所有追随者发送心跳(AppendEntries rpc,不携带日志条目),以维护他们的权威。如果一个追随者在一段称为选举超时的时间内没有收到任何通信,那么它就认为没有可行的领导者,并开始选举一个新的领导者。
具体选举过程如下:
- 要开始选举,follower增加自己的current term,切换到candidiate状态
- 给自己投票,并且发给其他服务器 RequestVote RPCs
- 等待其他节点的回复,节点将会一直处于候选人状态,直到产生如下三种结果之一
结果:
- 收到majority(majority voting即投票结果多数,超半数同意规则)的投票(含自己的一票),则赢得选举,成为leader。
- 被告知别人已当选,自行切换到follower。(因为 第一种情况,新的leader赢得了选举之后,会立刻给所有节点发消息,避免其余节点触发新的选举)
- 一段时间内没有收到majority投票,则保持candidate状态,重新发出选举
投票者是否投票,有以下约束:
- 在任一任期内,单个节点最多只能投一票
- 候选人知道的信息不能比自己的少(term要大于本地自己的term)
- first-come-first-served
第二种结果怎么产生的呢?比如有三个节点A B C。A B同时发起选举,而A的选举消息先到达C,C给A投了一票,当B的消息到达C时,已经不能满足上面提到的第一个约束,即C不会给B投票,而A和B显然都不会给对方投票。A胜出之后,会给B,C发心跳消息,节点B发现节点A的term不低于自己的term,知道有已经有Leader了,于是转换成follower。
第三种呢?split vote
BC 同时称为候选者,进入term4,然后ac分别投了BD一票,出现了平票的现象,直到超时后重新发起选举。
解决:raft引入了randomized election timeouts(150 - 300ms)来尽量避免平票情况。同时,leader-based 共识算法中,节点的数目都是奇数个,尽量保证majority的出现。
randomized election timeouts的好处除了上述,还有就是大多数情况下只有一台服务器会超时来选举。
5.3 Log replication 日志复制
选举完了leader,就要开始服务了。
请求流程如下:
Leader收到一个来自client的写请求,到返回给client,整个过程从leader的视角来看会经历以下步骤:
- leader append log entry. leader将该命令作为一个新条目附加到其日志中
- leader issue AppendEntries RPC in parallel,并行地向每个其他服务器发出AppendEntries rpc以复制该条目
- leader wait for majority response,回应收到log
- leader apply entry to state machine,leader将该条目应用于其状态机
- leader reply to client,将执行结果返回给客户机。
- leader notify follower apply log,一旦向客户端返回成功消息,那么系统就必须保证log(其实是log所包含的command)在任何异常的情况下都不会发生回滚。
commit(committed),apply(applied),前者是指日志被复制到了大多数节点后日志的状态;而后者则是节点将日志应用到状态机,真正影响到节点状态。
类似两阶段提交(2PC),不过与2PC的区别在于,leader只需要大多数(majority)节点的回复即可,这样只要超过一半节点处于工作状态则系统就是可用的。
日志在每个节点上,如下图。
Logs由顺序编号的log entry组成 ,每个log entry除了包含command,还包含产生该log entry时的leader term。五个节点的日志并不完全一致,raft算法为了保证高可用,并不是强一致性,而是最终一致性,leader会不断尝试给follower发log entries,直到所有节点的log entries都相同。
Log matching
如果两个节点上的某个log entry的log index相同且term相同,那么在该index之前的所有log entry应该都是相同的。
- 如果不同log中的两个entry具有相同的index和term,那么它们存储相同的命令。
- 如果不同log中的两个entry具有相同的index和term,则前面所有log都是相同的。
怎么做到的呢?
首先,leader在某一term的任一位置只会创建一个log entry,且log entry是append-only。
其次,consistency check。leader在AppendEntries中包含最新log entry之前的一个log 的term和index,如果follower在对应的term index找不到日志,那么就会告知leader不一致。
正常情况下可以保证一致性检查不会失败。但是如果leader崩溃可能会导致日志不一致。
某个follower可能会处于以下六个状态,leader、follower都可能crash,那么follower维护的日志与leader相比可能出现以下情况:
-
比leader日志少,如上图中的ab
-
比leader日志多,如上图中的cd
-
某些位置比leader多,某些日志比leader少,如ef
当出现了leader与follower不一致的情况,leader通过强制追随者的日志复制自己的日志来处理不一致。
leader会维护一个nextIndex[]数组,记录了leader可以发送每一个follower的log index,初始化为leader的最后一个log index加1,图7中就是10+1。
- leader 初始化nextIndex[x]为 leader最后一个log index + 1,即10+1
- AppendEntries里prevLogTerm prevLogIndex来自 logs[nextIndex[x] - 1],即LogTerm=6,LogIndex=10.
- 如果follower判断prevLogIndex位置的log term不等于prevLogTerm,那么返回 False,否则返回True。
- leader收到follower的回复,如果返回值是False,则nextIndex[x] -= 1, 跳转到s2. 否则s5
- 同步nextIndex[x]后的所有log entries
比如同步a,10的位置为空,返回false,即看9位置,然后返回true,接着同步10位置,完成a节点的同步。
优化:在拒绝AppendEntries请求时,follower可以包含冲突条目的term以及它为该term存储的第一个索引。这样,leader可以递减nextIndex以绕过该项中所有冲突的term。这样的代价就减少了很多:每个term一个RPC,而不是每个entry一个RPC了。
5.4 Safety
衡量一个分布式算法,有许多属性,如
- safety:nothing bad happens,
- liveness: something good eventually happens.
在任何系统模型下,都需要满足safety属性,即在任何情况下,系统都不能出现不可逆的错误,也不能向客户端返回错误的内容。比如,raft保证被复制到大多数节点的日志不会被回滚,那么就是safety属性。而raft最终会让所有节点状态一致,这属于liveness属性。
只有领导者选举以及复制日志,还不足以保证每个状态机以相同的顺序执行完全相同的命令。例如:当leader提交几个log entry时,一个follower可能不可用,然后它可以被选为leader并用新的log entry覆盖这些log entry; 导致已经提交的被覆盖。
上述问题其实是leader完整性问题。
5.4.1 Election restriction
raft 能够保证log在一个方向上流动,从领导者到追随者,并且领导者永远不会覆盖其日志中的现有条目。
RequestVote RPC实现了这个限制:RPC包含关于候选人日志的信息,如果投票者自己的日志比候选人的日志更新,那么投票者就会拒绝投票。
Raft通过比较log中最后一项的index和term来确定两个日志中哪一个是最新的。如果日志的最后一个entry具有不同的term,那么具有较晚的term的日志是最新的。如果日志以相同的term结束,那么较长的日志就是最新的。
5.4.2 Committing entries from previous terms
State Machine Safety: if a server has applied a log entry at a given index to its state machine, no other server will ever apply a different log entry for the same index.
图8,在时刻(a), s1是leader,在term2提交的日志只赋值到了s1 s2两个节点就crash了。在时刻(b), s5成为了term 3的leader,日志只赋值到了s5,然后crash。然后在©时刻,s1又成为了term 4的leader,开始赋值日志,于是把term2的日志复制到了s3,此刻,可以看出term2对应的日志已经被复制到了majority,因此是committed,可以被状态机应用。不幸的是,接下来(d)时刻,s1又crash了,s5重新当选,然后将term3的日志复制到所有节点,这就出现了一种奇怪的现象:被复制到大多数节点(或者说可能已经应用)的日志被回滚。
究其根本,是因为term4时的leader s1在(C)时刻提交了之前term2任期的日志。
解决:Raft never commits log entries from previous terms by counting replicas.
某个leader选举成功之后,不会直接提交前任leader时期的日志,而是通过提交当前任期的日志的时候“顺手”把之前的日志也提交了。如果leader被选举后没有收到客户端的请求呢,论文中有提到,在任期开始的时候发立即尝试复制、提交一条空的log。
因此,在上图中,不会出现(C)时刻的情况,即term4任期的leader s1不会复制term2的日志到s3。而是如同(e)描述的情况,通过复制-提交 term4的日志顺便提交term2的日志。如果term4的日志提交成功,那么term2的日志也一定提交成功,此时即使s1 crash,s5也不会重新当选。
5.5 Follower and candidate crashes
Follower and candidate crashes都以相同的方式处理。
如果一个follower or candidate崩溃,那么未来发送给它的RequestVote和AppendEntries rpc将失败。**Raft通过无限重试来处理这些失败;**如果崩溃的服务器重新启动,则RPC将成功完成。如果服务器在完成RPC后但在响应之前崩溃,那么它将在重新启动后再次接收相同的RPC。Raft rpc是idempotent幂等的,所以这不会造成伤害。例如,如果一个follower接收到一个AppendEntries请求,其中包含已经在其日志中出现的日志条目,那么它将忽略新请求中的那些条目。
同的方式处理。
如果一个follower or candidate崩溃,那么未来发送给它的RequestVote和AppendEntries rpc将失败。**Raft通过无限重试来处理这些失败;**如果崩溃的服务器重新启动,则RPC将成功完成。如果服务器在完成RPC后但在响应之前崩溃,那么它将在重新启动后再次接收相同的RPC。Raft rpc是idempotent幂等的,所以这不会造成伤害。例如,如果一个follower接收到一个AppendEntries请求,其中包含已经在其日志中出现的日志条目,那么它将忽略新请求中的那些条目。