In Search of an Understandable Consensus Algorithm (Extended Version)
1. 一致性算法的特征:
- 在所有的非拜占庭条件下保证安全(不会返回一个错误的结果),包括网络延时,分区,包丢失,复制,重新排序。
- 只要大多数服务器能够相互通信并且连接客户端,则可用;
- 不依赖于时间保证日志的一致性,因为错误的时钟或者及其大的消息时延在最坏的情况下能够导致可用性问题;
- 通常,只要集群中的大多数服务器回应了单个的RPC,一次命令就算完成;大多数服务器的缓慢不会影响系统的整体性能。
拜占庭条件和非拜占庭条件:
分布式领域一个著名的问题: 拜占庭将军问题。 指的是有
2. Raft
复制状态机 :Raft的总体模型如上图所示。
Raft将一致性问题分为三个子问题:
- leader选举: 当当前的leader失败后新的leader必须被选举出来;
- 日志复制: leader必须从客户端接收日志并将其在集群中复制,确保其他日志与其相同;
- 安全: Raft最主要的安全特性是复制状态机的特性:如果任何一个服务器将一个特殊的日志应用到其复制状态机,那么没有服务器在当前的日志索引下应用其他的日志;
安全包括:
- 选举安全: 最多一个leader被选举出来;
- leader只追加日志: leader不会删除和重写其含有的日志;
- 日志匹配: 如果两个日志包含具有相同索引和时期的条目,则日志在给定索引之前的所有条目中都是相同的。
- leader完整性: 在给定的时期,一个日志被提交,那么该日志在其他更高数字的时期的leader上也存在;
- 状态机安全性: 如果一台服务器在给定的索引下应用了一个日志,那么没有其他的状态机在相同的索引下应用其他的日志;
2.1 Raft基础
奇数台服务器,每台服务器有三种状态:leader,follower和candidate。
- leader: leader处理所有客户端请求(如果一个客户端请求follower,follower将该请求重定向到leader)
- follower: follower只处理来自于leader的请求;
- candidate: 用于选举一个新leader。
状态之间的转换如下图所示:
Raft将时间分为任意长度的时期,时期通过连续的数字标识:
每个时期以选举开始,此时一或多个candidator尝试成为leader;如果有一个赢得了选举,那么其成为当前时期的leader;如果出现了分离的选票(即没有一个candidator赢得选举),那么该时期结束,下一个时期开始;有些服务器不会观察到term的转换,因此这些服务器被丢弃(因为持有了旧数据)。
每一个服务器都有一个单调递增的current term number,在两台服务器通信时就会被交换;
- 如果一台服务器的current term number小于另一台服务器,则当前服务器更新自己的current term number为一个更大的值;
- 如果candidate/leader发现他的时期已经过期,那么其立刻将其状态转换为follower。
- 如果一台服务器收到的请求的current term number比自己所持有的小,则拒绝这个请求;
服务器间通过RPC通信;三种类型的RPC:RequestVote
用于candidate请求投票;AppendEntries
用于leader追加日志;第三种类型的RPF用于传输快照;RPC在特定时期没有收到回答时会重试;RPC是并行以提高性能。
2.2 Leader 选举
服务器刚启动时是follower状态;只要其收到leader和candidate的RPC请求,那么其一直保持follower状态;Leader向follower发送心跳(没有日志的AppendEntries RPC
)保持其权威;如果在一定时间后其没有收到RPC,则其认为没有可行的leader并开始一个选举产生新的leader;
开始选举时,其增加他的current term number
,状态改变为candidate,给自己投一票,并且并行向其他server发送RequestVote RPC
以请求他们为自己投票。candidate状态持续直到:
- **其赢得了选举:**如果一个candidate收到了大多数服务器的投票则认为赢得了选举;在一个时期内follower通过先到先得的方式只会给一个candidate投票(还有其他条件);如果一个candidate赢得了选举,其立刻向其他server发送心跳确保权威和阻止其他的candidate选举。
- 另一个服务器宣布其为leader: 如果选举期间candidate收到了心跳并且发现心跳的current term number 至少和自己的current term number一样大则认定leader合法,终止选举并将其状态变为follower;否则拒绝该心跳,仍然保持candidate状态。
- 一段时期过去了,没有赢家: 如果每一个candidate都没有拿到特定数量的票数,那么在一定超时时间之后重新开始投票。等待超时时间的原因是server之间网络可能不通。因此发送给某些server的请求得不到应答。另一个原因是前文提及如果RPC调用失败了那么会重试,因此需要等待超时时间。
通过随机的超时时间确保分裂投票很少发生:选举超时时间是在一个固定的时间间隔内随机选择(如150~300ms)。每次选举都会随机的选取一个超时时间。
2.3 日志复制
leader在处理客户端请求的时候步骤如下:
- 首先将客户端请求中包含的命令记录到自己的本地日志;
- 并行发送
AppendEntrics RPC
给每一台服务器去复制该日志;- 如果日志被安全复制,则leader将日志应用到复制状态机上,并将结果发送给客户端;
- 对于没有出错的RPC调用,leader会一直重试直到所有的follower复制了该日志。
每一个日志都有两个值:term number和log index。
leader同时决定何时将一个日志应用到状态机上;这样的条目叫做committed。一旦一个条目被集群中的大多数服务器复制,则认为该条目被提交;一个条目一旦被创建他的leader提交意味着所有之前的条目都是提交的状态(包括前面的leader创建的条目)。leader追踪其知道的最新被提交的条目的索引,并且将其包含在将来的AppendEntries RPC
上(包括心跳)因此其他服务器也会知道最新提交的条目。一旦一个follower知道一个新的条目被提交,那么其将这个条目应用到他的复制状态机上面。
以上的机制保证了:
- 如果两个不同的日志中的条目有相同的索引,那么他们包含相同的命令;因为leader在特定时期的特定索引上只会创建一个条目,并且条目内容永远不会被改变。
- 如果两个不同日志中的条目有相同的索引,那么两个日志中所有之前索引的条目都相同。通过
AppendEntries RPC
一个简单的一致性检查实现:leader会将新条目之前一个条目的term number和index发送给follower。如果follower发现自己前一个条目的term number和index与leader发送的不同,那么follower会拒绝这个AppendEntries RPC
请求。
正常情况下以上机制工作正常,但是当leader宕机的时候,先选出的leader并没有复制之前leader的所有日志的话机会出问题。当有一个新的leader接任时,下图所示的任何情况都会发生:
leader会强制follower的日志与自己的相同来处理这种不一致性。
为了让follower的日志与自己的相同,leader需要找到两个日志中一致的最近的日志条目,删除follow日志中该条目后的所有日志并且将leader从该条条目之后的所有日志发给follower。所有这些动作都是为了响应AppendEntries RPC
的一致性检查而发生的。leader为每一个follower维护一个nextIndex变量,其值表示leader向follower发送的下一个日志条目的索引。初始时leader将其设置为自己的log的下一个index;每次检查失败(即follower拒绝了leader的AppendEntries RPC
)leader会减少这个值并且重试AppendEntries RPC
。最终nextIndex会到达leader和follower相匹配的点,此时AppendEntries RPC
成功。一旦AppendEntries RPC
成功了则意味着leader和follower的日志是相同的。
可以做一些优化来降低AppendEntries RPC
被拒绝的次数。follower在拒绝的同时可以返回冲突的时期和在这个时期其存储的第一个日志条目的索引。leader因此可以绕过当前时期的所有冲突的索引。
需要注意的是,leader从来不会删除或者重写自己的日志。
这种日志复制机制展示了第 2 节中描述的理想共识属性:只要大多数服务器都启动,Raft 就可以接受、复制和应用新的日志条目; 在正常情况下,可以通过一轮 RPC 将新条目复制到集群的大多数; 并且单个慢follower不会影响性能。
2.4 安全性
前述机制并不能保证每个状态机以相同的顺序执行相同的指令exactly once。如果一个落后的follower在新的选举中被选举成为了leader,那么其会重写其他follower的日志导致复制状态机出现不一致性。需要添加如下限制:
2.4.1 选举限制
为了防止缺少log entry的candidate被选举为leader,在follower投票时做了如下的限制:candidate在投票(发送RequestVode RPC
)的时候会携带自己的日志信息,如果follower发现自己的日志超过了candidate的日志,那么其将会拒绝对candidate的投票。
日志之间如何进行比较:
- 如果两个日志的最后一个条目有不同的term number,那么term number大的日志更新;
- 如果term number相同,则哪个日志更长(index值更大)则更新;
2.4.2 提交之前term的条目
如果当前leader在提交某一条目的过程中宕机了,那么新的leader将会继续尝试提交之前leader未提交的内容。但是新leader在提交之前条目的过程中不能通过该条目是否被大多数follower存储来判断该条目被提交,原因如下:
(a). 是leader,部分复制了条目
(b). 被选举成为了新的leader,将新的操作
(c). 重启,虽然没获取到所有的选票(不会给投票),但是因为获得了大多数选票而被选举为新的leader;因此继续之前(时期)的复制;将复制给了大多数的服务器但是还没有提交(没有提交给复制状态机应用);之后宕机;
(d). 重新被选举成为leader(获得了来自,,的选票);将之前时期的条目(3,2)应用到所有服务器上;
(e). 但是如果在(c)阶段同时接受了日志条目的请求并将其复制到了大多数服务器上(,,),那么则会认为之前的所有日志已经被提交(包括日志)。此时不可能赢得选举从而继续应用之前term的条目。
所以raft采取的办法是通过计数的方式提交之前term未提交的内容,而是在当前term中,如果来了新的请求,那么通过提交当前条目的方式提交之前的term的条目。因为一旦当前term的条目被提交,则说明之前term的条目也被提交了。
2.5 follower和candidate宕机
如果一个candidate/follower宕机,那么之后发给他们的RPC
就会失败。其他server会通过无限的重试发送RPC
请求;如果candidate/follower在完成RPC请求之后,返回应答之前失败,那么其在重启之后会收到相同的RPC请求。Raft通过幂等操作来保证,即一个Raft follower在收到相同的RPC请求是,如果发现该日志条目已经存在于自己的日志中,那么他会简单的忽略这个请求。
2.6 时间和可用性
如果server之前通信用时过长,或者服务器宕机太频繁,则会导致无法选举出新的leader(candidate选举是有超时时间的)。因此需要满足如下的关系式:
是一个服务器并行发送RPC
到所有其他服务器,并且收到应答的平均时间;是选举超时时间;表示服务器两次宕机的平均时间间隔。一般要求选举超时时间在之间(取决于永久性存储(磁盘等等)的响应时间)
3.集群成员改变
任何直接的配置转换都是不安全的,因为会导致出现两个leader,一个使用旧配置,另一个使用新配置:
所以在配置转换的过程中,设置了一个用于转换的配置成为:联合一致性。联合一致性既包含旧的配置,又包含新的配置:
- 日志条目既被旧的配置的服务器复制,也被新的服务器复制;
- 任何一个配置中的任何一个服务器都可以成为leader;
- 协议(投票和提交)既要求旧配置中的大多数,又要求新的配置中的大多数;
配置转换过程:
- 首先,leader收到了将配置从转换到的请求;
- leader将联合一致性保存到自己的日志中;
- leader复制该日志;一旦一个follower将新的日志添加到自己的日志中,那么follower会立刻使用该新日志(应该指的是);
- leader会使用配置来判断是否提交成功。如果leader宕机,之后的leader也会这样做;
- 在日志提交成功后,leader继续提交新的日志,与1,2,3,4步相同;
配置也有三个问题:
- 新加入的server的日志是空的:通过转换过程中的中间状态来复制日志;
- leader不在新的配置中:leader在提交了日志之后会退回follower状态;
- 旧的follower会影响新的集群,因为他们不会再收到心跳报文,因此会重新选举leader;如果他们开始重新选举leader(以新的term number),将会导致当前的leader将状态转换为follower(leader收到了
RequestVote RPC
,发现其中的term number大于自己的current term number)。解决办法是服务器在确信当前存在leader的时候会拒绝投票。具体来说,如果服务器在听取当前领导者的最小选举超时内收到 RequestVote RPC,则不会更新其任期或授予其投票权。
4. 日志紧凑
日志过多的话,集群对请求的处理会越来越慢。最简单的处理方法是快照。
- 将所有的当前系统状态写入到永久存储中;
- 删除当前状态之前的所有日志
也可以使用LSM
树。
5. 客户端交互
客户端随机选一个服务器,如果这个服务器是leader会直接处理client的请求;如果不是,该服务器会拒绝客户端的请求并告诉客户端最新的leader的IP地址;
Raft的目标是实现线性的语义,保证了每个操作盒调用都在某个时间点执行(exactly once
的语义)。但是如果leader在commit成功后,返回结果给客户端前失败,客户端可能会超时重试导致执行两次。解决方法是为客户端的每个命令生成一个唯一的序列号,追踪最近一次执行的命令的序列号,如果之后接收的命令是之前执行过的(注意要能判断一个命令是否是之前执行过的)就直接返回结果。
只读操作可以不用写日志就可以返回结果,但是会出现返回旧数据的问题,因为相应请求的leader可能会被新的leader替代但是leader还不知道。
解决方法:
- leader需要知道最新提交的条目。这只会在新的term开始时出现,因此新term开始时leader会提交一个
nop
条目保证自己知道最新的提交信息;- leader在返回读的结果之前需要与其他服务器通过交换心跳消息判断自己是否被废黜。
6. 总结
以上。
最后附上Raft的实现细节。状态:
AppendEntries RPC
:
RequestVote RPC
:
规则: