分布式一致性协议Raft-从入门到爱上

Raft的诞生

在分布式领域,始终都要面临的一个挑战就是:数据一致性。它是指数据在各个机器节点上流转的时候,如何保证任一时刻数据都是正确并且最新的。为此,莱斯利·兰伯特(Leslie Lamport)在1990年提出了一种实现算法,也就是著名的Paxos。如今它是业界公认此类问题的最有效解。虽然Paxos在理论界得到了高度认可,但是却给工程界带来了难题。因为这个算法本身比较晦涩,并且抽象,缺少很多实现细节。这让许多工程师大为头疼。天无绝人之路。几十年后,希望的曙光出现了。来自斯坦福大学的两位仁兄 Diego Ongaro 和 John Ousterhout 在“In Search of an Understandable Consensus Algorithm (Extended Version)”一文中正式引入 Raft。并在论文开篇就表明Raft是为解决Paxos难以理解和实现的问题而提出的。从此,Raft一炮走红。

初识Raft

Raft 算法的工作流程主要包含五个部分:

  1. 领导选举(Leader election):在集群初始化或者旧领导异常情况下,选举出一个新的领导。
  2. 日志复制(Log replication): 当有新的日志写入时,领导能把它复制到集群中大多数节点上。
  3. 集群成员变更(Cluster Membership changes): 当集群有扩容或者缩容的需求,集群各节点能准确感知哪些节点新加入或者被去除。
  4. 日志压缩(Log compaction): 当写入的日志文件越来越大,重启时节点回放(replay)日志的时间将无限延长,并且新节点加入集群时传送日志文件也会无限拉大。需要定期对日志文件进行重整压缩。
  5. 读写一致性(Read/write consistency): 客户端作为集群的外部组件,当一个客户端写入新数据时,能保证后续所有客户端都能读到最新的值。

从上面可以看出,Raft 算法涵盖的内容很多。如果一条条逐个讲解,对于一个初学者来说,无异于“一口吃下一个胖子”,这显然不切实际,也会让人淹没在细节的海洋中,失去对算法的全局认识。所以本文采用“突出主要问题,忽略其次细节”的方法先让大家对其最核心的部分,领导选举和日志复制有一个深入理解。

Raft为什么是正确的

理解一个算法,最重要的是要知道如此设计的正确性。学习算法的思想,才能真正理解,继而分享和交流。上来就讲实现细节,很难产生共鸣。

那怎么论证Raft的正确性呢?要科学地论证一件事,一般有两种方法。

  1. 使用精巧的数学方程推导。这是最严谨的方式。
  2. 使用归纳假设推理。通过一步步逻辑推理最终归纳出结论。

本文采用第二种方式,也是Raft论文的论述推理形式。

假设领导永不挂,验证正确性

Raft为了简化Paxos的复杂性,提出了强领导模式它是指在一个正常运行的Raft集群中,有且只有一个Leader,并且所有的写入操作都必须通过Leader来全局控制。 现在我们来做一道题:

假设Raft集群有一个领导,这个领导运行在最理想的环境下,就是说它永远正常运行,并且网络通畅。请验证在这个假定条件下Raft的正确性。

之前谈到,Raft是为了解决集群中数据一致性而提出的。当外部Client进行一个写入操作(假定这个写入是一个赋值操作x=4)的时候,Raft要保证这个写入数据的一致性。需要说明的是:这里的一致性是指集群内部数据的一致性,也可以说是内部达成的共识,对于外部Client暂不做要求,后文会提到Client的一致性

其实数据一致性展开来讲,是有几层语义的:

  1. 数据的写入顺序要保持一致。否则可能出现很多不预期的情况,比如:旧值覆盖新值。先删后增变成先增后删,数据消失了
  2. 对成功写入的数据供认不讳。如果x=6被集群表明写入成功,那么集群各节点都应该认可并接受这个结果,而不会出现某些节点不知情的情况。
  3. 数据写入成功保证持久化的。如果集群表明数据写入成功,数据却没落盘。这时宕机了,那么数据就丢失了。

