Raft算法详解

  一致性算法Raft详解

背景

  熟悉或了解分布性系统的开发者都知道一致性算法的重要性,Paxos一致性算法从90年提出到如今已经有二十几年了,而Paxos流程太过于繁杂实现起来也比较复杂,可能也是觉得过于复杂 如今我据说过比较出名使用到Paxos的也就只是Chubby、libpaxos,搜了下发现Keyspace、BerkeleyDB数据库中也使用了该算法做为数据的一致性同步,虽然如今很普遍使用的Zookeeper也是基于Paxos算法来实现,可是Zookeeper使用的ZAB(Zookeeper Atomic Broadcast)协议对Paxos进行了不少的改进与优化,算法复杂我想会是制约他发展的一个重要缘由;说了这么多只是为了要引出本篇文章的主角Raft一致性算法,没错Raft就是在这个背景下诞生的,文章开头也说到了Paxos最大的问题就是复杂,Raft一致性算法就是比Paxos简单又能实现Paxos所解决的问题的一致性算法。
  Raft是斯坦福的Diego Ongaro、John Ousterhout两我的以易懂(Understandability)为目标设计的一致性算法,在2013年发布了论文:《In Search of an Understandable Consensus Algorithm》从2013年发布到如今不过只有两年,到如今已经有了十多种语言的Raft算法实现框架,较为出名的有etcd,Google的Kubernetes也是用了etcd做为他的服务发现框架;因而可知易懂性是多么的重要。算法

Raft概述

  与Paxos不一样Raft强调的是易懂(Understandability),Raft和Paxos同样只要保证n/2+1节点正常就可以提供服务;众所周知但问题较为复杂时能够把问题分解为几个小问题来处理,Raft也使用了分而治之的思想把算法流程分为三个子问题:选举(Leader election)、日志复制(Log replication)、安全性(Safety)三个子问题;这里先简单介绍下Raft的流程;
  Raft开始时在集群中选举出Leader负责日志复制的管理,Leader接受来自客户端的事务请求(日志),并将它们复制给集群的其余节点,而后负责通知集群中其余节点提交日志,Leader负责保证其余节点与他的日志同步,当Leader宕掉后集群其余节点会发起选举选出新的Leader;数据库

Raft简介数组

Raft是一个用于日志复制,同步的一致性算法。它提供了和Paxos同样的功能和性能,可是它的算法结构与Paxos不一样。这使得Raft相比Paxos更好理解,而且更容易构建实际的系统。为了强调可理解性,Raft将一致性算法分解为几个关键流程(模块),例如选主,安全性,日志复制,经过将分布式一致性这个复杂的问题转化为一系列的小问题进而各个击破的方式来解决问题。同时它经过实施一个更强的一致性来减小一些没必要要的状态,进一步下降了复杂性。Raft还包括了一个新机制,容许线上进行动态的集群扩容,利用有交集的大多数机制来保证安全性。安全

####一些背景知识网络

#####A. 一致性算法简单回顾框架

一致性算法容许一个集群像一个总体同样工做,即便其中一些机器出现故障也不会影响正常服务。正由于如此,一致性算法在构建可信赖的大型分布式系统中都扮演着重要的角色。Paxos算法在过去10年中统治着这一领域:绝大多数实现都是基于Paxos,同时在教学领域讲解一致性问题时Paxos也常常拿来做为范例。可是不幸的是,尽管有不少工做都在试图下降它的复杂性,可是Paxos仍然难以理解。而且,Paxos自身算法的结构不能直接用于实际系统中,想要使用必需要进行大幅度改变,这些都致使不管在工业界仍是教育界,Paxos都让人很DT。分布式

时势造英雄,在这样的背景下Raft应运而生。Raft算法使用了一些特别的技巧使得它易于理解,包括算法分解(Raft主要分为选主,日志复制和安全三个大模块),同时在不影响功能的状况下,减小复制状态机的状态,下降复杂性。Raft算法或多或少的和已经存在的一些一致性算法有着类似之处,可是也具备以下特征:性能

  • 强leader语义:相比其余一致性算法,Raft使用加强形式的leader语义。举个例子,日志只能由leader复制给其它节点。这简化了日志复制须要的管理工做,使得Raft易于理解。
  • leader的选择:Raft使用随机计时器来选择leader,它的实现只是在心跳机制(任何一致性算法中都必须实现)上多作了一点“文章”,不会增长延迟和复杂性。
  • 关系改变:Raft使用了一个新机制joint consensus容许集群动态在线扩容,保障Raft的可持续服务能力。

