Raft 分布一致性协议

Raft 分布一致性协议

1. 简介

Raft 协议用于在分布式系统中管理机器之间的日志复制。Raft协议与已有的共识(consensus)算法(如 Viewstamped Replication)有很多相似的地方,但是Raft有其独有的特点:

  • Leader拥有更大的权限,相比于其他的共识算法,Raft算法中的Leader有更大的能力。比如,日志只能从Leader 传送到其他的server。这种方式可以简化日志的复制管理;
  • Leader选举,Raft的Leader选举中,使用到了随机的计时器,该计时器建立在heartbeats 机制之上,并且使用这种随机的计时器可以更简单快速的解决选举中遇到的冲突问题;
  • 集群中成员的变化,Raft算法在集群中的成员变化的时候,使用了一种joint consensus的方式,就是在更新过程中的某个时间段,旧的与新的配置都会使用,这样可以保证集群能够在配置变化的过程中能够正常的运行。

2. 复制状态机(replicated state machine)

在分布式系统中,多个server上完成相同的计算,这样即使某些server宕机了整个系统也能完成运算。在分布式系统中,复制状态机被用于解决一些误差容忍(fault tolerance)的问题。

在这里插入图片描述
在GFS,HDFS,RAMCloud等大型系统中,使用的是单个集群Leader的方式,因此使用相互分开的复制状态机管理Leader的选举以及保存配置信息等任务,这样能够避免Leader宕机带来的问题。
复制状态机是一种理论上的概念,其在实际实现中一般是一个复制日志(replicated log)。图1中,每个server都保存一个包含一系列命令的日志,并且这些日志是相同的。而共识算法就是用来保证这些日志一致性的算法。在server上,共识模块(consensus module)从客户端接收命令并添加到日志中,并且与其他server上的共识模块通信来保证即使一些server宕机日志也是相同的。
在实际的应用系统中,共识算法典型的属性包括:

  • 保证安全(safety),即使在网络延迟、包丢失、重复、失序等情况下也会返回正确的结果;
  • 在大多数server都可用的情况下,系统内的server之间以及与客户端之间可以正常的通信。一个包含5个server的集群能够容忍2台server宕机;
  • 日志的一致性不依赖于时钟,因为错误的时钟以及极端的通信延迟会导致可用性(availability)问题;
  • 通常情况下,当大多数server都完成运算后,该命令就应该是执行完毕的了,少数执行缓慢的server不应该影响系统的整体性能。

3. Raft 共识算法

Raft算法会首先选举一个唯一的Leader完全负责对复制日志的管理。Leader从客户端接收日志并将它复制到其他的server,并告知何时将这些日志条目应用到状态机是安全的。
Raft算法将要解决的问题分解成三个相对独立的子问题:

  • Leader选举,当现有的Leader宕机后,新的Leader必须被选出;
  • Log 复制,Leader必须接收从client来的log并将该日志复制到集群中的其他的server上,并使得其他server上的日志与自己的相同;
  • 安全性,Raft算法的安全性的核心是状态机的安全一致。如果集群中某个server将某个日志条目应用到状态机上,那么其他的server不应该在相同位置应用不同的命令,即要保证集群中各个server的状态机的状态一致性。

Raft算法还有一些其他的属性:

  • 选举安全,每个阶段(term)最多只有一个leader被选出来。当然,对于没有选出leader的情况,比如没有哪个候选server得到大多数的投票,则会立即开始下一轮选举;
  • Leader只会添加(append)log条目,Leader不会覆盖或者是删除log中的条目,只会添加;
  • 日志一致性,如果两个日志在相同的位置(index),相同的阶段(term)的条目是相同的,则此之前的所有条目都应该是相同的。

