多图详解分布式Raft算法的一致性保证

2,845 阅读11分钟

分布式之Raft算法

仔细思索分布式的CAP理论,就能发现P数据分区是分布式的特性,是必须满足的特点。如果一个系统没有数据分区的存在,那这样的系统就是单体,而不是分布式。CAP三个特性无法同时满足,那么所有的分布式系统实现都是在A(可用性)和C(一致性)之间权衡选择。

Raft是一个分布式的一致性协议算法,放弃了CAP的可用性,保证严格的一致性。

Raft的应用非常广泛,etcd、consul等都使用了Raft协议。

Raft是一个分布式的一致性协议算法,通过选主机制和日志状态机模型来保证分布式系统的数据强一致性,如下图的基本模型:

ra1.png

关于选主机制和日志状态机模型,下面会详细的论述。

Raft的选主机制

为啥要有leader

在Raft协议里,系统中的所有节点中必须选出一个Leader。

因为分布式的数据分区特性,如果没有Leader来协调数据处理,就很难保证数据是一致的了。 如果没有Leader,则系统的每次操作都要进行一次投票,开销非常大。Paxos算法也是一个分布式一致性协议,使用P2P算法来保证,但是实际理解和实现都过于复杂。如果有一个leader会让决策变得简单快速。

Raft角色和演变

节点状态变迁如下:

raft6.png

Raft普通节点和Leader之间都有心跳,一有异常就等着重新选举。

  • 一个新的节点加入有leader的集群,先变成follower
  • term(任期):表示当前节点被选为leader节点的一段时间,如下图

raft9.png

  • S1在term=1这段时间内是leader,S2和S3是follower
  • 后面S3和leader没有心跳,则S3发起term=2的选举,但未得到大多数投票(term=2的时间很短,实际不存在这段时间)
  • 后面S2发起term=3的选举,得到大多数投票当选;
  • ...
  • term=4的这段时间,S3当选

系统参数和RPC通信

Raft系统有Leader、Follower、Candidate等角色, 每个角色都维护一些状态,遵守一些规则。

Raft系统采用RPC心跳来通信,主要有两种类型的RPC:

  • AppendEntries:
    • 用于Leader向其他Follower同步日志数据
    • 维护和Followers的心跳

ra2.png

  • RequestVote:Candidate调用的请求用于获取投票

ra3.png

选主过程

每次选主都有一个新的term,下图显示了选主的过程:

raft2.png

  • S2的时钟先发现心跳超时,发起选举(使用RequestVote的心跳包),作为Candidate投自己一票。
  • S3(使用RequestVote的心跳包)也投S2一票,则S2当选为新的Leader。
  • 当S1和S2重新有心跳时,发现系统有Leader,则也成为S2的Follower。

日志状态机

分布式要保证可用性,往往意味着多副本存储,且各副本节点的数据一致;Raft维持多副本数据一致的方式是使用日志状态机。

从上图对应的初始状态开始,这个时候如果有客户端发起操作数据的请求,这个数据会被leader接收,并且leader使用AppendEntries的RPC请求到其他各个Followers。

那么,什么是日志状态机呢?

日志状态机通过二元组(index,term)来作为日志状态向量

  • index是连续递增的数字,是日志的逻辑序号,各个副本上相同的index,有相同的数据日志
  • term任期也是连续递增的数字,如上面解释的那样,一个term则表示某个节点当选的一段时间

日志状态向量在Raft系统里面的数据复制和选主过程里发挥着重要的作用。

我们知道Raft分布式系统正常工作时就处理请求,内部复制日志,异常的时候发生新的内部选举,对外可能就出现短暂的可用。下面分这两种情况具体分析。

日志状态向量在正常复制流程是怎样工作的?

下图也整体表现了日志状态向量在正常复制流程是怎样工作的。 raft13.png

S1是Leader,从图上可以看出,此时的S2复制了S1的全部日志,而S3还有所落后。