Raft算法已经被证实是安全正确的,同时也有实验支撑Raft的效率不比其余一致性算法差。最关键是Raft算法要易于理解,在实际系统应用中易于实现的特色使得它成为了解决分布式系统一致性问题上新的“宠儿”。优化

#####B. 复制状态机spa

一致性算法是从复制状态机中提出的。简单地讲,复制状态机就是经过彼此之间的通讯来将一个集群从一个一致性状态转向下一个一致性状态,它要能容忍不少错误情形。经典如GFS,HDFS都是使用了单独的复制状态机负责选主,存储一些可以在leader crash状况下进行恢复的配置信息等,好比Chubby和ZooKeeper。

复制状态机的典型实现是基于复制日志,以下图所示:

每一个server上存储了一个包含一系列指令的日志,这些指令,状态机要按序执行。每个日志在相同的位置存放相同的指令(能够理解成一个log包含一堆entry,这些entry组成一个数组,每一个entry是一个command,数组相同偏移处的command相同),因此每个状态机都执行了相同序列的指令。一致性算法就是基于这样一个简单的前提:集群中全部机器当前处于一个一致性状态,若是它们从该状态出发执行相同序列的指令走状态机的话,那么它们的下一个状态必定一致。但因为分布式系统中存在三态(成功,失败,无响应),如何来确保每台机器上的日志一致就很是复杂,也是一致性算法须要解决的问题。一般来说,一致性算法须要具备如下特征:

  • 安全性:在非拜占庭故障下,包括网络分区,延迟,丢包,乱序等等状况下都保证正确。
  • 绝对可用:只要集群中大多数机器正常,集群就能错误容忍,进行正常服务。
  • 不依赖时序保证一致性:因为使用逻辑时钟,因此物理时钟错误或者极端的消息延迟都不影响可用性。
  • 一般状况下,一个指令能够在一轮RPC周期内由大多数节点完成,宕机或者运行速度慢的少数派不影响系统总体性能。

####Raft算法

前面对Raft算法进行了简要的介绍,这里开始对它进行深刻分析。Raft实现一致性的机制是这样的:首先选择一个leader全权负责管理日志复制,leader从客户端接收log entries,将它们复制给集群中的其它机器,而后负责告诉其它机器何时将日志应用于它们的状态机。举个例子,leader能够在无需询问其它server的状况下决定把新entries放在哪一个位置,数据永远是从leader流向其它机器。一个leader能够fail或者与其余机器失去链接,这种情形下会有新的leader被选举出来。

经过leader机制,Raft将一致性难题分解为三个相对独立的子问题:

  • Leader选举:当前leader跪了的状况下,新leader被选举出来。
  • 日志复制:leader必须可以从客户端接收log entries,而后将它们复制给其余机器,强制它们与本身一致。
  • 安全性:若是任何节点将偏移x的log entry应用于本身的状态机,那么其余节点改变状态机时使用的偏移x的指令必须与之相同。

#####A. Raft基本知识

一个Raft集群包含单数个server,5个是一个典型配置,容许该系统最多容忍两个机器fail。在任什么时候刻,每一个server有三种状态:leader,follower,candidate。正常运行时,只有一个leader,其他全是follower。follower是被动的:它们不主动提出请求,只是响应leader和candidate的请求。leader负责处理全部客户端请求(若是客户端先链接某个follower,该follower要负责把它重定向到leader)。第三种状态candidate用于选主。下图展现了这些状态以及它们之间的转化:

Raft将时间分解成任意长度的terms,以下图所示:

