Raft协议

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Raft协议相关的知识,希望对你有一定的参考价值。

参考技术A raft协议是一个共识算法,主要包括leader election,log replication,safety三个关键部分,另外还包括membership changes和snapshot。

复制状态机是分布式系统中解决fault tolerance问题的常用手段。raft通过log replication来保证集群的多个server,会有同样的数据输入到各自的状态机。如图1所示。

关键术语:

Apply:将entry输入到状态机

committed:entry可以被安全的Apply到状态机,一般情况下entry被同步到集群的大多数节点上时,就可以认为是committed(有特殊情况)。

每个server都有一个log,log中包含一系列的entry(entry中有相应的命令,即客户端请求),状态机按照log中的顺序执行这些命令。

如果每个server输入状态机的数据相同,状态机产生的结果也是相同的。因此共识算法的目的就是保证多个server的log一致。

leader上的consensus module接收到客户端的命令,将这些命令作为entry添加到log中,并且和其他follower上的consensus module通信,将log entry同步到其他follower,以确保多个server之间日志文件的最终一致。

当达到一定条件,即该条entry committed时,leader会将命令输入状态机,并将输出返回给客户端,同时通过心跳通知其他follower可以Apply该entry。

共识算法有以下特点:

1.safety,在所有非拜占庭条件下(包括网络延迟,分区,丢包,duplication,reordering等),不会返回错误的结果

2.大部分节点正常话,系统就可以正常工作

3.不依靠物理时钟来确保日志的一致,错误的物理时钟和消息延迟最多会造成可用性问题

4.集群中的大多数节点在一轮rpc调用中正常响应的话,一个客户端的请求就会被正常返回,不会受部分慢节点的影响

任何时刻,一个server处于以下三个状态之一:leader,follower,candidate。

一般情况下,有1个leader,其他节点都是follower,follower是被动的,不会发送请求,只会响应leader和candidate的请求。

leader处理所有客户端的请求(如果客户端请求了follower,follower将请求重定向到leader)。

candidate状态用于选举一个新的leader,状态转换如下图

raft将物理时间分隔为一个个的任意长度的term,term是连续的。

每个term从election开始,一个或者多个candidate尝试竞选为leader,如果一个candidate赢得了选举,就会成为term的余下时间内的leader。

一些情况下,会产生split vote,term会以没有leader的状态结束,开始新一轮的term以及选举

raft确保一个term中最多只会有一个leader。

term是逻辑时钟,每个server存储一个current term number,current term number随着时间单调递增,当节点之间通信时,会交换current term number,

如果一个server发现自己的current term number小于其他节点的,该server会将自己的term更新为更大的term,

如果一个candidate或者leader发现有节点的term大于自己的term,就会转变为follower(有特殊情况),

如果一个节点接收到一个有着过期term number的请求,则会拒绝这个请求。

raft的server之间使用RPC通信,主要为两种类型的RPC,

RequestVote RPC:用于candidate选举

AppendEntries RPC:用于leader发送log entry给follower,或者心跳

另外还有一种InstallSnapshot RPC,用于传输snapshot

raft协议首先需要选举一个唯一的leader,leader接受客户端的命令,将这些命令复制到其他follower,通知follower什么时候可以将这些日志输入到状态机。data flow是单向的,从leader到follower。

raft使用心跳机制触发leader election,当一个server start up,起始状态是follower,只要收到leader和candidate的正确RPC请求,server就会保持follower的状态。

如果follower在一定时间内(election timeout)没有收到心跳,follower会认为当前没有leader,并开始竞选。

开始竞选时,follower增加自己的current term并将状态转换为candidate,然后会选举自己并发送RequestVote RPC请求给集群中的其他server。

一个candidate会保持自己的状态直到下面三种情况之一发生:

1.赢得选举

一个candidate在接收到集群中大多数节点对当前term的投票之后,赢得选举。每个server在一个term中,最多只会给一个candidate投票,first-come-first-served,

2.其他节点成为leader

如果candidate收到其他节点的RPC请求,而且请求中的term大于等于candidate的current term,candidate会认为已经选出leader,并返回到follower状态。

如果RPC请求中的term小于candidate的current term,candidate会拒绝该RPC请求。

3.一定时间内(election timeout)没有选举出leader

每个candidate都会time out,并且增加自己的term,开始新一轮的选举