放大复制的日志条目来看: raft-rep1.png

  • Leader知道大部分节点都成功之后,将这个index的日志状态设置为committed。Follower通过心跳包得知这个log(日志状态向量标记的)已经被成功复制,然后所有节点会将该日志状态设置为committed。

  • commit的日志可以应用到系统上(板上钉钉,不会回滚)。

  • S3比较慢,同步到(index=2,term=1),和其他节点的(2,1)对应的数据都相同。

  • index=4,term=1)的数据已经有大多数统一,则状态为committed

  • 此时客户端都知道y=1,z=9,x=1

异常时,日志状态向量在选举流程是怎样工作的?

选举的时候也会利用到这个日志状态向量(index,term)。

客户端找Leader

如下图, Raft算法规定客户端将所有请求发送给Leader。

r1.png

如果Leader突然宕机,则客户端的请求不通。

r2.png

客户端启动的时候,如何知道哪一个节点是Leader呢?

具体办法是客户端随机挑选一个服务器进行通信,如果客户端选的服务器不是Leader, 那么被挑选的服务器会拒绝客户端的请求, 并且提供它最近接收到的Leader的信息,即通过收到Leader发送的心跳的RPC得到Leader的网络地址。 如果LEADER已经崩溃了,那么客户端的请求就会超时,客户端之后会再次重试随机挑选服务器的过程。

对于下图这种情况,客户端发送的请求则是无效的。

r3.png

在Raft系统里,特殊情况下,一部分请求可能存在短暂的不可用, 但保证了系统的严格一致性。

日志状态机帮助正确的选主

那么异常发生的时候,也就是说选举等过程是怎么利用这个日志状态向量的呢? 如下图的演变过程:

raft11.png

  1. 初始是S1是Leader,复制数据给S2和S3.

  2. 假设S3先发起选举Candidate(term=2), 因为S3节点最近commit的数据(index=2,term=1), 而S2最近commit的数据(index=4,term=1),已经commit的数据包含S3所没有的(index=3,term=1)、(index=4,term=1),也就是说数据比S3新,则S2不会投票给S3。

  3. 此后,S2会也发起选举Candidate(term=3),因为S2的最新commit的日志状态向量是(index=4,term=1), S3此时的日志状态向量(index=2,term=1),S3知道S2的数据比自己新,则同意S2的选举。

  4. 随后继续接收请求,复制数据。

通过上面的分析,可以看出,日志状态机使得不至于选出拥有很老数据的节点来当Leader。

Raft为何一定要设计term这个概念?

上面章节的假设稍微有一点改变,假设S3 term=2的选举也是成功的,因为客户端的请求完全相同,这两个情况下的系统最后都有完全一样的数据(1,2,3,4,5,6,7)。

raft12.png 两个假设的场景,系统有完全一样的数据(1,2,3,4,5,6,7),但是通过term的表示可知道系统演化的状态是有区别的。(index=2,term=1)和(index=2,term=2)是不同的日志状态向量。

从上面的分析我们知道,有了term,我们才知道这个事情发生时的状态。

Raft设计任期这个概念,主要是基于这样的一些考虑:

  • 首先很形象,就像总统选举一样,每次都有一个任期号
  • 每个Term都有自己的leader,而且这个Term会带到log的每个entry(AppendEntries)中去,来代表这个entry是哪个leader 的term时期写入的。
  • 任期连续递增。如果在规定的时间内leader没有发送心跳,Follower就会认为leader已经挂掉,会把自己收到过的最高的Term加上1做为新的term去发起一轮选举。

如果参选人的term还没自己的高的话,follower会投反对票,保证选出来的新leader的term是最高的。

raft14.png 如果在time out周期内没人获得足够的选票(这是有可能的),则follower会在term上再加上1去做新的投票请求,直到选出leader为止。

最初的Raft是用C语言实现的,这个timeout时间可以设置的非常短,通常在几十ms,因此在raft协议中,leader挂掉之后基本在几十ms就能够被检测发现,故障恢复时间可以做到非常短。

