在分布式系统中,通常需要多副本进行备份,但是副本的同步一致一直是一个比较棘手的问题。Raft算法是一个能够代替Paxos的分布式一致性算法,能够管理日志复制(replicated log),他的性能与Paxos不相上下,但是却比Paxos更容易理解。
Paxos存在的问题:
一致性算法通常具有的属性:
Raft新特性:
Raft提高可理解的方式:模块分解(leader选举、日志复制、安全性)、减少状态空间。
复制状态机用于解决分布式系统中的各种容错问题,通常通过复制日志实现,每个服务器存储一个包含一系列命令的日志,其状态机按顺序执行日志中的命令。每个状态机处理日志的顺序相同,因此也能够得到相同的输出序列。
一致性算法的工作就是保证复制日志的一致性。 每台服务器上的一致性模块接收来自客户端的命令,并将它们添加到其日志中。 它与其他服务器上的一致性模块通信,以确保每个日志最终以相同的顺序包含相同的命令,即使有一些服务器失败。 一旦命令被正确复制,每个服务器上的状态机按日志顺序处理它们,并将输出返回给客户端。 这样就形成了高可用的复制状态机。
Raft 把时间分割成任意长度的任期(term),每个任期从选举开始,一个或者多个 candidate 尝试成为 leader 。如果一个 candidate 赢得选举,然后他就在该任期剩下的时间里充当 leader。如果没选出leader,则任期很快结束,开启下一个leader,每个任期最多只能有一个leader。
在raft中,任期充当逻辑时钟,服务器节点可以通过任期发现一些过期的信息比如过时的 leader。任期单调递增,服务器之间通信的时候会交换当前任期号;如果一个服务器的当前任期号比其他的小,该服务器会将自己的任期号更新为较大的那个值。如果一个 candidate 或者 leader 发现自己的任期号过期了,它会立即回到 follower 状态。如果一个节点接收到一个包含过期的任期号的请求,它会直接拒绝这个请求。
Raft使用RPC进行通信,主要有三种类型RPC:
服务器状态:
服务器状态几个点:
选举规则:
follower 先增加自己的当前任期号并且转换到 candidate 状态;票给自己并且并行地向集群中的其他服务器节点发送 RequestVote RPC。Candidate 会一直保持当前状态直到以下三件事情之一发生:
为了防止第三种情况无限重复,使得没有leader选举出来,raft做了以下工作:
日志定义:客户端的每一个请求都包含一条将被复制状态机执行的指令。Leader 把该指令作为一个新的条目追加到日志中去,然后并行的发起 AppendEntries RPC ,同步到其他副本。
安全复制:当有超过一半的副本复制好了该条目,便认为已经安全的复制,leader接着会将条目应用到他的状态机中,并将结果返回给客户端。若遇到网络故障或丢失包,leader 会不断地重试 AppendEntries RPC(即使已经回复了客户端)直到所有的 follower 最终都存储了所有的日志条目。
日志索引:日志如图所示,每个日志条目存储一条状态机指令和 leader 收到该指令时的任期号,同时每个日志条目都有一个整数索引值来表明它在日志中的位置。
日志提交:一旦创建该日志条目的 leader 将它复制到过半的服务器上,该日志条目就会被提交,raft 算法保证所有已提交的日志条目都是持久化的并且最终会被所有可用的状态机执行。不仅如此,leader 日志中该日志条目之前的所有日志条目也都是已经提交的条目。Leader会保存当前已提交最大的日志索引,未来的所有 AppendEntries RPC 都会包含该索引,这样其他的服务器才能最终知道哪些日志条目需要被提交。Follower 一旦知道某个日志条目已经被提交就会将该日志条目按顺序应用到自己的本地状态机中。
日志条目的两点:
故障情况:正常操作期间,leader 和 follower 的日志保持一致,所以 AppendEntries RPC 的一致性检查从来不会失败。但也有可能出现下图的故障:当一个 leader 成功当选时(最上面那条日志),follower 可能是(a-f)中的任何情况。每一个盒子表示一个日志条目;里面的数字表示任期号。Follower 可能会缺少一些日志条目(a-b),可能会有一些未被提交的日志条目(c-d),或者两种情况都存在(e-f)。例如,场景 f 可能这样发生,f 对应的服务器在任期 2 的时候是 leader ,追加了一些日志条目到自己的日志中,一条都还没提交(commit)就崩溃了;该服务器很快重启,在任期 3 重新被选为 leader,又追加了一些日志条目到自己的日志中;在这些任期 2 和任期 3 中的日志都还没被提交之前,该服务器又宕机了,并且在接下来的几个任期里一直处于宕机状态。
leader 通过强制 follower 复制它的日志来解决不一致的问题。这意味着 follower 中跟 leader 冲突的日志条目会被 leader 的日志条目覆盖。
优化:如果想要的话,该协议可以被优化来减少被拒绝的 AppendEntries RPC 的个数。例如,当拒绝一个 AppendEntries RPC 的请求的时候,follower 可以包含冲突条目的任期号和自己存储的那个任期的第一个 index 。借助这些信息,leader 可以跳过那个任期内所有冲突的日志条目来减小 nextIndex;这样就变成每个有冲突日志条目的任期需要一个 AppendEntries RPC 而不是每个条目一次。在实践中,我们认为这种优化是没有必要的,因为失败不经常发生并且也不可能有很多不一致的日志条目。
目的:防止缺少很多日志条目的follower被选举为leader
实现方式:
Raft 使用投票的方式来阻止 candidate 赢得选举除非该 candidate 包含了所有已经提交的日志条目。候选人为了赢得选举必须与集群中的过半节点通信,这意味着至少其中一个服务器节点包含了所有已提交的日志条目。如果 candidate 的日志至少和过半的服务器节点一样新(接下来会精确地定义“新”),那么他一定包含了所有已经提交的日志条目。
投票规则:RPC 中包含了 candidate 的日志信息,如果投票者自己的日志比 candidate 的还新,它会拒绝掉该投票请求。(如果两份日志最后条目的任期号不同,那么任期号大的日志更新。如果两份日志最后条目的任期号相同,那么日志较长的那个更新。)
存在的问题:如图的时间序列展示了为什么 leader 无法判断老的任期号内的日志是否已经被提交。在 (a) 中,S1 是 leader ,部分地复制了索引位置 2 的日志条目。在 (b) 中,S1 崩溃了,然后 S5 在任期 3 中通过 S3、S4 和自己的选票赢得选举,然后从客户端接收了一条不一样的日志条目放在了索引 2 处。然后到 (c),S5 又崩溃了;S1 重新启动,选举成功,继续复制日志。此时,来自任期 2 的那条日志已经被复制到了集群中的大多数机器上,但是还没有被提交。如果 S1 在 (d) 中又崩溃了,S5 可以重新被选举成功(通过来自 S2,S3 和 S4 的选票),然后覆盖了他们在索引 2 处的日志。但是,在崩溃之前,如果 S1 在自己的任期里复制了日志条目到大多数机器上,如 (e) 中,然后这个条目就会被提交(S5 就不可能选举成功)。 在这种情况下,之前的所有日志也被提交了。
解决:Raft 不通过计算副本数目的方式来提交之前任期内的日志条目。只有 leader 当前任期内的日志条目才通过计算副本数目的方式来提交;
如果 follower 或者 candidate 崩溃了,那么后续发送给他们的 RequestVote 和 AppendEntries RPCs 都会失败。Raft 通过无限的重试来处理这种失败;如果崩溃的机器重启了,那么这些 RPC 就会成功地完成。如果一个服务器在完成了一个 RPC,但是还没有响应的时候崩溃了,那么在它重启之后就会再次收到同样的请求。Raft 的 RPCs 都是幂等的,所以这样的重试不会造成任何伤害。例如,一个 follower 如果收到 AppendEntries 请求但是它的日志中已经包含了这些日志条目,它就会直接忽略这个新的请求中的这些日志条目。
只要整个系统满足下面的时间要求,Raft 就可以选举出并维持一个稳定的 leader:广播时间(broadcastTime) << 选举超时时间(electionTimeout) << 平均故障间隔时间(MTBF)
为了使配置变更机制能够安全,在转换的过程中不能够存在任何时间点使得同一个任期里可能选出两个 leader 。
存在的问题:直接从一种配置转到另一种配置是不安全的,因为各个机器会在不同的时候进行转换。在这个例子中,集群从 3 台机器变成了 5 台。不幸的是,存在这样的一个时间点,同一个任期里两个不同的 leader 会被选出。一个获得旧配置里过半机器的投票,一个获得新配置里过半机器的投票。
方法:联合一致(joint consensus):
配置变更步骤:
配置变更时存在的问题: