聊聊Raft算法

gosh

1.概述

最近终于有时间去看看raft算法的论文,raft作为一个易于理解和实现的一致性算法(毕竟我这样的菜🐶都能看懂),已经应用到很多系统的构建上(分布式数据库TIDB,RocketMq的Dledger等),是非常值得去深入理解和研究的。本文主要是学习raft论文的总结以及自己的一些想法。如果大家对论文已经足够了解。那就不用继续往下看了。光看看文末的个人想法(愚见),欢迎来和我探讨!

2.Raft算法概述

raft算法之所以容易理解,其一是他将一致性问题划分成几个子问题,这几个子问题都是独立、可理解和解释的。从传统的思维来讲,对于一个复杂的系统或者工程,都是大化小,分解实现,然后去尝试融合解决整体逻辑。包括CS系统的设计也是如此。

一致性算法的目标

1.安全性:在非拜占庭错误情况下,包括网络延迟、分区、丢包、冗余和乱序等错误都可以保证正确。
2.可用性:只要集群中大多数节点处于runing,并且不分区,和客户端能通信,那么我们需要保证这个集群可用。
3.对于数据同步,小部分慢节点的不会影响系统性能。因为对于日志复制,我们如果等待所有节点响应,那么系统的性能会存在短板效应。

说白了,就是如果一个集群中,如果大多数节点可用(网络、服务),那么通过raft算法,我们就能保证整个系统可用(可处理请求,数据一致性)。后面我们主要研究的就是raft是如何做到的。

首先我们要知道,Raft算法将其问题划分为

  • 领导选举
  • 日志复制
  • 安全性

对于一个集群只有一个leader(领导),那么我们就很容易理解。只要领导操作同步到对应的followers(跟随者),数据必然一致。当leader宕机,需要进行领导选举。

日志复制其实就是同步操作数据的过程。leader将操作日志同步到其他节点。

安全性:如何安全的同步,在不同的情况,我们都能保证一致性,这也就是安全性需要考虑的问题。

其实就是如此,raft首先假设了领导选举。然后实现了日志复制,最后在安全问题上解决上面的漏洞问题。

3.领导选举

目的:当集群初始化或者领导gg的时候选出一个新的领导。毕竟一个集群不能没有领导,如果没有,那么这个集群就不可用了。

触发机制:通过心跳。
1.png
这幅图展现了跟随者、候选者和领导者之间的状态转换关系,下面主要介绍他们的转换流程。

跟随者

如果他能持续从领导者或者候选者接收到有效的RPCs,那么他的状态就不会变。一直保持跟随者身份。但如果没有持续接受,也就是在一段时间没收到有效的RPCs,那就选举超时,他会变成候选者。

候选者

要变为候选者,也就意味着他要开启一轮新的选举。那么跟随者会增加自己的任期号,转为候选者。

成为候选者后,自己会并行发送一个为自己投票的RPCs请求 给其他服务器。

成为候选者的整个过程也会保持当前状态,知道满足下面三个条件

  • 他赢得选举,转变为领导制
  • 其他节点赢了,他转为跟随者
  • 一段时间没有任何人获胜。说明选票被瓜分,重复执行。

这里需要注意的点:

1.对于同一任期号,每个节点一会投一张票。比如服务器A作为候选者生成了编号为5的任期号,那么如果接收到其他节点的编号为5的任期号的投票请求,他会忽略。这个过程遵循的是先来先投的原则。
2.候选者接收到领导者的声明。会判声明中RPC的任期号,如果比自己的还小,那么他还会保持候选者身份,否则转为跟随者。
3.对于上面第三点,重复执行,Raft采用随机超时选举时间进行了优化。降低选票瓜分的可能性。

4.Raft日志复制

直接去理解日志复制,是很容易的,客户端的一条指令到达,领导者会为这条指令创建一条日志条目,并且并行发送到其他跟随者。当日志被安全复制(所谓安全复制后面会有),领导会将日志应用到状态机(比如如果是mysql的insert,那么就是执行insert操作),然后响应客户端。

2.png

如上图,每条日志都会有对应的任期号,和指令。
每个日志都会有对应的索引。

raft日志匹配特性
1.如果在不同的日志中的两个条目拥有相同的索引和任期号,那么他们存储了相同的指令。
2.如果在不同的日志中的两个条目拥有相同的索引和任期号,那么他们之前的所有日志条目也全部相同。

第一点:一个任期只有一个领导人,并且领导人在一个任期中对于同一索引日志,只会创建一条日志,是不会改变的,是确定的。这就保证第一点成立。
第二点:要想全部相同,就要保证跟随者得到的日志是领导者发送的顺序附加上去的。领导者在发送新的日志时,会附加这条日志之前日志的索引和任期号。如果跟随者发现数据匹配,才会附加上去,否则拒绝。就是一个个状态保证了日志的匹配特性。

对于日志不一致的现象,raft是通过跟随者强制复制领导者的日志来保证的。

3.png

如上图,对于a-f,最终都会和leader同步,也就是说,d会丢弃日志。f的对应日志也会被丢弃和覆盖。

其实就是通过日志覆盖解决。但是对于日志覆盖,我们就会想到一个问题,会不会覆盖已经提交的日志(日志对应指令已经返回给客户端)。那当然不会,如果真有这样,就会有不一致,或者指令丢失现象。

那么如何去做覆盖跟随者日志

其实就是跟随者在append日志的时候,会进行错误校验。