一个Raft集群通常应该有5个server,这样能够容忍两台server宕机。在每个时刻,每个server有三种可能的身份,leader、follower、candidate。Follower不发送请求,它只会对leader以及candidate的请求进行回应。Leader处理所有的客户端的请求,如果客户端将请求发给了follower,则follower将该请求重定向到leader。Candidate是有可能称为leader的server。
Raft将时间分成一个个的term,这些term并不等长,并且连续编号。每个term在起始时刻是选举过程,此时candidates尝试成为leader,当某个candidate成为leader之后,则在该term的接下来的时间,该server负责leader的工作。在某些特殊的情况下,可能没能选举出一个leader,那么开始下一个term新的选举。

server状态之间的转换以及条件如下图所示。
在这里插入图片描述
下图中,term1的开始阶段进行选举,选举出一个leader之后便是正常的处理工作。在term之间,可能leader宕机或某种原因,需要选举新的Leader。在term3,没能选举出新的leader,则会进入到下一个term继续选举。
在这里插入图片描述
每个server都会保存当前所在的term,并且该term在集群内server之间通信的时候会进行比较。如果某个server当前的term比另外一个server的term小,那么该server将当前的term更新到这个更大的值。如果成为candidate或者leader的server发现它的term过时了,则会立即转为follower状态。如果server接收到的request附带的term比自己的小,则忽略该请求。

Raft 的服务器之间使用远程过程调用(Remote Procedure Calls, RPCs)进行通信。对于基本的共识算法,仅需要两种类型的RPC,RequestVote RPC以及AppendEntries RPC。其中RequestVote RPC是在term的开始,由candidate servers 发起;AppendEntries RPC由leader发起用于复制日志以及发送heartbeat 使得其他的server感知到当前的leader。

3.1 Leader选举的详细过程

集群中的server在一开始的时候都是followers,并且只要server能够接收到来自leader 或者candidate 的合法的RPC,那么该server就仍然保持follower状态。这里需要强调这个合法的RPC,如果term比该server的term要小,则该RPC不是合法的。Leader需要定期的向followers发送heartbeat(即不包含log 条目的 AppendEntries RPC)维持自己的leader身份。如果某个server 在一段时间之内没有收到这种heartbeat,即election timeout,则该server认为当前没有leader,并且开始新的选举。
当新的选举开始时,follower将当前term +1并且变为candidate,为自己投票并且发起RequestVote RPC给集群中的其他的server。接下来,有三种可能的情况发生:

  • 该serevr成为leader
  • 另外一个server成为leader;
  • 没有leader产生

一个candidate只有获得当前term的大多数投票才能成为leader,这样可以确保最多只有一个server成为leader。
在candidates等待选票的时候,可能会从其他的server接收到AppendEntries RPC声明成为leader。那么需要判断一下发送这个RPC的server的term,如果该term 小于当前该candidate的term,则仍保持candidate状态;否则承认该server 成为leader并且该candidate退回follower状态。
当许多follower是candidate的情况下,由于candidate都会给自己投一票,且每个server只能投一票,这样可能会导致最终没能选出leader。当这种情况发生时,每个candidate都会超时并且重新开始新的选举过程,term+1,以及发起RequestVote RPC。但是这样处理,candidate与之前一样多,可能会重复出现之前的问题。
Raft为了避免这样的问题,使用了随机选举超时 (randomized election timeouts)保证这种candidate较多导致选票过度分散的情况尽量减少,并且选举过程能够更快的完成。选举的超时时长从某个固定的区间中选择,比如 150 ∼ 300 m s 150\sim 300 ms 150300ms。这样,大多数情况下,只有一个server会率先超时并赢得选举。

3.2 日志复制(Log Replication)