terms有连续单调递增的编号,每一个term开始于选举,这一阶段每一个candidate都试图成为leader。若是一个candidate选举成功,它就在该term剩余周期内履行leader职责。在某种情形下,可能出现选票分散,没有选出leader的状况,这时新的term当即开始。Raft确保在任何term都只可能存在一个leader。term在Raft用做逻辑时钟,servers能够利用term判断一些过期的信息:好比过期的leader。每台server都存储当前term号,它随时间单调递增。term号能够在任何server通讯时改变:若是某台server的当前term号小于其它servers,那么这台server必须更新它的term号,与它人保持一致;若是一个candidate或者leader发现本身的term过时,它就必需要“放下身段”变成follower;若是某台server收到一个过期的请求(拥有过期的term号),它会拒绝该请求。Raft servers使用RPC交互,基本的一致性算法只须要两种RPC。RequestVote RPCs由candidate在选举阶段发起。AppendEntries RPCs在leader复制数据时发起,leader在和其余人作心跳时也用该RPC。servers发起一个RPC,若是在没获得响应,则须要不断重试。另外,发起RPC是并行的。

#####B. leader选举

Raft使用心跳机制来触发选举。当server启动时,初始状态都是follower。每个server都有一个定时器,超时时间为election timeout,若是某server没有超时的状况下收到来自leader或者candidate的任何RPC,定时器重启,若是超时,它就开始一次选举。leader给其余人发RPC要么复制日志,要么就是用来告诉followers老子是leader,大家不用选举的心跳(告诉followers对状态机应用日志的消息夹杂在心跳中)。若是某个candidate得到了大多数人的选票,它就赢得了选举成为新leader。每一个server在某个term周期内只能给最多一我的投票,按照先来先给的原则。新leader要给其余人发送心跳,阻止新选举。

在等待选票过程当中,一个candidate,假设为A,可能收到它人的声称本身是leader的AppendEntries RPC,若是那家伙的term号大于等于A的,那么A认可他是leader,本身从新变成follower。若是那家伙比本身小,那么A拒绝该RPC,继续保持candidate状态。

还有第三种可能性就是candidate既没选举成功也没选举失败:若是多个followers同时成为candidate去拉选票,致使选票分散,任何candidate都没拿到大多数选票,这种状况下Raft使用超时机制来解决。Raft给每个server都分配一个随机长度的election timeout(通常是150——300ms),因此同时出现多个candidate的可能性不大,即便机缘巧合同时出现了多个candidate致使选票分散,那么它们就等待本身的election timeout超时,从新开始一次新选举(再一再二不能再三再四,不可能每次都同时出现多个candidate),实验也证实这个机制在选举过程当中收敛速度很快。

#####C. 日志复制

一旦选举出了一个leader,它就开始负责服务客户端的请求。每一个客户端的请求都包含一个要被复制状态机执行的指令。leader首先要把这个指令追加到log中造成一个新的entry,而后经过AppendEntries RPCs并行的把该entry发给其余servers,其余server若是发现没问题,复制成功后会给leader一个表示成功的ACK,leader收到大多数ACK后应用该日志,返回客户端执行结果。若是followers crash或者丢包,leader会不断重试AppendEntries RPC。Logs按照下图组织:

每一个log entry都存储着一条用于状态机的指令,同时保存从leader收到该entry时的term号。该term号能够用来判断一些log之间的不一致状态。每个entry还有一个index指明本身在log中的位置。

leader须要决定何时将日志应用给状态机是安全的,被应用的entry叫committed。Raft保证committed entries持久化,而且最终被其余状态机应用。一个log entry一旦复制给了大多数节点就成为committed。同时要注意一种状况,若是当前待提交entry以前有未提交的entry,即便是之前过期的leader建立的,只要知足已存储在大多数节点上就一次性按顺序都提交。leader要追踪最新的committed的index,并在每次AppendEntries RPCs(包括心跳)都要捎带,以使其余server知道一个log entry是已提交的,从而在它们本地的状态机上也应用。

Raft的日志机制提供两个保证,统称为Log Matching Property:

  • 不一样机器的日志中若是有两个entry有相同的偏移和term号,那么它们存储相同的指令。
  • 若是不一样机器上的日志中有两个相同偏移和term号的日志,那么日志中这个entry以前的全部entry保持一致。