现在我们来看看在领导永不挂机这一假定条件下,Raft的做法。

  1. 保序性。之前谈到Raft是强领导模式,所有写入必须先经过Leader,再由Leader决定是否写入成功。并且Raft把日志看成一个有序(Ordered)的只能追加(Appendonly)的列表。 当写入数据到达Leader的时候,Leader首先就会把这个写入操作转换成Log条目(Log entry)追加到Log列表的尾部。如下图,x=4被追加到Leader的Log列表的尾部,也就是序号8的位置。Log列表的序号称为Log index。通过这种Appendonly操作,Leader上的写入数据可以保证是按照接受的顺序追加到列表中,并且列表不会删除已经append的条目,所以Leader上写入数据是具有保序性的。 那集群中其他节点的保序性呢?当Leader成功append一个条目到自己的Log列表后,它会把这个写入操作并行转发给集群中其他节点。其他的每个节点也会像Leader的操作一样,把这个Log entry追加到自己的Log列表中。这样,Leader按照接收entry->追加entry到列表->转发entry到其他节点的流程处理每一条写入操作,就能保证其他节点也是按照顺序追加entry,从而保证整个集群范围内都是保序的。
    分布式一致性协议Raft-从入门到爱上_第1张图片

  2. 共识性。Raft规定:当集群中大多数节点都成功写入一个操作后,那么这个操作就被认为是写入成功的,并会在集群中对这个操作成功的结果达成共识。 使用多数派来进行决议达成共识其实是生活中很普遍的现象,比如:班级里投票选班干部,多数票的会获选,并且获选的的结果会被班级所有同学所知悉。那Raft具体是怎么实现的呢?上文提到,当Leader收到一个写入操作,做完必要的追加操作后,会并行转发给其他节点。当其他的节点也append日志成功后,会响应成功的状态给Leader。Leader在收到集群大多数节点的成功响应后,就会认为这个写入操作成功了,提交(commit)这个日志,返回成功状态给Client。至于非Leader节点提交日志的确认时机是在Leader提交日志后,下一次Leader发送心跳(Heartbeat)请求中进行顺带确认的。比如:集群有5个节点,Leader会转发日志给其他4个节点,当收到2个节点的成功响应,加上Leader自身的,一共三票处于大多数,那么Leader就会认为这个写入操作成功,继而集群内达到共识。

  3. 持久性。Raft规定:集群中每个节点都有自己的状态机(State machine),当日志提交后,会将日志应用(appley)到状态机。状态机会执行真正的操作,并持久化这个操作的结果。可能有人会有疑问:如果日志在提交成功,即将要apply到状态机这一时刻宕机了,那日志不就丢失了吗?其实是不会有这个问题的。Raft明确规定:在Append日志条目到Log列表时要确保日志落盘到文件系统中。由于这是一个Appendonly操作,磁盘对于这种顺序写效率还是很高的。

我们对于数据一致性的三个方面进行了分析,发现Raft都能很好的支持。这就能证明:在假定领导永不宕机的前提下,Raft是能够保证集群数据一致性的

如果领导挂掉了,验证正确性

常言道,理想很丰满,现实很骨感。在现实世界运行的计算机,其所处环境非常复杂。据统计,在一个大型数据中心里,每天都会有机器宕机,网络抖动。像磁盘坏道,网线老损,网卡降速等硬件问题也会时有发生。而磁盘容量不足,系统超载等软件问题更是频发。所以一个进程永远正常运行是不可能的。所以对于Raft集群来说,Leader一定会存在非正常运行的情况。那这个时候,Raft是怎样保证数据的一致性呢?

要回答这个问题,其实是要拆解成两个子问题。首先是如何发现Leader处于非正常运行状态。在Raft中,Leader为了巩固自己的领导地位,会定期向集群中其他节点发送心跳,表明自己还活着。 而其他节点会维持一个心跳超时时间(Heartbeat Timeout),如果在这个时间段内没有如期收到Leader的心跳,那么该节点会认为Leader处于非正常服务状态。而这个时候,Leader的真实情况对于该节点来说其实是未知,Leader有可能确实挂掉了,也可能是系统负载高导致Leader不响应卡死,还有可能是网络出现了抖动,消息传送延迟。