当集群系统选举出一个Leader后,系统从客户端接收包含状态机需要执行的一系列指令。Leader将这些指令添加在log中,并向集群中的其他的server发送AppendEntries RPC将日志复制到其他的server上。当日志被认为已经安全的复制到其他的server上之后,leader便将log上的条目应用到状态机上面并将结果反馈给客户端。如果某个follower宕机或者是运行过慢、网络包丢失等问题出现,leader会将日志不停的通过AppendEntries RPC传给该server,即使leader已经将结果反馈给了客户端。
在前面一段中,当日志被认为已经安全的复制到其他的server上之后,leader才会将log中的entries应用到状态机上。这里"被认为安全"是当log entry 被复制到大多数的server上。不仅当前leader 已确认safe的log entries 会被执行,此entry之前的log entries也会被提交执行,尽管这些entries可能是之前term中的leader创建的。
Raft中对于日志的匹配属性有很强的约束:

  • 不同server具有相同term以及index的log entry,存储的是相同的指令;
  • 不同server具有相同term以及index的log entry,在此之前所有的entries 都是相同的

其中第一个属性是基于下面这个事实,即在某个term,leader最多会在log的某个index创建一个entry,并且log中的entry不会改变位置。第二条属性是通过AppendEntries的一致性检查来保证的。如果系统正常工作,常规的添加log entry的操作不会破坏日志的一致性,但是当出现leader宕机的时候,可能会导致日志的不一致,比如之前term的leader可能没有完成日志的复制。下图中包括entry缺失、follower包含未提交的entry的情况。
在这里插入图片描述
为了保证leader的日志与follower的日志一致,leader需要找到和follower日志最新的能够一致的位置,将follower日志中该位置之后的删除掉,并将leader日志中该位置之后的entry复制到follower中。因此,leader会对每个follower记录一个nextIndex字段,表示leader要给follower发送的日志起始位置。当某个server 刚刚成为leader后,初始化这些nextIndex为自己当前的log entry的最大的index+1。如果follower的log与leader的不一致,则AppendEntries一致性检查就会失败,之后leader对nextIndex-1,并重试。最终nextIndex就会指向leader与follower最近的同步的位置,之后就是follower上的log 删除操作,leader将该位置之后的entry复制到follower上。
上面的这个操作还可以进一步的优化,因为每个term中leader与follower的日志是保持一致的,因此在递减nextIndex的过程中,如果在某个term日志一致性检查不一致,则nextIndex可以减到前一个term的位置。论文中对这种操作持保留态度,认为系统中的机器不会频繁出现问题,日志不一致的情况不会频繁发生。

3.3 安全性