第一个保证是因为一个leader在指定的偏移和指定的term,只能建立一个entry,log entries不能改变位置。第二个保证经过AppendEntries RPC的一个简单的一致性检查机制完成。当发起一个AppendEntries RPC,leader会包含正好排在新entries以前的那个entry的偏移和term号,若是follower发如今相同偏移处没有相同term号的一个entry,那么它拒绝接受新的entries。这个一致性检查以一种相似概括法的方式进行:初始状态你们都没有日志,不须要进行Log Matching Property检查,可是不管什么时候followers只要日志要追加都要进行此项检查。所以,只要AppendEntries返回成功,leader就知道这个follower的日志必定和本身的彻底同样。

在正常情形下,leader和follower的日志确定是一致的,因此AppendEntries一致性检查从不失败。然而,若是leader crash,那么它们的日志极可能出现不一致。这种不一致会随着leader或者followers的crash变得很是复杂。下图展现了全部日志不一致的情形:

如上图(a)(b)followers可能丢失日志,(c)(d)有多余的日志,或者是(e)(f)跨越多个terms的又丢失又多余。在Raft里,leader强制followers和本身的日志严格一致,这意味着followers的日志极可能被leader的新推送日志所覆盖。

leader为了强制它人与本身一致,势必要先找出本身和follower之间存在分歧的点,也就是个人日志与大家的从哪里开始不一样。而后令followers删掉那个分歧点以后的日志,再将本身在那个点以后的日志同步给followers。这个实现也是经过AppendEntries RPCs的一致性检查来作的。leader会把发给每个follower的新日志的偏移nextIndex也告诉followers。当新leader刚开始服务时,它把全部follower的nextIndex都初始化为它最新的log entry的偏移+1(如上图中的11)。若是一个follower的日志和leader的不一致,AppendEntries RPC会失败,leader就减少nextIndex而后重试,直到找到分歧点,剩下的就好办了,移除冲突日志entries,同步本身的。固然这里有很大的优化余地,彻底不须要一步一步回溯,怎么玩请本身看论文 1,很简单。

#####D. 安全性

前文讲解了Raft如何选主和如何进行日志复制,然而这些还不足以保证不一样节点能执行严格一致的指令序列,须要额外的一些安全机制。好比,一个follower可能在当前leader commit日志时不可用,然而过会它又被选举成了新leader,这样这个新leader可能会用新的entries覆盖掉刚才那些已经committed的entries。结果不一样的复制状态机可能会执行不一样的指令序列,产生不一致的情况。这里Raft增长了一个能够确保新leader必定包含任何以前commited entries的选举机制。

(1) 选举限制

Raft使用了一个投票规则来阻止一个不包含全部commited entries的candidate选举成功。一个candidate为了选举成功必须联系大多数节点,假设它们的集合叫A,而一个entry若是能commit必然存储在大多数节点,这意味着对于每个已经committed的entry,A集合中必然有一个节点持有它。若是这个candidate的Log不比A中任何一个节点旧才有机会被选举为leader,因此这个candidate若是要成为leader必定已经持有了全部commited entries(注意我说的持有指只是储存,不必定被应用到了复制状态机中。好比一个老的leader将一个entry发往大多数节点,它们都成功接收,老leader随即就commit这个entry,而后挂掉,此时这个entry叫commited entry,可是它不必定应用在了集群中全部的复制状态机上)。这个实如今RequestVote RPC中:该RPC包含了candidate log的信息,选民若是发现被选举人的log没有本身新,就拒绝投票。Raft经过比较最近的日志的偏移和term号来决定谁的日志更新。若是二者最近的日志term号不一样,那么越大的越新,若是term号同样,越长的日志(拥有更多entries)越新。

(2) 提交早期terms的entries

正如前面所述的那样,一个leader若是知道一个当前term的entry若是储在大多数节点,就认为它能够commit。可是,一个leader不能也认为一个早于当前term的entry若是存在大多数节点,那么也是能够commit的。下图展现了这样一种情况,一个老的log存储在大多数节点上,可是仍有可能被新leader覆盖掉:

