Raft算法之日志复制
上一篇文章:Raft算法之Leader选举
之前说完了Raft算法中的Leader选举过程,本文将在上一篇文章的基础上说明日志复制。
Raft算法之日志复制
先看以下日志所包含的基本内容:
- 可以被复制状态机执行的命令
- 任期号 :创建该日志时Leader所处的当前任期号
- 索引号 :整数,用于标识日志所在的位置
日志的状态分为两种:未被提交,已被提交(日志为安全的,不会被删除或覆盖)。
1 正常情况
- 当
Leader
接收到由客户端发送的请求(请求中包含可以被复制状态机执行的命令)时,Leader将会把该请求作为新的内容添加到日志中(任期号为当前Leader
所处的任期号,索引号为当前Leader
本地存储的日志集合中的日志的最高索引号加1)。Leader
在当前任期内最多只能创建一个给定索引号的日志(即不可能在一个任期内创建两个以上的具有相同索引的日志条目)
- 然后将该日志通过
AppendEntries RPC
消息发送到网络中其他的服务器(以下简称Follower
),从而复制该日志。 - 在网络中
Follower
接收到该日志消息后则会返回复制成功的回复。 - 在
Leader
接收到网络中大部分的Follower
的成功复制的回复之后,Leader
便认为该日志可以被提交。此时Leader
将会同时做三件事:
- 将该日志应用到
Leader
本地的复制状态机 - 向所有
Follower
发送消息通知所有接收到该日志的Follower
将该日志进行提交,然后应用到各自本地的复制状态机 - 将执行结果通知客户端
当该日志消息成功在网络中大部分Follower
本地的复制状态机执行过后,则可认为该日志已被提交。在当前日志被提交的过程中,如果Leader
先前的某些日志还没有被提交,则将会一同提交。
而网络中有些Follower
可能由于网络状态原因反应缓慢或者崩溃,那么Leader
将会无限次地尝试重复发送AppendEntries RPC
消息到该Follower
。直到成功为止。
1.1 日志的一致性检查
在上面,我们说了Follower
在接收到AppendEntries RPC
消息后则会返回复制成功的回复。实际上在接收到消息后会首先进行日志的一致性检查(正常情况下Leader
与Follower
的日志会保持一致,所以一致性检查不会失败),一致性检查内容如下:
- 在
Leader
创建AppendEntries RPC
消息时,消息中将会包含当前日志之前日志条目的任期号与索引号。 Follower
在接受到AppendEntries RPC
消息后,将会检查之前日志的任期号与索引号是否可以匹配到- 如果匹配到则说明和
Leader
之前的日志是保持一致的。 - 如果没有匹配则会拒绝
AppendEntries RPC
消息.
- 如果匹配到则说明和
一致性检查是一个归纳的过程。正常情况下,网络中第一条日志一定满足日志的一致性检查,然后第二条日志中包含第一条日志的任期号与索引号,所以只要Leader
与Follower
的第一条日志保持一致,那么第二条日志也会满足一致性检查,从而之后的每一条日志都会满足一致性检查。
从而得出了日志匹配属性:
- 如果两个不同的日志实体具有相同的索引和任期号,那么他们存储有相同的命令。
- 如果两个不同的日志实体具有相同的索引和任期号,则所有先前条目中的日志都相同。(由一致性检查结果得出)
2 特殊情况
而网络不可能一直处于正常情况。因为Leader
或者某个Follower
有可能会崩溃,从而导致日志不能一直保持一致。因此存在以下三种情况:
Follower
缺失当前Leader
上存在的日志条目。Follower
存在当前Leader
不存在的日志条目。(比如旧的Leader
仅仅将AppendEntries RPC
消息发送到一部分Follower
就崩溃掉,然后新的当选Leader
的服务器恰好是没有收到该AppendEntries RPC
消息的服务器)- 或者
Follower
即缺失当前Leader
上存在的日志条目,也存在当前Leader
不存在的日志条目
图中最上方是日志的索引号(1-12),每个方块代表一条日志信息,方块内数字代表该日志所处的任期号。图中当前Leader
(图中最上方一行日志代表当前Leader
日志)处于任期号为8的时刻。以此图说明以上三种情况存在的原因:
Follower
a,b(Follower
崩溃没有接收到Leader
发送的AppendEntries RPC
消息)满足以上说明的第一种情况。- (
Follower
c在任期为6的时刻,Follower
d在任期为7的时刻)为Leader
,但没有完全完成日志的发送便崩溃了.满足以上说明的第三种情况。 Follower
e在任期为4的时刻,Follower
f在任期为2,3的时刻为Leader
,,但没有完全完成日志的发送便崩溃了,同时在其他服务器当选Leader
时刻也没有接收到新的Leader
发送的AppendEntries RPC
消息,满足第三种情况。
2.1 日志不一致的解决方案
Leader
通过强迫Follower
的日志重复自己的日志来处理不一致之处。这意味着Follower
日志中的冲突日志将被Leader
日志中的条目覆盖。因此Leader
必须找到与Follower
最开始日志发生冲突的位置,然后删除掉Follower
上所有与Leader
发生冲突的日志。然后将自己的日志发送给Follower
以解决冲突。
Leader
不会删除或覆盖自己本地的日志条目
这些步骤从之前说到的日志的一致性检查开始。
- 当发生日志冲突时,
Follower
将会拒绝由Leader
发送的AppendEntries RPC
消息,并返回一个响应消息告知Leader
日志发生了冲突。 Leader
为每一个Follower
维护一个nextIndex
值。该值用于确定需要发送给该Follower
的下一条日志的位置索引。(该值在当前服务器成功当选Leader
后会重置为本地日志的最后一条索引号+1)- 当
Leader
了解到日志发生冲突之后,便递减nextIndex
值。并重新发送AppendEntries RPC
到该Follower
。并不断重复这个过程,一直到Follower
接受该消息。 - 一旦
Follower
接受了AppendEntries RPC
消息,Leader
则根据nextIndex
值可以确定发生冲突的位置,从而强迫Follower
的日志重复自己的日志以解决冲突问题。
- 情况a: 如图,服务器S1在任期为2的时刻仅将日志
<index:2,term:2>
发送到了服务器S2便崩溃掉。 - 情况b: 服务器S5在任期为3的时刻当选
Leader
(S5的计时器率先超时,递增任期号为3因此高于服务器S3,S4,可以当选Leader
),但没来得及发送日志便崩溃掉。 - 情况c: 服务器S1在任期为4的时刻再次当选
Leader
(S1重启时,任期仍然为2,收到新的Leader
S5发送的心跳信息后更新任期为3,而在Leader
S5崩溃后,服务器S1为第一个计时器超时的,因此发起投票,任期更新为4,大于网络中其他服务器任期,成功当选Leader
),同时将日志<index:2,term:2>
发送到了服务器S2,S3,但还没有通知服务器对日志进行提交便崩溃掉。 - 情况d: 情况(a->d)如果在任期为2时服务器S1作为
Leader
崩溃掉,S5在任期为3的时刻当选Leader
,由于日志<index:2,term:2>
还没有被复制到大部分服务器上,并没有被提交,所以S5可以通过自己的日志<index:2,term:3>
覆盖掉日志<index:2,term:2>
。 - 情况e: 情况(a->e)而如果在任期为2时服务器S1作为
Leader
,并将<index:2,term:2>
发送到S2,S3,成功复制到大多数成员服务器上。并且成功提交了该日志,那么即便S1崩溃掉,S5也无法成功当选Leader
,因为S5不具备网络中最新的已被提交的日志条目(这里说明了上一篇文章Raft算法之Leader选举中选举Leader
的要求中没有介绍的那一点要求).
2.2 选举Leader
的对日志的要求
- Raft使用投票程序来防止
Candidate
赢得选举,除非其日志中包含所有已提交的日志条目。 Candidate
必须联系集群的大多数才能被选举,这意味着每个提交的条目都必须存在于其中至少一台服务器中。如果Candidate
的日志至少与该多数服务器日志中的日志一样最新(以下精确定义了最新),则它将保存所有已提交的条目。- Raft通过比较日志中最后一个条目的索引和任期来确定两个日志中哪个是最新的。如果日志中的最后一个条目具有不同的任期,则带有较新任期的日志是最新的。如果日志以相同的任期结尾,则以索引更大的日志为准。
解决方案的优化
在Follower
拒绝AppendEntries RPC
消息时,可以选择将发生冲突的日志的任期与该任期内的第一条日志索引包含在拒绝消息中返回给Leader
,从而使得Leader
可以快速定位到发生冲突的位置。有了这些信息,Leader
可以递减nextIndex
来绕过该任期中所有冲突的条目。每个具有冲突日志条目所处的任期都需要一个AppendEntries RPC
消息,而不是每个日志条目都需要一个AppendEntries RPC
消息。
3 日志复制安全性
Raft保证任何时刻这里的每一条属性都成立
Leader
只追加特性:Leader
从不覆盖或删除它的日志条目,只追加新的。- 日志匹配: 如果两个日志包含的实体具有相同的索引和任期,那么直到给定索引为止,所有条目中的日志都是相同的。
Leader
完整性:如果一个日志提示在给定的任期内被提交,那么该条目将出现在所有任期更高的领导者的日志中.- 状态机安全:如果服务器应用一条给定索引的日志实体到它的状态机,那么没有其他服务器可以应用一条不同的日志到相同的索引位置。
3.1 Leader
完整性证明
假设Leader
完整性不成立,然后证明是矛盾的。
假设任期为T的Leader
提交了当前任期的日志条目,但是该日志没有被任期高于T的任期为U的未来的新的Leader
所存储。
- 被提交任期为T的日志必须不存在于将要选举的任期为U的
Leader
的复制状态机中(因为Leader
从不覆盖或删除它的日志条目)。 - 任期为T的
Leader
将日志复制到集群中的大部分成员本地。并且任期为U的Leader
在选举阶段接收到集群中大部分成员的投票,因此至少集群中有一个成员(以下称为投票者)即接收到来自任期为T的Leader
发送的日志,也为任期为U的Leader
投了票。所以该投票者是证明矛盾的关键所在。 - 投票者必须在为任期为U的
Leader
投票之前将任期为T的Leader
的发送的日志提交。不然投票者将会拒绝任期为T的Leader
的AppendEntries PRC
请求(因为一旦接收到任期为U的Leader
投票请求,投票者的任期将会高于T)。 - 投票者当为任期为U的
Leader
投票时,将会一直存储该日志条目。假设在任期为T和U之间的每一个Leader
都包含该日志条目(Leader
从不删除日志条目,而Follower
仅在与Leader
冲突时才删除条目)。 - 投票者为任期为U的
Leader
投票,所以任期为U的Leader
日志必须至少和投票者的日志一样新。这将导致产生两个矛盾之中的一个矛盾。 - 首先,如果投票者和任期为U的
Leader
具有相同的最新的日志任期。那么任期为U的Leader
的日志至少和投票者的日志一样长。所以任期为U的Leader
的日志将包含投票者所有的日志。这是一个矛盾,因为之前假设的投票者包含被提交的任期为T的日志,而任期为U的Leader
不包含。 - 否则,任期为U的
Leader
的最后一条日志的任期号必须大于投票者的最后一条日志的任期号。而且,它比T大,因为投票者的上一个日志任期号至少为T(它包含任期T中的所有已提交的条目)。创建任期为U的Leader
的最后一个日志条目的较早的Leader
必须在其日志中(通过假设)包含已提交的条目。然后,通过日志匹配属性,任期为U的Leader
的日志还必须包含已提交的条目,这是矛盾的。 - 这样就证明了矛盾,因此所有任期大于T的
Leader
都必须包含所有任期为T的被提交的日志。 - 日志匹配属性保证未来的
Leader
还将包含间接提交的日志条目。
下一篇文章:Raft算法之成员关系变化