election timeout是在一个固定范围内(例如150ms-300ms内)随机的

上述机制保证在一个term中,只有一个candidate会成为leader,当一个candidate成为leader,它会发送心跳信息给所有其他的节点。

当一个leader被选举出来之后,client发送请求给leader,leader将将请求作为一个新的entry添加到log中,然后并行的发送AppendEntries RPC请求(携带该entry)给follower。

leader判断当前是否可以安全地将entry apply到状态机中,此时该entry被叫做committed。然后leader将请求Apply到状态机,并返回执行结果。

log entry中会保存接受到entry时的term,以及一个用于标记log entry位置的index。

raft保证committed entries是持久化的,并最终会被所有的状态机执行。

当一个entry被leader replicate到集群中的大多数节点上时,该entry就是committed。

如果某条entry是committed的,该entry之前的entry也都是committed的,包括之前的leader创建的entry。

leader会记录committed的日志的最高index,并将该index包含在之后的AppendEntries RPC中(包括 heartbeats),

follower知道某个entry是committed,就会将该entry apply到状态机中。

AppendEntries Consistency Check:

当发送一个AppendEntries RPC,leader将新entries之前最近的log entry的index和term包含在RPC请求中。如果follower发现自己的log中没有该index和term的entry,就会拒绝新的entries。

类似于一个归纳的过程,最初的空的log满足Log Matching Property,当有新的log entry时,consistency check同样保证了新的log entry满足Log Matching Property。

这样,当AppendEntries请求返回成功的响应时,leader就知道follower的log在new entries之前的部分和自己的log一样。

一个新的leader被选举出来之后,follower的log可能和新的leader不一样,follower可能有leader没有的entry,也可能有老的leader没有commit的entry。

为了让follower的log和leader的完全一致,leader需要找到follower的log和自己的log分叉的地方,删除follower在分叉点之后的log entry,然后leader向follower发送自己在分叉点之后的log entry。

上述操作通过AppendEntries RPC来实现,leader会记录每个follower的nextIndex,即leader应该发送给这个follower的下一个log entry的index。

如果follower的log和leader的不一样,AppendEntries RPC会失败,leader减小nextIndex并重试。

如果需要,这个协议也可以优化,如果AppendEntries RPC失败,follower可以返回冲突的term,以及该term的第一个index。这样原来一个不同的entry就需要一个AppendEntries请求,现在一个term需要一个AppendEntries请求。

这样多个节点之间的日志就会收敛一致。同时,leader从不会覆盖或者删除自己的log entry,符合Leader Append-Only Property。

上述部分并不能完全保证每个状态机以相同的顺序执行相同的命令。

例如,一个follower可能在当前leader commit一些log entry的时候不可用,然后该follower被选举为新的leader后,就可能覆盖之前committed的日志,从而造成不同的状态机执行了不同的命令。

下面讨论leader election的限制,这些限制能保证任何term的leader都会包含之前term中committed的log entry。

RequestVote RPC请求包含candidate的log,如果voter的log比candidate的log更加up-to-date,voter会拒绝这次投票。

up-to-date:两个log,如果term不同,term更大的更新,如果term相同,日志更长的更新

一个leader不能立即判断出一个之前term的entry是否应该committed,即使该entry被存储到了大多数节点上。

(a)S1是leader,写入一条命令,index是2

(b)S1 crash,S5选举为leader,写入一条命令,index是3

(c)S5 crash,S1选举为leader,写入一条命令,index是4,并将index为2的log entry同步到S3,commit和apply index为2的log entry

(d)S1 crash,S5选举为leader,会覆盖掉index2,造成多个server的状态机apply不一样的log entry

因此,raft不会因为之前term的log entry被存储到了大多数节点上,就将该entry commit(raft never commits log entries from pervious terms by counting replicas),只有当前term的log entry被存储到大多数节点上时,才会判断该entry为commit