要消除上图中的问题,Raft采起针对老term的日志entries毫不能仅仅经过它在集群中副本的数量知足大多数,就认为是能够commit的。完整的commit语义也演变成:一个日志entry若是可以被认为是能够提交的,必须同时知足两个条件:

  • 这个entry存储在大多数节点上
  • 当前term至少有一个entry存储在大多数节点。

以上图为例,这两个条件确保一旦当前leader将term4的entry复制给大多数节点,那么S5不可能被选举为新leader了(日志term号过期)。综合考虑,经过上述的选举和commit机制,leaders永远不会覆盖已提交entries,而且leader的日志永远绝对是”the truth”。

(3) 调解过时leader

在Raft中有可能同一时刻不仅一个server是leader。一个leader忽然与集群中其余servers失去链接,致使新leader被选出,这时刚才的老leader又恢复链接,此时集群中就有了两个leader。这个老leader极可能继续为客户端服务,试图去复制entries给集群中的其它servers。可是Raft的term机制粉碎了这个老leader试图形成任何不一致的行为。每个RPC servers都要交换它们的当前term号,新leader一旦被选举出来,确定有一个大多数群体包含了最新的term号,老leader的RPC都必需要联系一个大多数群体,它必然会发现本身的term号过时,从而主动让贤,退变为follower状态。

然而有多是老leader commit一个entry后失去链接,这时新leader必然有那个commit的entry,只是新leader可能还没commit它,这时新leader会在初次服务客户端前先把这个entry再commit一次,followers若是已经commit过直接返回成功,没commit就commit后返回成功而已,不会形成不一致。

#####E. Follower 或者candidate crash

Followers和candidate的crash比起leader来讲处理要简单不少,它们的处理流程是相同的,若是某个follower或者candidate crash了,那么将来发往它的RequestVote和AppendEntries RPCs 都会失败,Raft采起的策略就是不停的重发,若是crash的机器恢复就会执行成功。另外,若是server crash是在完成一个RPC但在回复以前,那么在它恢复以后仍然会收到相同的RPC(让它重试一次),Raft的全部RPC都是幂等操做,若是follower已经有了某个entry,可是leader又让它复制,follower直接忽略便可。

#####F. 时间与可用性

Raft集群的可用性须要知足一个时间关系,即下面的公式:

broadcastTime表明平均的广播延迟(从并行发起RPC开始,直到收到大多数的回复为止)。electionTimeout就是前文所述的超时时间间隔,MTBF表明一个机器两次宕机时间间隔的平均值。broadcastTime必须远远小于electionTimeout,不然会频繁触发无心义的选举重启。electionTimeout远远小于MTBF就很好理解了。

####集群扩容

迄今为止的讨论都是假设集群配置不变的状况下(参与一致性算法的机器数目不变),可是实际中集群扩容(广义概念,泛指集群容量变化,可增可减)有时是必须的。好比业务所需,集群须要扩容,或者某机器永久损坏,须要从集群中剔除等等。

 

扩容最大的挑战就是保证一致性很困难,由于扩容不是原子的,有可能集群中一部分机器用老配置信息,另外一部分用新配置信息,如上图所示,所以究竟多少算大多数在集群中可能存在分歧。扩容通常都分为两个阶段,有不少实现方法,比较典型的就是第一阶段,使全部旧配置信息失效,这段时间不对外提供服务;第二阶段容许新配置信息,从新对外服务。在Raft中集群第一阶段首先使集群转向一个被称做联合一致性(joint consensus)的状态;一旦这个联合一致性(有一个特殊的Cold,new entry表征)被提交,这个系统就开始向新配置迁移。joint consensus既包含老配置信息,又包含新配置信息,它的规则(Cold,new规则)以下:

  • log entries复制给集群中全部机器,无论配置信息新旧
  • 任何配置的server都有权利当leader
  • 达成一致所须要的大多数(无论是选举仍是entry提交),既要包括老配置中的一个大多数也要包括新配置机器中的一个大多数。