针对日志复制问题,在某些极端的情况下,比如某个term,leader在复制日志的时候,某个follower是宕机的,后面某个term,则个follower成为leader,这样会将一些已经提交的log entries覆盖掉。因此,这一部分,论文中对哪些server可以成为leader进一步添加了约束条件,确保新选出来的leader包含了此前term所有的log entries。
在任何基于leader的共识算法中,leader最终必须包含所有已提交的log entries。在某些共识算法中,例如 Viewstamped Replication中,那些没有包含所有已提交的log entries的server也可以被选举为leader。因此,这些算法需要其他的一些机制来填补leader缺失的log entries。而Raft算法则保证了新的leader中包含了所有之前term已提交的log entries。因此,Raft中,log entries只会从leader传送给followers。
Raft的投票过程中,candidate需要与集群中的大多数server进行通信,而完整的已提交的log entries必包含在这些server中。如果candidate的log entries不比这些server中的log更旧,则该candidate包含了所有已提交的log entries。
Raft判断哪个log是比较新的是根据下面这些来判断的。Raft比较log中最后一个entry的index与term的值,如果term不同,则term值更大的是更新的;如果term相同,则哪个log更长哪个是更新的。
根据前面的规则,当leader将log entry复制到大多数的server上时,该log entry就被认为是已经提交的。如果leader在提交entry之前宕机了,后面term的leader会尝试完成entry的复制。但是leader只有当entry复制到大多数的server上之后才能知道该entry是否已提交,这样就可能会出现下图这种问题。在(a)阶段,S1为leader,将log中index为2的entry复制到其他的server上去。在(b)阶段,S1宕机,S5成为leader,但是S1在上一个term仅完成将entry复制到S2的操作,此时S5在index 2位置添加了新的entry。在( c)阶段,S5宕机,S1重新成为leader,继续完成log entry的复制操作,此时index 2的entry已经复制到大多数的server上,但还未提交。在(d)阶段,S1宕机,S5成为leader,将自己index 2处的log entry复制到其他的server上,这样就会把S1复制的log entry覆盖掉。如果S1在( c)阶段在宕机之前提交完成,即(e)所示,则按照规则,S5是不能被选举为leader的,这样log就不会被覆盖掉了。
在这里插入图片描述
为了解决上图所示的问题,Raft不按照复制到其他server的个数来提交前面term的log entries。只有leader当前term的log entries通过这种复制到大多数server的规则判断来提交。一旦当前term的entry提交了,那么之前的所有entries都按照日志匹配属性被提交。
下面通过反证法证明Raft的这种策略的安全性。首先设term T T T的leader提交了log entry,但是后面term的leader没有保存这个log entry,假设后面最近的term U U U中的leader没有该log entry。那么可以得到:

  • l e a d e r U leader_{U} leaderU被选出来的时候,该log entry不在该leader的log中;
  • l e a d e r T leader_{T} leaderT将entry复制到集群中的大多数的server上,并且 l e a d e r U leader_{U} leaderU得到了大多数的选票才成为了term U U U的leader。因此,至少有一个投票的server既从 l e a d e r T leader_{T} leaderT接收了log entry又把票投给了 s e r v e r U server_{U} serverU;
  • 该voter一定是在给 l e a d e r U leader_{U} leaderU投票之前接收的 l e a d e r T leader_{T} leaderT的log entry。因为如果在此之后,由于term 会比 l e a d e r T leader_{T} leaderT推送的log entry的term值更大,则AppendEntries RPC会被拒绝。
  • 根据修改后的投票原则,该voter将票投给了 l e a d e r U leader_{U} leaderU,那么 l e a d e r U leader_{U} leaderU的log 至少不会比该voter旧。这样就与假设矛盾。

以上内容探讨了针对leader的各种问题的分析。对于follower以及candidate的问题相比leader的要更简单一些。如果follower或者candidate宕机,那么后面的RequestVote或者AppendEntries RPC会失败。Raft处理这种问题的方法是不断的重试。
如果问题发生在接收到RPC之后,但是还没有response,则该follower或者candidate会重新接收到该RPC。

3.4 时间与可用性(availability)

Raft算法要求系统的安全性不应该依赖于时间,即不应该因为某些异常情况发生的过快或者过慢而导致产生错误的结果。但是可用性不可避免的要依赖于时间。
Raft选举产生leader的过程中,时间是很重要的,并且选举的时间需要满足下面的不等式:
广 播 时 间 ≪ e l e c t i o n    t i m e o u t ≪ M T B F 广播时间 \ll election\ \ timeout \ll MTBF 广election  timeoutMTBF
其中广播时间(broadcastTime)是server给集群内的其他server发送RPC以及接收反馈的平均时长,election Timeout 是前面提到的用于选举的一个之间,即某个server在该timeout之后感知不到leader就发起选举,MTBF是对单个server而言两次宕机之间的平均时间。

4. 集群成员变更(cluster membership changes)

