在分布式领域,始终都要面临的一个挑战就是:数据一致性
。它是指数据在各个机器节点上流转的时候,如何保证任一时刻数据都是正确并且最新的。为此,莱斯利·兰伯特(Leslie Lamport)在1990年提出了一种实现算法,也就是著名的Paxos。如今它是业界公认此类问题的最有效解。虽然Paxos在理论界得到了高度认可,但是却给工程界带来了难题。因为这个算法本身比较晦涩,并且抽象,缺少很多实现细节。这让许多工程师大为头疼。天无绝人之路。几十年后,希望的曙光出现了。来自斯坦福大学的两位仁兄 Diego Ongaro 和 John Ousterhout 在“In Search of an Understandable Consensus Algorithm (Extended Version)”一文中正式引入 Raft。并在论文开篇就表明Raft是为解决Paxos难以理解和实现的问题而提出的。从此,Raft一炮走红。
Raft 算法的工作流程主要包含五个部分:
从上面可以看出,Raft 算法涵盖的内容很多。如果一条条逐个讲解,对于一个初学者来说,无异于“一口吃下一个胖子”,这显然不切实际,也会让人淹没在细节的海洋中,失去对算法的全局认识。所以本文采用“突出主要问题,忽略其次细节”的方法先让大家对其最核心的部分,领导选举和日志复制有一个深入理解。
理解一个算法,最重要的是要知道如此设计的正确性。学习算法的思想,才能真正理解,继而分享和交流。上来就讲实现细节,很难产生共鸣。
那怎么论证Raft的正确性呢?要科学地论证一件事,一般有两种方法。
本文采用第二种方式,也是Raft论文的论述推理形式。
Raft为了简化Paxos的复杂性,提出了强领导模式。它是指在一个正常运行的Raft集群中,有且只有一个Leader,并且所有的写入操作都必须通过Leader来全局控制。 现在我们来做一道题:
假设Raft集群有一个领导,这个领导运行在最理想的环境下,就是说它永远正常运行,并且网络通畅。请验证在这个假定条件下Raft的正确性。
之前谈到,Raft是为了解决集群中数据一致性而提出的。当外部Client进行一个写入操作(假定这个写入是一个赋值操作x=4)的时候,Raft要保证这个写入数据的一致性。需要说明的是:这里的一致性是指集群内部数据的一致性,也可以说是内部达成的共识,对于外部Client暂不做要求,后文会提到Client的一致性
。
其实数据一致性展开来讲,是有几层语义的:
现在我们来看看在领导永不挂机这一假定条件下,Raft的做法。
保序性。之前谈到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规定:当集群中大多数节点都成功写入一个操作后,那么这个操作就被认为是写入成功的,并会在集群中对这个操作成功的结果达成共识。
使用多数派来进行决议达成共识其实是生活中很普遍的现象,比如:班级里投票选班干部,多数票的会获选,并且获选的的结果会被班级所有同学所知悉。那Raft具体是怎么实现的呢?上文提到,当Leader收到一个写入操作,做完必要的追加操作后,会并行转发给其他节点。当其他的节点也append日志成功后,会响应成功的状态给Leader。Leader在收到集群大多数节点的成功响应后,就会认为这个写入操作成功了,提交(commit)这个日志,返回成功状态给Client。至于非Leader节点提交日志的确认时机是在Leader提交日志后,下一次Leader发送心跳(Heartbeat)请求中进行顺带确认的。比如:集群有5个节点,Leader会转发日志给其他4个节点,当收到2个节点的成功响应,加上Leader自身的,一共三票处于大多数,那么Leader就会认为这个写入操作成功,继而集群内达到共识。
持久性。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发现心跳超时后,自动转换而来。下图详细描述了节点在各角色之间转换的触发条件。
回到刚才话题,当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集群选择了 C和 P,也就一定程度失去了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)