新加入的日志是怎样应用压缩日志的?

  • 当日志被leader做了快照并删除了的时候,leader需要把快照发送给follower
  • 或者集群有新的节点加入,leader也可以直接发送快照,大量节点日志传输和回放时间。

快照只包括已提交的数据。 将已提交的数据和未提交的数据一起给follower就可以,大大节省了日志传输和回放时间。

raft5.png

未提交的日志在选主之后何去何从?

日志都提交的场景

这个场景,发起选举时,假设黑色节点比红色节点的index大1,且是提交的。此时黑色节点和红色节点commit的日志是(index=6,term=4), 黑色节点都有提交的数据(index=7,term=4)。

r6.png

选举后:

  • Follower的日志落后,从落后的地方开始复制Leader的数据。

有一个日志index未提交

下图这个场景,发起选举时,假设黑色节点比红色节点的index大1,但是这个数据的状态因为没有大多数的认同,目前还处于uncommit的状态: 此时黑色节点和红色节点commit的日志是(index=6,term=4), 黑色节点都有未提交的数据(index=7,term=4)。

r8.png 两种情况:

  • 黑色节点其中一个发起选举,被选为leader
    • 将未提交的数据也复制给其他Follower,等这个日志提交
  • 红色节点其中一个发起选举,被选为leader
    • leader上没有未提交的index日志,黑色节点的数据要保持和leader的一致,则会删除 未提交的数据(index=7,term=4)

新Leader当选之后的数据复制

这个图是Raft论文里面的一个图,这是一个Raft系统某一时间的日志状态场景:

此时这个Leader节点处于任期8,准备向其他节点复制数据。

当选发生的时机是d宕机,当前节点发起TERM=8的选举,看看其他节点对选举的反应:

  • f节点:Candidate的当前日志(index=10,term=6)比f节点当前日志(index=11,term=3)的term大,所以f同意
  • a同意
  • b,e同意
  • 可是c不同意,d也不同意,不过已经有5票了,当前节点当选
    • c当前日志(index=11,term=6),Candidate的当前日志(index=10,term=6),先比较term相等,但index 11>10,所以c不同意
    • d当前日志(index=12,term=7),Candidate的当前日志(index=10,term=6),先比较term 7<6,所以d不同意

r9.png

变为Leader后,当前节点要把数据同步给其他节点,那么对于每个节点是怎么同步的呢?

r10.png

  1. a节点接收一个(10,6)
  2. b节点还有很多数据要同步,从(5,4)开始
  3. c节点多了一个,删除(11,6)
  4. d节点多了两个(12,7),(11,7),需要删除
  5. e节点要同步的数据也比较多,(7,5)和leader的(7,4)不一样,(6,5)和Leader的(6,4)也不一样,这两个日志先删掉,然后重新从leader同步数据过来
  6. f节点也是,需要删除的日志很多,然后再同步。
    • 产生f节点的原因是因为它当选了term 2和 term 3的leader,但是数据都不曾同步给其他节点,如下面这张图所示

r3.png

扩展问题

Raft复制和MySQL的复制有什么异同?

  • MySQL的binlog也是日志,被复制到其他的备机。

  • Raft在有异常的时候,是自动的选主、切换主备。

  • MySQL没有通过投票选主机制+复制日志的状态模式来选主,一般都是手动切换的。这是因为Raft看重分布式的一致性;而实际应用的时候,MySQL更看重可用性。

  • MySQL不支持这么复杂的分布式情况,侧重点在业务关系复杂性,保证高可用性。

什么是脑裂现象

在一个分布式系统系统,如果出现多个leader,则是脑裂。

r5.png

怎么解决? 需要设置大于半数的节点同意才是主。

如上图,如果要满足必须大于半数的节点同意才是主,则选不出Leader,系统瘫痪。

但是如果使用奇数个节点,必须大于半数的节点同意才是主,这样很容易满足,如下图:

r4.png

参考文献