你好,我是徐文浩。
在前面课程中,我们了解过的这些大数据处理系统,其实都属于分布式系统。所以,它们也都需要解决分布式一致性,或者说分布式共识的问题。
我们之前已经介绍过Chubby,这个Google开发的分布式锁。正是通过Chubby这样的系统,使得我们可以确保系统里始终只有一个Master,以及所有的数据分区在一个时间点只有一个所有者。而Chubby的底层,就是Paxos这样的分布式共识(Distributed Consensus)算法。另外在后面的Spanner这样的分布式数据库里,我们也看到了Paxos也可以直接拿来作为分布式数据库的解决方案。
在之前的这些论文里,我们对Paxos的算法做了一些简单的介绍。不过,我们并没有对Paxos的算法做非常深入地剖析。一方面,Paxos算法其实并不容易理解,这也是为什么Paxos的论文第一次发表的时候,并没有得到足够的关注。另一方面,目前市场上实际的开源项目,大部分也并不是采用了Paxos或者Multi-Paxos算法,而是往往采取了简化和变形。Google的Chubby并没有开源,而开源的ZooKeeper实现的是自家的ZAB算法,对Paxos做了改造。
事实上,在2021年的今天,最常被使用的分布式共识算法,已经从Paxos变成了Raft。这要归功于来自斯坦福大学,在2013年发表的一篇论文《In Search of an Understandable Consensus Algorithm》。
这一篇论文,也是我们接下来两讲的主题。这两讲的目标,是帮助你掌握这样两点:
在具体讲解Raft算法之前,我们先做一个小小的复习,那就是我们之前讲到过的Paxos算法,到底是在帮助我们解决一个什么样的问题。
无论是在Chubby还是Spanner里,我们通过Paxos算法想要解决的问题,其实都是一个“状态机复制(State Machine Replication)”问题,简称SMR问题。
我们可以把一个数据库的写入操作,看成是一系列的顺序操作日志,也就是把对于数据库的写入,变成了一个状态机。而对于这个状态机的更新,我们需要保障高可用性和容错能力。这个需要我们对于某一个服务器上的日志追加写入,能够做到同步复制到多个服务器上。
这样,在当前我们写入的节点挂掉的时候,我们的数据并不会丢失。为了避免单点故障,我们希望可以从多台服务器都能写入数据,但是又不能要求所有服务器都同步复制成功。前者,是为了确保某个服务器挂掉的时候,我们可以快速切换到另一台备用服务器;后者,是避免我们写入的时候,因为某一台服务器挂掉了就无法写入成功。
所以,Paxos这样的分布式共识算法的基本思路,就是这样几点:
对于Paxos算法的具体实现,我就不在这里重复了。你可以回头看课程里关于Chubby论文的部分,或者去对应看Paxos算法的原文。
Paxos算法本身在理论上并没有什么问题,它的主要问题是,不太容易理解。不知道你在课程之外,有没有去读一下Paxos的原始论文,或者其他能够找到的深入解析这个算法的资料。至少对我来说,我做不到能把算法牢牢记在脑子里,而是每次需要的时候,都要再花上几个小时重新看一遍算法。
而Raft算法,其实就是针对Paxos算法的这个缺点而来的,相信你从论文的名字《In Search of an Understandable Consensus Algorithm》也能看到这一点了。
那么,Raft算法是为什么可以让我们容易理解的呢?我觉得有三点原因:
那么接下来,我们就来深入看看Raft算法具体是怎么样实现的。Raft算法的思路非常简单明确,它做出了这样几个选择:
这里,我们先来看看,Raft系统的基本数据写入流程是怎么样的。首先,Raft的系统里也会和Chubby/ZooKeeper一样,有多台服务器。这些服务器,会分成这样三个角色:
外部的客户端写入数据的时候,都是发送给Leader。
Leader,本质上是通过一个两阶段提交,来做到同步复制的。一方面,它会先把日志写在本地,同时也发送给Follower,这个时候,日志还没有提交,也就没有实际生效。Follower会返回Leader,是否可以提交日志。当Leader接收到超过一半的Follower可以提交日志的响应之后,它会再次发送一个提交的请求给到Follower,完成实际的日志提交,并把写入结果返回给客户端。
那么,既然是一个两阶段提交,我们就会遇到Leader节点挂掉的时候。Raft不能让整个系统有单点故障,所以节点挂掉的时候,它需要能够协商出来一个新的Leader,这个协商机制就是Leader选举。
Raft的Leader选举是这样的:
而我们的Candidate,会遇到三种情况。
第一种,自然是超过半数服务器给它投票。那么,它就会赢得选举,变成Leader,并且进入一个新的任期。
第二种,是有另外一个Candidate赢得了选举,变成了下一任的Leader。这个时候,我们还不知道自己已经输了。过了一会儿,新的Leader接收到了外部客户端的写入请求,就会向我们这个Candidate发起日志同步的请求。或者,新的Leader也会向我们发送一个心跳请求。无论是哪种请求,里面都会包含一个任期的信息,这个任期的信息比我们当前知道的最新的Leader大,那么我们就知道自己在投票里面输了,会把自己变成一个Follower,并更新最新的任期和Leader信息。
第三种,是过了一段时间,无人获胜。我们会为这种情况设置一个超时时间,如果到了超时时间,我们既没有赢也没有输,那么我们会对任期自增1,再发起一轮新的投票。
你会发现这个选举过程,会遇到一种尴尬的情况。那就是当Leader挂掉的时候,很多个Follower都会成为Candidate。他们都会先给自己投票,然后向其他人发起RequestVote。这样,就会导致票被很多人“瓜分”,没有人能拿到超过半数的票。然后我们就会陷入前面的第三种情况,无限循环出不来了。
Raft对于这个问题的解决方案,是让选举的超时时间在一个区间之内随机化。这样,不同的服务器会在不同的时间点超时,最先超时的那个服务器,大概率会在其他服务器发现超时之前,就赢得投票。
Leader选出来了,那我们自然就可以向Leader发送写入数据的请求。既然是一个“状态机复制”的方案,写入请求其实就是一条操作日志的追加写。在Raft里,就是通过一个AppendEntries的RPC调用实现的。
整个数据写入,就是一个前面我们说的两阶段提交的过程,只不过这个两阶段提交只需要“半数”通过,就可以发起第二阶段的提交操作,而不需要等待所有服务器都确认可以提交。当然,我们可能会遇到某些节点挂掉了,和两阶段提交里一样,我们要做的就是无限重试。
我们追加写入的每一条日志,都包含三部分信息:
我们在追加写入日志的时候,不只是单单追加最新的一条日志,还需要做一个校验,确保对应的Follower的数据和Leader是一致的:
本质上,Raft的复制操作,是让Leader为每一个Follower都从Leader的尾部往头部循环,找到Follower最新同步到哪里的日志。然后从这个位置开始,往后复制Leader的日志,直到最新一条的日志。通过这个过程,我们把每一次Leader的日志复制,都变成了一次强制所有Follower和当前Leader日志同步的过程。
之所以需要这个过程,是因为我们不同服务器的日志,可能因为网络延时还没有同步,也可能因为硬件故障还包含未提交的日志。我在这里放上了论文里的图7,你可以看到:
这里的脏日志,则是来自服务器在担任Leader的时候,可能已经在本地写入了日志,然后挂掉了,所以日志没有能够完成提交。
而我们让Leader在追加写入的时候,顺便进行强行同步的过程,给我们带来了一个好处,那就是我们不需要有什么特殊机制,在硬件故障或者两阶段提交失败的时候去做回滚,而是只需要不断强制Follower和Leader同步,就能保障数据的一致性。
不过,这个也给我们带来了一个挑战,那就是我们需要确保,我们的Leader始终是包含了最新提交的日志。也就是无论我们因为故障也好,其他原因也好,随便怎么切换Leader,Leader的日志都是最新并准确的。
而这个,则需要我们下面所说的安全性机制来保障。
首先要注意,Raft里,每一个服务器写入的日志,会有两种状态,一种是未提交的,一种是已提交的。我们这里所说的最新,指的是已提交的日志。我们想要确保Leader的日志是最新的,只需要在Leader选举的时候,让只有最新日志的Leader才能被选上就好了。
而要做到这一点,也并不困难,Raft的做法是,直接在选举的RPC里,顺便完成Leader是否包含所有最新的日志就好了。Raft是这么做的:
在这种情况下,一旦投票通过,就意味着Candidate的已提交的日志,至少和一半的Follower一样新或者更新。而Raft本身写入数据,就需要至少一半成功,才会提交成功。所以,在前面愿意给Candidate投票的里面,至少有一个服务器,包含了最新一次的数据提交,而Candidate至少和它一样新,自然也就包含了最新一次的数据提交。
就这样,我们简单地通过共识算法需要半数以上通过的原理,很容易就可以只让包含最新提交数据的服务器,才能选上Leader。
好了,到这里,我们对于Raft的基本算法原理已经讲完了。Raft的主要目标是找到一个容易理解的分布式共识算法。所以,它通过直接从“状态机复制”这个角度,来直接设计算法,而不是通过共识问题绕一圈,映射成一个状态机复制问题。
在算法设计层面,Raft采取了分而治之的办法。通过把问题拆分成Leader选举、日志复制、安全性等一系列子问题,来使得算法更容易理解。
在Leader选举上,Raft采用的是典型的心跳检测Leader存活,以及随机超时时间投票,确保不会死循环,来确保我们可以快速选出Leader。
在日志复制层面,Raft采用的是一个典型的两阶段提交。为了让算法简单,它直接在日志复制过程中,强制Follower和Leader同步,来解决删除未提交的脏日志的办法。而为了做到这一点,Raft又需要确保无论Leader如何通过选举切换,都需要包含最新已提交的日志的办法。而要确保这一点,它也只是在Leader选举的时候,带上了日志索引和任期信息就简单地解决了。
那么,在下一讲里,我们会深入Raft的安全性,看看能不能通过理论证明,进一步地,我们还会一起来看看Raft的成员变化问题,以及Raft的最终性能是怎么样的。
对于Raft算法,你不仅可以去读论文原文,也能直接在网上找到论文两位作者对应的讲座。我把链接放在这里,你可以自己去看一下。
在Raft集群刚启动的时候,我们是如何来确定哪一台服务器是Leader的呢?欢迎在留言区分享你的答案和思考,也欢迎你把今天的内容分享给更多的朋友。
评论