(only log entries from the leader's current term are committed by counting replicas)。这样,由于Log Matching Property,所有之前的entries都会间接地被commit掉。

raft使用two-phase的方案来处理configuration change,集群首先会切换到一个名叫joint consensus的中间状态,一旦joint consensus被committed了,集群就会使用新的configuration。

joint consensus将老的和新的configuration结合在一起:

集群configuration也是以log entry的方式存储和同步到其他server上。

当leader接收到configuration从C-old变为C-new的请求之后,将C-old,new的entry存储到log中,并同步到其他server上。

follower接收到entry后,无论该entry是否已经committed,都会使用entry包含的configuration替换当前的configuration。

如果leader crash,新的leader的configuration只可能是C-old或者C-old,new。

C-old,new被committed之后,leader创建一条C-new的entry,并同步到其他server上。

follower接收到该entry之后,无论该entry是否已经committed,都会使用C-new替换之前的configuration。

当C-new被committed之后,C-old中的节点就可以被shut down。

上述方案需要解决三个问题:

1.新加入的server需要很长时间才能追上leader,在这段时间内无法committed,为此raft引入了non-voting 成员

2.老的leader可能不在新的configuration中。为此,leader在C-new committed之后,leader需要变成follower

3.removed servers可能会影响集群。这些节点不会接收到心跳,然后time out,然后开始新一轮的选举。这会造成当前的leader变成follower,然后重新选举leader。上述过程会不断重复。

为此,server需要忽略RequestVote RPC,如果当前的leader没有time out。

snapshotting是log compacting的最简单的办法,状态机将当前系统状态被写进snapshot,之前的log entry会被删除。

每个server会独立的take snapshot,snapshot会包含log中已经committed的log entry。

snapshot中会包含少量的元数据,

last included index:状态机apply的最后一个log entry,也就是snapshot替换掉的最后一个log entry 的index。

last included term:上述entry的term

元数据用于snapshot之后的第一个log entry的AppendEntries consistency check,由于该entry需要之前的log的index和term。

元数据也包含最近的configuration。

对于一个刚加进集群的server,leader使用InstallSnapshot RPC发送snapshot给follower。

raft需要把所有的请求发送给leader,当一个client start,client连接集群中的任意一个节点,如果该节点不是leader,则会拒绝client的请求,并返回leader的信息(AppendEntries请求包含了leader的网络地址)。

如果leader crash,client请求会timeout,然后随机选择一个节点继续重试。

raft协议需要实现线性语义(linearizable semantics),每个操作会且只会执行一次(exactly once),但是仅靠之前提到的几点,raft协议的可能会让一个命令执行多次。

例如,leader在commit一个log entry,但是还没有来得及返回给client之后,就crash掉,client会在新的leader上重复发送相同的请求,造成该请求执行两次。

解决方法是client给每个命令一个序列号,状态机记录每个client最近执行的序列号。如果状态机收到一个命令,该命令的序列号是之前执行过的,就立即返回而不再执行该命令。

只读操作可能会读到过期的数据。因为client访问一个leader时,集群中选举出了其他leader,该leader马上就会变成follower。linear semantics不能返回过期数据。

raft的解决方案分两步,

首先,一个leader必须确认哪些entry是committed,Leader Completeness Property保证一个leader拥有所有committed的entry,但是在term的开始阶段,leader并不知道哪些是已经committed的。因此,leader需要在term的开始,先commit一个no-op entry。

然后,leader必须检查当前是否有其他leader被选举出来,将要取代自己的leader位置。raft在返回read-only请求的响应之前,需要和集群中的大多数节点发送心跳。

由浅入深理解Raft协议

0 - Raft协议和Paxos的因缘

读过Raft论文《In Search of an Understandable Consensus Algorithm》的同学都知道,Raft是因为Paxos而产生的。Paxos协议是出了名的难懂,而且不够详细,紧紧依据Paxos这篇论文开发出可用的系统是非常困难的。Raft的作者也说是被Paxos苦虐了无数个回合后,才设计出了Raft协议。作者的目标是设计一个足够详细并且简单易懂的“Paxos协议”,让开发人员可以在很短的时间内开发出一个可用的系统。


Raft协议在功能上是完全等同于(Multi)-Paxos协议的。Raft也是一个原子广播协议(原子广播协议参见《),它在分布式系统中的功能以及使用方法和Paxos是完全一样的。我们可以用Raft来替代分布式系统中的Paxos协议如下图所示:

1 - Raft的设计理念

严格来说Raft并不属于Paxos的一个变种。Raft协议并不是对Paxos的改进,也没有使用Paxos的基础协议(The Basic Protocol)。Raft协议在设计理念上和Paxos协议是完全相反的。正是由于这个完全不同的理念,使得Raft协议变得简单起来。


Paxos协议中有一个基本的假设前提:可能会同时有多个Leader存在。这里把Paxos协议执行的过程分为以下两个部分:

  • Leader选举

  • 数据广播

在《》的“Leader的选取”一节中提到过,Paxos协议并没有给出详细的Leader选举机制。Paxos对于Leader的选举没有限制,用户可以自己定义。这是因为Paxos协议设计了一个巧妙的数据广播过程,即Paxos的基本通讯协议(The Basic Protocol)。它有很强的数据一致性保障,即使在多个Leader同时出现时也能够保证广播数据的一致性。


而Raft协议走了完全相反的一个思路:保证不会同时有多个Leader存在。因此Raft协议对Leader的选举做了详细的设计,从而保证不会有多个Leader同时存在。相反,数据广播的过程则变的简单易于理解了。


2 - Raft的日志广播过程

为了保证数据被复制到多数的节点上,Raft的广播过程尽管简单仍然要使用多数派协议,只是这个过程要容易理解的多:

  1. 发送日志到所有Followers(Raft中将非Leader节点称为Follower)。

  2. Followers收到日志后,应答收到日志。

  3. 当半数以上的Followers应答后,Leader通知Followers日志广播成功。


- 日志和日志队列

Raft将用户数据称作日志(Log),存储在一个日志队列里。每个节点上都有一份。队列里的每个日志都一个序号,这个序号是连续递增的不能有缺。

由浅入深理解Raft协议

日志队列里有一个重要的位置叫做提交日志的位置(Commit Index)。将日志队列里的日志分为了两个部分:

  • 已提交日志:已经复制到超过半数节点的数据。这些日志是可以发送给应用程序去执行的日志。

  • 未提交日志:还未复制到超过半数节点的数据。

由浅入深理解Raft协议

当Followers收到日志后,将日志按顺序存储到队列里。但这时Commit Index不会更新,因此这些日志是未提交的日志,不能发送给应用去执行。当Leader收到超过半数的Followers的应答后,会更新自己的Commit Index,并将Commit Index广播到Followers上。这时Followers更新Commit Index,未提交的日志就变成了已提交的日志,可以发送给应用程序去执行了。


从上面的解释我们可以知道,日志队列中已经提交的日志是不可改变的,而未提交的日志则可以被更新成其他的日志(在Leader发生变化时会发生)。


Raft的日志队列和《》中的“预存储队列+存储队列”功能是一样的,但是巧妙的合并到了一起。这样做解决的问题和《》“预存储队列+存储队列”解决的问题也是一样的,这里就不再叙述。


3 - Raft的Leader选举

Raft称它的Leader为“Strong Leader”。Strong Leader 有以下特点:

  • 同一时间只有一个Leader

  • 只能从Leader向Followers发送数据,反之不行。

下面我们看一下Raft通过哪些机制来实现Strong Leader。


- 多数派协议

为了保证只有一个Leader被选举出来,选举的过程使用了多数派协议。这样很好理解,当一个Candidate(申请成为Leader的节点)请求成为Leader时,只有半数以上的Followers同意后,才能成为Leader。投票过程如下:

  1. 当发现Leader无响应后(一段时间内没有日志或心跳),Candidate发送投票请求。

  2. Followers投票。

  3. 如果超过半数的Followers投了票,则Candidate自动变成Leader,开始广播日志。


- 随机超时机制

和《》中提到问题一样,这里也会发生多个Candidate同时发送投票请求,而导致谁都不能够得到多数赞成票的情况,有可能永远也选不出Leader。为了保证Leader选举的效率,Raft在投票选举中使用了随机超时的机制:

  1. 在每个Followers上设定的Leader超时时间是在一个范围内随机的。这样可以尽量让Followers不在同一时间发起Leader选举。

  2. 每个Candidate发起投票后,如果在一段时间内没有任何Candidate称为Leader则,需要重新发起Leader选举。这段等待的时间,在每个Candidate上也是随机的。从而保证不会有多个Candidate同时重新发起Leader选举。

虽说是随机的超时时间,但是也有个范围,太小或者太大都会影响系统的可用性。太小会导致过多的选举冲突,太大又会影响系统的平滑运行。在Raft的论文中,作者将这个超时时间称为electionTimeout,并给出了合理的范围,公式如下:

broadcastTime ≪ electionTimeout ≪ MTBF

“≪”代表数量级上的差异(10倍以上)。


- Candidate的日志长度要等于或者超过半数节点才能选为Leader

当Leader故障时,Followers上日志的状态很可能是不一致的。有的多有的少,而且Commit Index也不尽相同。


由浅入深理解Raft协议

我们知道已经提交的日志是不能够丢弃的,必须要最终复制到所有的节点上才行。假如在选Leader时,图中Candidate A变成了Leader,就必须要首先从Candidate B上将日志4复制过来,然后才能开始处理新的日志。为了减少复杂性,raft就规定,只有包含了所有已提交日志的Candidate才能当选为Leader。

实现也很简单:

  • 当发现Leader无响应后(一段时间内没有数据或心跳),Candidate发送投票请求,请求中包含自己日志队列的长度(或者说最大日志的Index)

  • Followers检查Candidate的日志长度,只有Candidate的日志等于或者长于自己才投票。

  • 如果超过半数的Followers投了票,则Candidate自动变成Leader,开始广播数据。

因为已经提交的日志一定被复制到了多数节点上,所以日志长度等于或者长于多数节点的Candidate一定包含了所有已经提交的日志。


为什么不是检查Commit Index?

因为Leader故障时,很有可能只有Leader的Commit Index是最大的。

由浅入深理解Raft协议

如果图中的Candidate A被选举为Leader,那么日志4就会被丢弃。但是日志4已经在原来的Leader上提交了,因此必须被保留才行。所以只能让日志长度更长的Candidate B选为Leader。这种做法有可能把原来Leader没广播完成的日志(图中的日志5)接着广播完成,这没有什么关系。


- Followers日志补齐

当Leader故障时,Followers上的日志状态是不一样的,有长有短。因此新的Leader选出后,首先要将所有Followers的日志补齐才行。因此Leader要询问Followers的日志长度,从最小的日志位置开始补齐。


- Followers未提交日志的更新

新Leader的日志一定包含所有已经提交的日志。但新Leader的日志不一定是最长的,那些新Leader没有的日志,一定是未提交的日志,因此可以被更新,没有关系的。Leader只需要从自己的当前位置开始插入日志并广播出去就可以了。Followers会用新的日志去更新指定位置上的日志。


4 - 新旧Leader的交替
新的Leader选出后,开始广播日志。这时如果旧的Leader故障恢复了(比如网络临时中断),并且还认为自己是Leader,也会广播日志。这不就导致了同时有两个Leader出现吗?是的,Raft也没办法让旧的Leader不发日志,但是Raft有办法让Followers拒绝旧Leader的日志。

- Term
Raft将时间划分为连续的时间段,称为Term。 Term是指从一次Leader选举开始到下一次Leader选举的一段时间。这段时间内只能有一个Leader被选举成功,并负责管理系统或者没有Leader选出。



Raft论文上的Term图片

每个Term都有一个唯一的数字编号。所有Term的数字编号是从小到大连续排列的。


- 作废旧Leader

Term编号在作废旧Leader的过程中至关重要,但却十分简单。过程如下:

  1. 发送日志到所有Followers,Leader的Term编号随日志一起发送

  2. Followers收到日志后,检查Leader的Term编号。如果Leader的Term编号等于或者大于自己的当前Term(Current Term)编号,则存储日志到队列并且应答收到日志。否则发送失败消息给Leader,消息中包含自己的当前Term编号。

  3. 当Leader收到任何Term编号比自己的Term编号大的消息时,则将自己变成Follower。收到的消息包括:Follower给自己的回复消息、新Leader的日志广播消息、Leader的选举消息。


- Raft的实现

论文中作者仅用了两个RPC就实现了Raft的功能,它们分别是:

  • RequestVote() Candidate发起的投票请求

  • AppendEntries() 将日志广播到Followers上

AppendEntries()除了广播日志外,作者还巧妙的用它实现了以下的功能:

  • 发送心跳(heartbeat): 没有客户日志时,通过AppendEntries()广播空日志,当做心跳。

  • 发送Commit Index:当Commit Index更新后,可以随着当前的日志通过AppendEntries()广播到Followers上。如果没有客户端日志,则可以随着心跳广播出去。



以上是关于Raft协议的主要内容,如果未能解决你的问题,请参考以下文章

raft 和 zab协议

聊聊分布式一致性协议---Raft协议

Raft协议简析

Raft协议处理各种failover情况

raft协议

Raft 协议学习笔记