集群的配置有可能发生变化,比如某个机器坏掉了,需要从集群中移除、向集群中添加一些机器等。尽管可以将整个集群停掉,重新配置好了之后再重启,但是在这个时间内,整个集群是不能向外提供服务的。论文中提出了自动的配置变更策略并与Raft算法协同。
在配置变更的过程中,同样需要保证系统遵循前面的那些规则,比如不能在某个时间点出现两个leader的情况。直接从旧的配置切换到新的配置是不安全的,因为不可能一次完成所有server的转换,所以可能会导致出现如下图所示的两个’大多数’的情况。
在这里插入图片描述
因此,为了保证安全性,配置的变更需要分两步走。首先进入到一个过渡期,称为joint consensus,之后再进入到完全使用新配置的阶段。
在过渡阶段,由于同时使用新旧两个配置,所以需要遵循如下规则:

  • log entry需要复制到server的新旧两个配置中;
  • 新旧配置中的server都可以称为leader;
  • 成为leader需要在新旧两个配置中都获得大多数的投票
    如下图所示,在第一阶段,leader创建了 C o l d , n e w C_{old,new} Cold,new配置entry,该entry commit 之后,进入到第二阶段。在第二个阶段, C o l d 与 C n e w C_{old}与 C_{new} ColdCnew同时使用,此时成为leader需要同时获得新旧配置中大多数的投票才可以。在整个过程中,不存在新旧配置同时且独立决策的。在中间的过渡阶段,如果leader不在新的配置中,则在提交 C n e w C_{new} Cnew 之后,该leader变成follower身份。
    在这里插入图片描述
    在新的server加入到集群中时,该server可能没有任何的log entry。因此在加入之后,需要一段较长的时间补充log entry,这样可能使得log entry 无法被提交。为了避免这种情况对系统可用性的影响,Raft引入了一个额外的阶段,该阶段在配置变更之前。在该阶段,新加入的server作为一个非投票的成员,leader将log entries复制到该server,但是在投票选举leader的时候,该server不参与投票以及计数。当该server的log entry赶上来后,就开始前面提到的配置变更。
    对于删除server的情况,由于被删除的server不能再接收到leader的heartbeat,因此会超时并发起新的选举,而这一情况还会不断的重复。因此,为了避免这种情况,server在距离上次从leader接收到消息未超过election timeout的情况下,对接收到的RequestVote RPC拒绝接受。这并不影响正常的选举过程,并且还能够避免配置修改删除某些server导致系统可用性降低的问题。

5. 日志压缩(log compaction)

系统运行中不断的接受并处理客户端的请求,log的内容会越来越多,导致占用大量的磁盘空间。并且log的内容过多,会使得重放(reply)的时间变得更长。
快照技术(snapshot)是精简log的一种比较简单的方式。系统的状态snapshot存储在磁盘上,在此之前的log的内容都可以删除掉。
在这里插入图片描述
Leader使用InstallSnapshot RPC将snapshots发送给落后过多的followers。Snapshot与Raft的强leader的准则相违背,因为followers可以在不知道leader的情况下使用snapshot,但是文中认为这是合理的,因为在snapshot的时候,已经达成了共识。
在使用snapshot的时候有两点影响性能的问题需要考虑。一是server合适进行snapshot,过于频繁的snapshot会占用磁盘带宽,反之则会导致磁盘空间被日志大量占用。一个简单的策略是当log的大小达到某个预设的字节数的时候进行snapshot。二是写入snapshot会花费比较多的时间,可采用写时复制技术(copy-on-write),在Linux上可使用fork实现。

6. 与客户端的交互

Raft中,客户端只与leader进行交互。如果客户端率先启动,则会随机连上一个server,如果该server不是leader,则该server会拒绝该请求,并将最新的leader信息发送给客户端。如果leader宕机,则客户端的请求会超时,则客户端重试,随机连上一个server,接下来的过程与前面的相同。
对于只读(read-only)的操作,可以不写入到log中。在没有其他措施的情况下,这可能会导致返回过时的数据,比如当leader返回数据时没有注意到此时已经有新的leader了。
使用线性化(linearizable)的操作可以避免出现这种返回过时数据的问题。Raft需要额外的两个措施来保证。其一,leader必须包含最新的提交的entry信息,但是在term的开始时刻,可能并不知道哪一个是leader,这时就需要leader在term的开始时刻提交一个空白的no-opentry到log中。其二,leader需要在处理只读请求之前判断是否已经不是leader了,Raft通过leader与大多数的server交换heartbeat信息来确认。

参考

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值