Raft算法
根据raft一致性论文做的归纳
概述
Raft是一种为了管理复制日志的一致性算法。它提供了和 Paxos 算法相同的功能和性能,但是它的算法结构和 Paxos 不同,使得 Raft 算法更加容易理解并且更容易构建实际的系统。为了提升可理解性,Raft 将一致性算法分解成了几个关键模块,例如领导人选举、日志复制和安全性。同时它通过实施一个更强的一致性来减少需要考虑的状态的数量。一项用户研究的结果表明,对于学生而言,Raft 算法比 Paxos 算法更加容易学习。Raft 算法还包括一个新的机制来允许集群成员的动态改变,它利用重叠的大多数来保证安全性。
Raft 算法在许多方面和现有的一致性算法都很相似(主要是 Oki 和 Liskov 的 Viewstamped Replication),但是它也有一些独特的特性:
- 强领导人:和其他一致性算法相比,Raft 使用一种更强的领导能力形式。比如,日志条目只从领导人发送给其他的服务器。这种方式简化了对复制日志的管理并且使得 Raft 算法更加易于理解。
- 领导选举:Raft 算法使用一个随机计时器来选举领导人。这种方式只是在任何一致性算法都必须实现的心跳机制上增加了一点机制。在解决冲突的时候会更加简单快捷。
- 成员关系调整:Raft 使用一种共同一致的方法来处理集群成员变换的问题,在这种方法下,处于调整过程中的两种不同的配置集群中大多数机器会有重叠,这就使得集群在成员变换的时候依然可以继续工作。
复制状态机
一致性算法是从复制状态机的背景下提出的。
复制状态机在分布式系统中被用于解决很多容错的问题,通常都是基于复制日志实现的
一致性算法的任务是保证复制日志的一致性,服务器上的一致性模块接收客户端发送的指令然后添加到自己的日志中。它和其他服务器上的一致性模块进行通信来保证每一个服务器上的日志最终都以相同的顺序包含相同的请求,即使有些服务器发生故障。一旦指令被正确的复制,每一个服务器的状态机按照日志顺序处理他们,然后输出结果被返回给客户端。因此,服务器集群看起来形成了一个高可靠的状态机。
一致性算法通常含有以下特性:
- 安全性保证(绝对不会返回一个错误的结果):在非拜占庭错误情况下,包括网络延迟、分区、丢包、重复和乱序等错误都可以保证正确。
- 可用性:集群中只要有大多数的机器可运行并且能够相互通信、和客户端通信,就可以保证可用。因此,一个典型的包含 5 个节点的集群可以容忍两个节点的失败。服务器被停止就认为是失败。它们稍后可能会从可靠存储的状态中恢复并重新加入集群。
- 不依赖时序来保证一致性:物理时钟错误或者极端的消息延迟只有在最坏情况下才会导致可用性问题。
- 通常情况下,一条指令可以尽可能快的在集群中大多数节点响应一轮远程过程调用时完成。小部分比较慢的节点不会影响系统整体的性能。
为了可理解性的设计
设计Raft算法的初衷:
- 必须提供一个完整的实际的系统实现基础,这样才能大大减少开发者的工作
- 必须在任何情况下都是安全的并且在大多数的情况下都是可用的
- 它的大部分操作必须是高效的
- 必须对于普通人群都可以十分容易的去理解
解决可理解性:
- 问题分解,Raft算法被分解成领导人选举,日志复制,安全性和成员变更
- 减少状态的数量来简化需要考虑的状态空间,使得系统更加连贯并且在可能的时候消除不确定性
Raft一致性算法
三个独立的子问题
- 领导选举:当现存领导人发生故障的时候,一个新的领导人需要被选举出来
- 日志复制:领导人必须从客户端接收日志条目然后复制到集群中的其他节点,并强制要求其他节点的日志和自己保持一致
- 安全性:如果有任何的服务器节点已经应用了一个确定的日志条目到它的状态机中,那么其他服务器节点不能在同一个日志索引位置应用一个不同的指令。章节 5.4 阐述了 Raft 算法是如何保证这个特性的;这个解决方案涉及到选举机制(5.2 节)上的一个额外限制。
追加条目(AppendEntries)RPC:
由领导人调用,用于日志条目的复制,同时也被当做心跳使用
请求投票(RequestVote)RPC
由候选人负责调用用来征集选票
所有服务器需遵守的规则:
所有服务器:
- 如果
commitIndex > lastApplied,则 lastApplied 递增,并将log[lastApplied]应用到状态机中(5.3 节) - 如果接收到的 RPC 请求或响应中,任期号
T > currentTerm,则令currentTerm = T,并切换为跟随者状态(5.1 节)
跟随者:
- 响应来自候选人和领导人的请求
- 如果在超过选举超时时间的情况之前没有收到当前领导人(即该领导人的任期需与这个跟随者的当前任期相同)的心跳/附加日志,或者是给某个候选人投了票,就自己变成候选人
候选人:
- 在转变成候选人后就立即开始选举过程
- 自增当前的任期号(currentTerm)
- 给自己投票
- 重置选举超时计时器
- 发送请求投票的 RPC 给其他所有服务器
- 如果接收到大多数服务器的选票,那么就变成领导人
- 如果接收到来自新的领导人的附加日志(AppendEntries)RPC,则转变成跟随者
- 如果选举过程超时,则再次发起一轮选举
领导人:
-
一旦成为领导人:发送空的附加日志(AppendEntries)RPC(心跳)给其他所有的服务器;在一定的空余时间之后不停的重复发送,以防止跟随者超时(5.2 节)
-
如果接收到来自客户端的请求:附加条目到本地日志中,在条目被应用到状态机后响应客户端(5.3 节)
-
如果对于一个跟随者,最后日志条目的索引值大于等于 nextIndex(
lastLogIndex ≥ nextIndex),则发送从 nextIndex 开始的所有日志条目:
- 如果成功:更新相应跟随者的 nextIndex 和 matchIndex
- 如果因为日志不一致而失败,则 nextIndex 递减并重试
-
假设存在 N 满足
N > commitIndex,使得大多数的matchIndex[i] ≥ N以及log[N].term == currentTerm成立,则令commitIndex = N(5.3 和 5.4 节)
一个关于 Raft 一致性算法的浓缩总结(不包括成员变换和日志压缩)。
| 特性 | 解释 |
|---|---|
| 选举安全特性 | 对于一个给定的任期号,最多只会有一个领导人被选举出来(5.2 节) |
| 领导人只附加原则 | 领导人绝对不会删除或者覆盖自己的日志,只会增加(5.3 节) |
| 日志匹配原则 | 如果两个日志在某一相同索引位置日志条目的任期号相同,那么我们就认为这两个日志从头到该索引位置之间的内容完全一致(5.3 节) |
| 领导人完全特性 | 如果某个日志条目在某个任期号中已经被提交,那么这个条目必然出现在更大任期号的所有领导人中(5.4 节) |
| 状态机安全特性 | 如果某一服务器已将给定索引位置的日志条目应用至其状态机中,则其他任何服务器在该索引位置不会应用不同的日志条目(5.4.3 节) |
Raft基础
领导人选举
Raft使用一种心跳机制触发领导人选举。当服务器程序启动时,他们都是跟随者身份,一个服务器节点继续保持着跟随着状态只要他从领导人或者候选人处接收到有效的RPCs。领导人周期性的向所有跟随者发送心跳包(即不包含日志项内容的附加条目(AppendEntries) RPCs)来维持自己的权威。如果一个跟随者在一段时间里没有接收到任何消息,也就是选举超时,那么他就会认为系统中没有可用的领导人,并且发起选举以选出新的领导人。
要开始一次选举过程,跟随者先要增加自己的当前任期号并且转换到候选人状态。然后他会并行的向集群中的其他服务器节点发送请求投票的 RPCs 来给自己投票。候选人会继续保持着当前状态直到以下三件事情之一发生:(a) 他自己赢得了这次的选举,(b) 其他的服务器成为领导人,(c) 一段时间之后没有任何一个获胜的人。
- 当一个候选人从整个集群的大多数服务器节点获得了针对同一个任期号的选票,那么就成为领导人了。它会向其他的服务器发送心跳信息来建立自己的权威并阻止新的领导人产生
- 候选人会从其他的服务器接收到声明它是领导人的附加条目RPC。如果这个领导人的任期号不小于候选人当前的任期号,那么候选人会承认领导人合法并回到跟随者状态
- 候选人既没有赢也没有输,那么选票可能会被瓜分以至于没有候选人可以赢,如果新一轮选举没有其他机制的话,选票还是可能被瓜分
- Raft算法使用随机选举超时时间的方法来确保很少会发生选票瓜分的情况,为了阻止选票起初就被瓜分,选举超时时间是从一个固定的区间(例如 150-300 毫秒)随机选择。这样可以把服务器都分散开以至于在大多数情况下只有一个服务器会选举超时;然后他赢得选举并在其他服务器超时之前发送心跳包。同样的机制被用在选票瓜分的情况下。每一个候选人在开始一次选举的时候会重置一个随机的选举超时时间,然后在超时时间内等待投票的结果;这样减少了在新的选举中另外的选票瓜分的可能性。
日志复制
一旦一个领导人被选举出来,他就开始为客户端提供服务。客户端的每一个请求都包含一条被复制状态机执行的指令。领导人把这条指令作为一条新的日志条目附加到日志中去,然后并行的发起附加条目 RPCs 给其他的服务器,让他们复制这条日志条目。当这条日志条目被安全的复制(下面会介绍),领导人会应用这条日志条目到它的状态机中然后把执行的结果返回给客户端。如果跟随者崩溃或者运行缓慢,再或者网络丢包,领导人会不断的重复尝试附加日志条目 RPCs (尽管已经回复了客户端)直到所有的跟随者都最终存储了所有的日志条目。
Raft 的日志机制来维护一个不同服务器的日志之间的高层次的一致性。这么做不仅简化了系统的行为也使得更加可预计,同时他也是安全性保证的一个重要组件。Raft 维护着以下的特性:
- 如果在不同的日志中的两个条目拥有相同的索引和任期号,那么他们存储了相同的指令。
- 如果在不同的日志中的两个条目拥有相同的索引和任期号,那么他们之前的所有日志条目也全部相同。
在 Raft 算法中,领导人处理不一致是通过强制跟随者直接复制自己的日志来解决了。这意味着在跟随者中的冲突的日志条目会被领导人的日志覆盖。
通过这种机制,领导人在获得权力的时候就不需要任何特殊的操作来恢复一致性。他只需要进行正常的操作,然后日志就能自动的在回复附加日志 RPC 的一致性检查失败的时候自动趋于一致。领导人从来不会覆盖或者删除自己的日志。
日志复制机制展示出了第 2 节中形容的一致性特性:Raft 能够接受,复制并应用新的日志条目只要大部分的机器是工作的;在通常的情况下,新的日志条目可以在一次 RPC 中被复制给集群中的大多数机器;并且单个的缓慢的跟随者不会影响整体的性能。
安全性
这一限制保证了任何的领导人对于给定的任期号,都拥有了之前任期的所有被提交的日志条目
选举限制
在任何基于领导人的一致性算法中,领导人都必须存储所有已提交的日志条目
Raft使用投票的方式来阻止一个候选人赢得选举除非这个候选人包含了所有已经提交的日志条目。候选人为了赢得选举必须联系集群中的大部分节点,这意味着每一个已经提交的日志条目在这些服务器节点中肯定存在于至少一个节点上。如果候选人的日志至少和大多数的服务器节点一样新(这个新的定义会在下面讨论),那么他一定持有了所有已经提交的日志条目
提交之前任期内的日志条目
Raft 永远不会通过计算副本数目的方式去提交一个之前任期内的日志条目。只有领导人当前任期里的日志条目通过计算副本数目可以被提交;一旦当前任期的日志条目以这种方式被提交,那么由于日志匹配特性,之前的日志条目也都会被间接的提交。在某些情况下,领导人可以安全的知道一个老的日志条目是否已经被提交。
当领导人复制之前任期里的日志时,Raft会为所有日志保留原始的任期号
跟随者和候选人崩溃
如果跟随者或者候选人崩溃了,那么后续发送给他们的 RPCs 都会失败。Raft 中处理这种失败就是简单的通过无限的重试;如果崩溃的机器重启了,那么这些 RPC 就会完整的成功。如果一个服务器在完成了一个 RPC,但是还没有响应的时候崩溃了,那么在他重新启动之后就会再次收到同样的请求。
时间和可用性
Raft 可以选举并维持一个稳定的领导人,只要系统满足下面的时间要求:
广播时间(broadcastTime) << 选举超时时间(electionTimeout) << 平均故障间隔时间(MTBF)
集群成员变化
为了保证安全性,配置更改必须使用两阶段方法。目前有很多种两阶段的实现。在 Raft 中,集群先切换到一个过渡的配置,我们称之为共同一致;一旦共同一致已经被提交了,那么系统就切换到新的配置上。共同一致是老配置和新配置的结合:
- 日志条目被复制给集群中新、老配置的所有服务器。
- 新、旧配置的服务器都可以成为领导人。
- 达成一致(针对选举和提交)需要分别在两种配置上获得大多数的支持。
日志压缩
快照是最简单的压缩方法。在整个快照系统,整个系统的状态都以快照形式写入到稳定的持久化存储中,然后到那个时间点之前的日志全部丢弃。增量压缩的方法,例如日志清理或者日志结构合并树,都是可行的。这些方法每次只对一小部分数据进行操作,这样就分散了压缩的负载压力。
每个服务器独立的创建快照,只包括已经被提交的日志。主要的工作包括将状态机的状态写入到快照中。Raft也包含一些少量的元数据到快照中:最后被包含索引指的是被快照取代的最后的条目在日志中的索引值,最后被包含的任期指的是该条目的任期号
尽管通常服务器都是独立的创建快照,但是领导人必须偶尔的发送快照给一些落后的跟随者。这通常发生在当领导人已经丢弃了下一条需要发送给跟随者的日志条目的时候。幸运的是这种情况不是常规操作:一个与领导人保持同步的跟随者通常都会有这个条目。然而一个运行非常缓慢的跟随者或者新加入集群的服务器将不会有这个条目。这时让这个跟随者更新到最新的状态的方式就是通过网络把快照发送给他们。
客户端交互
Raft中的客户端发送所有请求给领导人。当客户端启动的时候,他会随机挑选一个服务器进行通信。如果客户端第一次挑选的服务器不是领导人,那么那个服务器会拒绝客户端的请求并且提供他最近接收到的领导人的信息(附加条目请求包含了领导人的网络地址)。如果领导人已经崩溃了,那么客户端的请求就会超时;客户端之后会再次重试随机挑选服务器的过程。
Raft 的目标是要实现线性化语义(每一次操作立即执行,只执行一次,在他调用和收到回复之间)。但是,如上述,Raft 是可以执行同一条命令多次的:例如,如果领导人在提交了这条日志之后,但是在响应客户端之前崩溃了,那么客户端会和新的领导人重试这条指令,导致这条命令就被再次执行了。解决方案就是客户端对于每一条指令都赋予一个唯一的序列号。然后,状态机跟踪每条指令最新的序列号和相应的响应。如果接收到一条指令,它的序列号已经被执行了,那么就立即返回结果,而不重新执行指令。