30分钟了解Raft算法

2023年 7月 25日 54.6k 0

Raft算法是什么

当存储系统单点部署时,单个节点出故障就会导致数据丢失、系统不可用等问题,因此各大公司用于生产的存储系统一般都会部署多个节点。分布式系统最大的挑战是数据一致性问题,即需要保证多个不同节点上的数据保持一样。

image.png

最简单的分布式存储系统如上所述,有一个master节点和slave节点,client的写入操作全部到master,读取操作可根据需求选择master和slave节点。该方案能够较好的解决数据一致性问题,但是引入了另一个问题,当master挂掉时,存储系统将不可用。

image.png

Raft是一个共识算法(Consensus Algorithm),它保证当集群中小于一半的节点挂掉时,存储系统仍然可用。单个Raft节点由三部分组成:

  • 共识模块
    • 接收请求,并复制到所有节点;当一个请求被超过一半的节点接收时即commit(也就是应用到状态机中,代表请求成功)
  • 日志
    • 请求以日志的形式写到磁盘中,保证数据不丢
  • 状态机
    • committed的日志以相同的序列进入多个节点的状态机后,将得到相同的结果,这个机制保证不同节点的存储数据是相同的
      在系统运行正常时,Raft集群跟上面的简单一主多从集群非常相似,但是当主节点挂掉时,Raft算法可以自动选出新的主节点,保证集群可用性。

image.png

Raft算法将共识问题分解成以下三个子问题:

  • 选主
    • 一个集群中同一时刻有且只有一个Leader,当原来的leader挂掉时,新的leader马上会被选出来
  • 日志复制
    • leader接收client的请求且将日志复制给follower
  • 安全性
    • Raft算法保证,当任何一个节点将某条日志应用到状态机之后(也就是committed),那么所有节点都将保证这个index的日志是同一条(这里又引入了一个日志index的概念,后续会介绍)

选主

节点的状态

image.png

每个raft节点的状态会在follower、candidate、leader之间转变。

  • 节点启动时状态为follwer
    • 当一段时间之内,没有收到来自leader的心跳,则转变为candidate
    • 如果收到leader的心跳,则维持follower
  • candidate将向所有节点发起选举,
    • 如果在选举过程中,发现leader已经存在,则变回follower
    • 如果选举成功,变为leader
    • 如果选举不成功,但是又没收到来自leader的心跳,则term+1,继续下一轮选举
  • leader将向所有节点发送心跳,维持自己的leader地位
    • 如果发现有更高term的leader存在,则变为follower

这里又引入了一个term的概念。raft算法保证,同一个term最多只可能存在一个leader,这是由Raft的选主机制决定的,下一节会细讲。

image.png

节点间RPC通信

为了说明选主过程,我们需要引入两个rpc方法。

一个用于竞选,叫做RequestVote

message RequestVoteRequest {
    int64 term = 1;
    int64 candidateId = 2;
    int64 lastLogIndex = 3;
    int64 lastLogTerm = 4;
}

message RequestVoteReply {
    int64 term = 1;
    bool  voteGranted = 2;
}

一个用于心跳(以及后续会讲到的日志复制),叫做AppendEntries

message AppendEntriesRequest {
    int64          term = 1;
    int64          leaderID = 2;
    int64          prevLogIndex = 3;
    int64          prevLogTerm = 4;
    repeated Entry entries = 5;
    int64          leaderCommit = 6;
}

message AppendEntriesReply {
    int64 term = 1;
    bool  success = 2;
}

message Entry {
    int64  operation = 1;
    string key = 2;
    string value = 3;
    int64  index
}

选主的过程

以五个节点的集群为例:

1)初始状态下,所有节点的term都为1,状态都为follower,同时开始一轮倒计时。如果在倒计时结束前,follower收到RequestVote或者ApependEntries的请求,且请求中的term大于等于follower的term,则保持follower状态。如果倒计时结束,没有收到请求,则转变为candidate。这里有一个细节,每个节点的倒计时时间是随机的,有快有慢,所以会有节点先于其他节点成为candidate。

image.png

2)有一个节点先于其他节点成为candidate,向剩余节点发送RequestVote,它需要得到多数票才能成为Leader。另外一个follower在一个term里只能投票给一个candidate,由此可以保证一个集群在一个term里只有一个leader。

image.png

3)成为leader后,需要向其他节点不断发送心跳,维持leader地位。
 
image.png

4)如果其他pod收不到心跳,则会认为leader挂了,则会term+1并转变为candidate参与竞选。图中2号leader挂掉,3号成为新leader。

image.png

5)此时如果2号leader又活过来了,它会收到来自其他leader的term更大的心跳,转变为follower。

image.png

日志复制

竞选成功后,leader需要承担起日志复制的责任。

一次成功的日志复制过程

1)leader收到请求,写入自身日志,然后使用AppendEntries命令发送给followers。这条日志目前处于uncommitted状态,图中用虚线表示。这里注意,每条日志都会带上term和index信息,确保唯一性。