上一小节我们论证了在Leader正常运行时,Raft能保证集群的数据一致性。那么在Leader非正常运行情况下,如果能选举出一个Leader,不就能继续保证数据一致性吗。的确,Raft就是这么干的。

Raft规定:当集群中任意非Leader节点的选举时间超时,它能够自行发动一轮Leader投票选举,设法让自己成为Leader。这里需要声明的是,Raft把集群节点划分成三种角色,分别是领导者(Leader)跟随者(Follwer)候选者(Candidate)。其中Follower是被动接受来自Leader的心跳和写入日志请求。Candidate是能够发起新一轮Leader投票选举的角色,它是由Follwer发现心跳超时后,自动转换而来。下图详细描述了节点在各角色之间转换的触发条件。
分布式一致性协议Raft-从入门到爱上_第2张图片
回到刚才话题,当Candidate发起新一轮选举时,应该基于什么策略去选择合适的Leader呢?换句话说,应该选举谁作为Leader可以保证数据的一致性。Raft规定:Leader上已提交的日志是不可以被覆盖或更新的。 当一个Leader由正常变成非正常的那一时刻,它应该拥有集群中最新最全的日志,如果我们能保证当选新Leader的Candidate节点的日志尽可能和前Leader的日志一样新,那么这样就不会丢失任何数据,会和之前集群日志状态一模一样。当然这是最理想情况,实际很难做到。我们知道:已提交的日志是会通知客户端写入成功的。如果已提交的日志不能存在于新的Leader中,那客户端会很困惑,明明提示写入成功,结果再次查询却找不到数据,这样数据一致性就无法保证,所以新的Leader要保证至少包含所有已提交的日志

那这个要如何做到呢?Raft规定:Candidate选举时,只有自己log列表中最新日志的log index和Term与集群大多数节点一样或者更大的时候,才能提升自己为Leader。这里出现了一个新的术语,叫任期(Term),也就是日志列表图中每个方块内顶部居中的数字,它表示当前Leader所处的任期号,每个Leader在位的时候都有一个唯一的任期号,并且保证任期内不可变。而新Leader会对Term进行自增。从整个过程看,Term是一个非严格单调递增的整数值。回到问题讨论,有人可能会疑惑这个为什么就能保证新Leader包含所有已提交日志。道理很简单,因为所有被判定为已提交的日志必须在集群大多数节点中持久存在,而要推选为新Leader的Candidate的日志至少要和集群中大多数节点一样新,两个多数派节点的集合必然存在至少一个节点的交集,那么这个节点既有所有已提交日志,又能保证新Leader至少和它一样新,也就能推论出新Leader至少拥有所有已提交的日志。这个推理可能有点绕,可以好好想想。

上面的推论,保证了Leader在非正常运行情况下,推选出的新Leader至少拥有所有已提交的日志,从而保证数据一致性。心细的读者可能会问,那集群在旧Leader异常和新Leader选举出来之前这段时间,该怎么保证数据一致性呢?其实这个时候,集群处于“群龙无首”的状态,而Raft规定:一切写入操作必须由Leader管控,所以这段时间客户端的写入会被告知失败或者进行不断重试。这里其实一定程度上牺牲了集群的可用性来保证一致性。然而就像CAP定理告诉我们的,分布式系统不能既保证一致性C,又保证可用性A。而Raft集群选择了 CP,也就一定程度失去了A

结论

我们在假设Leader正常运行的前提下,证明了Raft能保证数据一致性。而在Leader非正常运行的情况下,同样证明了它能保证数据一致性。所以综上,Raft算法能保证集群的数据一致性。

转送门:下一篇会列举几个案例进行分析 《剖析Raft》

参考引用
Leslie Lamport. The part-time parliament. ACM Transactions on Computer Systems, 16(2):133–169, May 1998.
Diego Ongaro,John Ousterhout.In Search of an Understandable Consensus Algorithm(Extended Version)

你可能感兴趣的:(Raft协议,分布式)