在候选者成为领导者的时候,会为每个跟随者初始化一个nextIndex数组,数组的值初始化为自己最后条日志+1,其实就是理想化状态,默认认为日志都已经同步成功。但是理想总会因为各种原因导致不正确,就用上面那张图。leader会初始化所有nextIndex为11,但是在同步日志的过程中。f节点会出现校验错误的响应。因为f节点10索引对应的日志和leader10索引对应的日志不相同(主要是根据任期号判断)

这里再强调一下为什么根据任期号就可以判断日志是否一致,就是上面所说的日志匹配原则。

5.安全性

我们明白了如何选举和日志复制,但是没有考虑安全性问题。其实我上慢提到,比如一个宕机很久的跟随着会被选为领导者,进行日志覆盖操作会有丢失问题。

其实解决这个办法很简单,就是在领导选举的时候,只能让安全的节点当leader,所谓安全,就是对应节点拥有当前领导者已经提交的所有日志。Raft就是这么做的。

Raft中节点在投票的时候,会判断被投票的候选者对应的日志是否至少和自己一样新。如果不是,则不会给该候选者投票。

日志比较的方法:
1.最后一条日志的任期号。如果大说明新。如果小,说明不新。如果相等。跳到2
2.判断索引长度。大的更新。

还有一个问题,就是领导人不能保证一个已经在大多数节点存在的日志是否已经提交。

4.png

如图

a、b、c、d、e代表不同的任期阶段

(a)S1是leader。同步任期2的数据给S2

(b)S1宕机,S5当选(S3、S4、S5投票),产生任期3的日志

(c)S5宕机,S1恢复当选(同步任期2的数据给S3)。

(d)S1宕机,S5当选(因为他的任期日志比其他的都新),复制了任期3的所有数据。

假如说在c阶段,S1提交了任期2的数据,那么如果出现d,则会导致任期2数据被覆盖,丢失。也就是说,S1在任期4时候,不能保证已经在大多数节点存在的日志(任期2的日志)是否提交。

所以raft永远不会通过计算副本数目的方式(大多数存在)去提交一个之前任期内(任期2)的日志条目。只有领导人当前任期里的日志条目通过计算副本数目可以被提交(e阶段)。这样之前任期的数据也会被提交。

那这里我理解一点就是,加入S1当选为leader,如图c状态,那么,如果不再有新的日志出现,任期2对应的日志就不会提交。那么会导致客户端对应的任期2请求失败。

6.跟随者和候选人宕机

这个就比较容易理解了,宕机的话RPCs就会失败,Raft通过无限重试却解决这个问题。
所以对于每个RPCs,做到幂等和无限重试,在节点恢复后,就还是会保证一致性状态。

7.Raft集群成员变化

对于集群成员配置变化,如果直接更新每台机器配置,那么就会有安全性问题。以为对于同一时刻,不同节点使用的不同的配置去执行算法逻辑,这就是不安全的。

5.png

如图,蓝色代表新的配置。绿色代表老的配置。Old状态有三台机器Server1、2、3。 New加入两台server4、5。

那么随着配置时间应用的不同,可能会导致选举出两个leader。
比如server1、2使用老配置,那么1和2都有可能被当选为leader。3、4、5使用心得配置,他们之中的一个也会被当选为leader。

其实这个问题原因就是节点使用了不同配置执行算法逻辑。为了解决这个问题,raft采用两阶段方法(其实只需要保证不会让新或者旧配置单独作出决定就行)

6.png

raft吧配置当作普通日志形式去提交。

为了实现两阶段,引入了C(old、new)配置。
还有一点就是,一点一个新的配置日志增加到对应的节点日志中,那么该节点就会立刻使用这条新的日志配置。

对于C(old、new)配置,其实就是只有同时满足old和new配置的时候才会生效。

这样理想状态下,如果拥有C(old、new)配置的节点当选为leader。并且提交了该配置,那么说明C(old、new)配置已经在大多数节点应用。下次选举的产生的leader日志中必然会有该配置。这个时候在创建一条新的C(new)配置提交,即可。

8.个人想法总结

其实对于raft算法,论文描述还是非常清晰并且容易理解的。核心问题的分解。让我们的思路会变得非常清晰。
对于raft的设计,其中任期这个概念是非常绝妙的。无论是在领导人选举,日志复制的时候,都是非常依赖任期的。

任期这个单调增长的概念,让多节点处理事情变得非常易于理解。

对于领导选举,论文做到让我们只关注选举问题。通过单调增长任期,以及投票进行选举,并且在逻辑上设计了节点在各个角色的职能和转换。

对于日志复制,也是通过日志匹配规则保证了日志复制的正确执行。其实还是依赖任期。无论是在日志提交,还是在判断是否接受的时候,都是依赖任期,必要时依赖日志索引。对日志数据是无状态的。

完成上述两个过程,如果不去看安全性,我们其实自己都能想到选举过程中如果没有限制条件的漏洞。在复制过程中可能会存在数据被覆盖丢失的问题。
然后raft通过候选人的最后日志条目的索引值候选人最后日志条目的任期号来决定是否能当选。


通过对算法的学习,让我们更加易于去理解分布式一致性问题。并且让人和惊喜的是,根据论文描述,我们自己去用代码实现算法是非常容易的。毕竟论文开篇为我们吧数据结构都定义好了。

对于算法的实现,其实业界有很多,这里介绍一个哥们的实现,我大概看了一遍,逻辑上没啥大漏洞,可以助于大家理解。https://github.com/stateIs0/lu-raft-kv
如果想要深入理解,推荐大家还是看看开源项目。比如sofa-jraft。
https://github.com/sofastack/sofa-jraft

文章参考

raft论文:In search of an Understandable Consensus Algorithm (Extended Version) 

阅读 1.3k
6 声望
1 粉丝
0 条评论
你知道吗?

6 声望
1 粉丝
宣传栏