image.png

2)follower收到请求,反馈给leader成功。当leader确认大于一半的节点成功复制日志后,这条日志就转变成committed状态,可以提交到状态机里了,图中用实线表示。

image.png

3)在下一次心跳时,leader把commited信息告知各节点。

image.png

raft如何保证日志的一致性

1)Raft 保证:如果不同的节点日志集合中的两个日志条目拥有相同的 term 和 index,那么它们一定存储了相同的指令。这是因为:每一个term最多只会选出一个leader,leader同一个index只会写一条日志,且leader不会修改日志。

2)同时 Raft 也保证:如果不同的节点日志集合中的两个日志条目拥有相同的 term 和 index,那么它们之前的所有日志条目也全部相同。这是因为:leader 发出的 AppendEntries请求中会额外携带上一条日志term和index,如果follower在本地找不到相同的日志,则拒绝接收这次新的日志。

3)所以,只要 follower 持续正常地接收来自 leader 的日志,那么就可以通过归纳法验证follower和leader拥有相同的日志序列。

image.png

leader遇到的实际情况

image.png

分布式系统非常复杂,当leader上任时,有可能会遇到a-f的各种情况:

  • a、b两个follower日志复制的进度略落后与leader
  • c、d两个follower日志比leader多几条
  • e、f两个follower日志比leader多几条同时少几条

这里需要注意,raft的机制(在下一节会细讲)保证选上的leader拥有所有committed的日志,follower比leader多出的日志全部都是uncommitted的。

下面讲这些情况发生的原因:

  • a和b少几条日志
    • 比较简单,中间挂掉了导致少复制了一些日志
  • c比leader还多一条term6的日志
    • term6的leader将11号日志复制给c后挂掉了,没能复制给其他节点,现在的leader在term8竞选成功
  • d比leader多两条term7的日志
    • 情况跟c差不多,term7的leader将两条日志同步给d后挂掉了,没能同步给现在的leader
  • e比leader多出两条term4的日志,少term5、6的日志
    • term4的leader同步两条日志给e后来不及committed就挂掉了,随后e自身也挂掉了,没能收到term5、6的日志
  • f比leader多处term2、3的日志,少term5、6的日志n
    • 跟e类似,term2、3的leader同步日志给f后来不及committed就挂掉了,然后f自身也挂掉了

至于leader处理不一致的方法很简单:向前回溯follower跟自身的分歧点,然后从分歧点开始修复覆盖follower的日志。

安全性

到目前为止的这套机制还不能100%保证一致性,想象以下情况:

  • leader将某条日志复制到大部分节点上,日志commited
  • leader挂掉了
  • 没有复制到这条日志的节点选上了leader
  • 新leader把后续日志同步给其他节点,覆盖了那条commited的日志
  • 因此仍然需要增加一些选主和日志commit的限制,才能保证一致性

    选举限制

    回过头来看选举的RPC请求,candidate竞选时会带上log index和term,follower只会给日志比自己新的candidate投票。因为leader当选必须获取超过一半的选票,所以能当选的leader一定比超过一半的节点拥有更新的日志。又由于一条日志想要commit,必须复制到超过一半的节点上,那么leader一定拥有所有已经committed的日志。

    message RequestVoteRequest {
        int64 term = 1;
        int64 candidateId = 2;
        int64 lastLogIndex = 3;
        int64 lastLogTerm = 4;
    }
    
    message RequestVoteReply {
        int64 term = 1;
        bool  voteGranted = 2;
    }
    

    日志commit限制

    复习一下commit是什么:一条日志确认生效,应用到状态机中。
    一般情况下,leader将日志复制到超过一半的节点后,即可commit日志,但是这么做在以下case时会出现问题:

    image.png

    a) term2的leader s1将日志复制到s2节点后挂掉了
    b) s5节点在term3选为leader,日志3还没来的急复制就挂掉了
    c) s1节点在term4重新选为leader,将日志2复制到s3后挂掉,此时日志2已经复制到超过一半的节点了,符合commit条件
    d) 因为此时s2、s3、s4的最新日志都是2,不比s5新,s5可以获得多数票;s5再次成为leader时,把日志3复制给s1-s4,覆盖了已经提交的日志2
    e) 位了避免这种情况,引入一条日志commit限制:leader只能commit当前term内的日志,之前的日志只能顺带同时提交;图中s1成为leader,把日志2复制出去后,没有commit,等到日志4复制出去后,才顺带commit,这样s1-s3的日志都比s5新,s5无法成为leader

    以上就是Raft算法的介绍了,更全面的信息可以关注参考资料。

    参考文档

    • raft官网 raft.github.io/
    • raft paper raft.github.io/raft.pdf
    • 深度解析 Raft 分布式一致性协议
    • 分布式理论(六) - 一致性协议Raft
    • 终于明白了,一文搞懂Raft协议

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论