joint consensus容许集群配置平滑过渡而不失去安全和一致性,同时集群可正常对外服务。集群配置信息以一个特殊的entry表征,存储在log中并和其余entry语义同样。当一个leader收到扩容请求,它就建立一个Cold,new的entry,并复制给集群其它其它机器,一旦有某个follower收到此entry并追加到本身的日志中,它就使用Cold,new规则(不须要committed)。leader按照Cold,new 规则对Cold,new进行commit。若是一个leader crash了,新leader既多是没收到Cold,new的又多是收到的,可是这两种状况都不会出现leader用Cnew规则作决策。

一旦Cold,new被提交,就开始向新配置转换。Raft保证只有拥有Cold,new的节点才会被选举为leader。如今leader能够很安全地建立一个叫Cnew的entry,而后复制给集群中属于新配置的节点。配置信息仍是节点一收到就能够用来作决策。Cnew的commit使用Cnew 规则,旧配置已经与集群无关,新配置中不存在的机器能够着手关闭,Raft经过保证不让Cold和Cnew的节点有机会同时作决策来保障安全性。配置改变示意图以下:

有三个要注意的点:

(1) 新server加入进集群时也许没有存储任何log entries,它们须要很长时间来追上原有机器的“进度”,为了防止集群性能出现巨大的抖动,Raft在配置改变前又引入了一个特殊的阶段,当新机器加入集群时,它们做为无投票权的节点,即leader仍是会将log entries复制给它们,可是不会被当作大多数对待。一旦这些“新人”追上了其余机器的进度,动态扩容能够像上述同样执行。

(2) 当前leader也许不是集群新配置中的一份子,在这种状况下,一旦Cnew被提交,这个leader就主动退出。这意味着当Cnew提交过程当中这个leader要管理一个不包括它本身的集群。这时,它在给其余节点复制log entries时不把本身计入大多数。当Cnew提交成功,这个leader的使命就光荣完成了,在这个时间点前,只有Cold的节点才会被选举为leader。

(3) 第三个问题是被移除的机器可能会影响集群服务。这些机器不会再接收到心跳信息,它们所以可能重启选举,结果是它们用一个更新的term号发起RequestVote RPCs,当前leader无奈的退化为follower。新leader(拥有新配置的)仍是会被选举出来,可是那些被移除的机器仍是继续超时,开启选举,致使系统可用性下降。为解决这一问题,须要有一个机制让集群中节点可以忽略这种RequestVote RPCs,相信当前leader正在正常工做。Raft使用最小超时延迟(前面说过,Raft会随机为每一个节点指定一个随机的election timeout,典型的是150ms到300ms,最小超时延迟就是它们之间的最小值)来保证,若是一个节点在最小超时延迟前收到一个RequestVote RPCs,那么该节点不会提高term号,也不会为其投票。这个不会影响正常的选举,由于正常选举中都是至少等待一个最小超时延迟才开始下一次选举的。

####Raft新集群扩容方案

为了下降集群扩容的复杂性,Raft还有一个能够备选的方案。它加强了扩容的限制:一次只容许一台机器加入或移除集群。复杂的扩容能够经过一系列的单机增长或删除变向实现。

当从集群中添加或删除一台机器时,任何旧配置集群中的大多数和任何新配置集群中的大多数必然存在一个交集,以下图所示。

注意就是这个交集让集群不可能存在两个互相独立,没有交集的大多数群体。所以系统中不会同时存在两个leader(一个由旧配置集群选举出来,一个由新配置集群选举出来),所以扩容是安全的,集群能够直接从旧配置转向新配置。

当leader收到添加或删除一台机器的请求,它将该Cnew添加到本身的log中,而后使用同样的复制机制,把这个entry复制给属于新配置集群的全部节点。只要节点把Cnew添加进log,就立刻使用新配置信息,无需等到该entry提交。leader使用Cnew规则对Cnew进行提交,一旦提交完成,扩容就宣告完成,leader就知道任何没有Cnew的节点都不可能造成一个大多数,这样的节点也不可能成为集群的leader。Cnew的提交暗示了如下三件事情:

  1. leader获知扩容成功完成
  2. 若是新配置移除一台机器,那么这台机器能够着手关闭
  3. 新的扩容能够开始

正如上面所讲的那样,servers永远使用log中最新的配置,无论这个配置entry是否被提交。这样leader知道一

相关文章
相关标签/搜索