分布式 | Raft 共识算法

1,122 阅读14分钟

1、简介

  • Raft算法讨论的也是分布式系统中如何就一系列的值达成共识和各个节点日志保持一致, 但是Raft算法是通过一切以一个领导者为核心的方式, 所有写请求都是通过Leader去发表提案
  • Raft算法属于Multi-Paxos算法,它是在Multi-Paxos思想的基础上,做了一些简化和限制, 比如增加了日志必须是连续的,只有领导者跟随者候选者三种状态
  • Raft主要有三个子问题, 分别是
    • Leader Election (领导者选举)
    • Log Replication(日志复制),
    • Safety(安全, 数据恢复)

2、角色介绍(节点状态)

每个节点可能有三种不同的状态, 分别是

  • 领导者[Leader]: 负责处理写请求、管理日志复制, 不断地发送心跳信息
  • 跟随者[Follower]: 负责接收和处理领导者的消息(选举消息,数据写入消息等),当领导者心跳信息超时的时候或者倒计时结束时,会变成候选者发起投票选举请求
  • 候选者[Candidate]: 一个跟随者变成领导者的过度状态, 它需要去请求其他节点进行投票, 如果得到半数以上选票就会成为领导者.

图片说明

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

3、关于日志

下面是某一时刻节点A和B的本地日志的存储情况:

  • 日志ID 本地唯一标识每条操作日志的编号, 它是连续且单调递增` 的. 用于在分布式系统日志不一致时进行数据的恢复, 还有保证相同的日志序列通过相同的状态机时各个节点最终都能达到一致的状态.
    • 比如下面节点A有8条操作日志, 而节点B此时只有4条日志. 说明此时两个节点数据不一致了. 假如节点A此时是领导者,它通过与B基于心跳通信后发现它本地存储最新的日志才到第4条, 之后的每一次心跳请求, 都会向B同步复制一条操作日志. 在4次心跳后节点B就与领导者的日志数据保持一致了. 其实这个过程就是一次数据恢复的过程
  • 任期编号: 其实这个有点类似Paxos 算法里的提案ID, 但是它不是发起每个提案(把每一个共识协商的请求都叫提案)都会去递增, 而是在发出选举投票请求提案是才会去递增,在发出写请求时并不会递增, 所以在Raft算法了它又叫做任期编号[Term], 在日志里描述的是由哪个任期的领导者发出的提案请求(写请求). 在功能上与Paxos的提案ID一样, 每个节点不会接受比自己小的提案ID的请求, 在这里就是每个节点不接受比自己本地保存的任期编号小的请求(无论是选举请求还是日志复制请求)
  • 具体指令内容: 这个就类似与Paxos算法里的提案内容, 描述的是客户端真正的操作指令

在这里插入图片描述

4、核心共识协商过程

4.1 领导者选举的过程

  • 如下图一开始每个节点都属于跟随者[follower]状态, 并且每个跟随者或者候选者都有一个时钟倒计时机制, 就是那个外层不断缩小的线条, 每个节点的倒计时时长都是随机产生的一般在几百毫秒之间. 如果当前是跟随者, 一旦跟随者先倒计时结束, 就会成为候选者. 并且发起选举请求. 而如果是候选者, 一旦候选者先倒计时结束, 就会发起新一轮的选举投票请求, 这个过程也可以叫选举超时
  • 如图 节点S4和节点S5先后倒计时结束, 并且变成了候选者状态. S4节点先给自己投一票, 然后将本地保证的任期编号递增为2 , 然后把投票请求(或者叫提案请求, 提案ID是S4节点的任期编号[2])发送给其他4个节点. S1和S2和S3这三个跟随者先收到了候选者S4节点的选举请求, 按照先来先服务原则, 并且当前提案请求ID即任期编号2没有小于自己本地保证的任期编号1所以可以接受请求, 随后这三个节点都会把票投给候选者S4节点, 并且将本地已保证的任期编号改成当前提案请求的ID即2. 三个跟随者投票结束后, 候选者S5的投票请求也来了, 但是由于已经投过票了.所以三个跟随者都拒绝了候选者S5的选举请求. 最终候选者S4收到的选票是4张, 而候选者S5只有一张选票, 按照多数派原则[超过半数以上] , 候选者S4会把自己更新为领导者Leader状态, 然后领导者S4将会发送心跳包给其他3个跟随者和S5候选者. 跟随者收到领导者的心跳包之后将会重置自己的倒计时. 而候选者S5收到领导者的心跳后不但会重置自己的倒计时并且会降级为跟随者状态

在这里插入图片描述

4.1.1 选举超时案例

  • 如上图, S5节点在候选者期间也是有时钟倒计时机制的, 如果自己没有选举成功并且也没有收到新的领导者的投票请求(比如: 新Leader挂了或者都没有候选者得到半数以上的选票)
  • 比如下图, 新领导者S4发出心跳包之前就挂了, 那么倒计时还会一直进行, 一旦倒计时候选者S5又回发起新一轮的投票请求 ,并且把本地保证的任期编号递增为3.

在这里插入图片描述

4.2 数据写入过程(日志复制)

  • 假如现在客户端要写入一个数据(比如指令set x=3), 领导者S5收到后先把操作日志写到自己本地但是还未提交. 然后再发送日志复制请求给其他4个跟随者, 4个跟随者收到请求后发现该请求的任期编号没有小于自己当前本地已保证的任期编号(即都是3), 所以接受此次日志复制请求, 也将操作日志写到自己本地但是还没提交, 然后响应领导者. 当领导者S5收到半数以上的跟随者写入成功, 就在本地把该操作日志正式提交然后响应写入成功信息给客户端. 之后跟随者在收到领导者的心跳消息或者日志复制消息后, 如果发现领导者已经提交了某条日志,而它还没提交,那么跟随者就会把这条日志进行针正式提交.。

在这里插入图片描述

5 异常场景

5.1 Leader挂了

  • 领导者S3挂了之后, S4先倒计时结束成为候选者, 发起一轮选举投票最终得到超过半数以上的选票成了新的领导者. 这时旧的领导者S2又恢复了, 会先自动降级成跟随者. 收到领导者S4的心跳消息后, 因为当前心跳请求的任期编号3大于自己本地的任期编号2, 所以会把本地任期编号改成3. 并保证以后不处理比任期编号3小的任何请求(无论是选举请求还是数据写入请求)

在这里插入图片描述

5.2 数据不一致问题 (安全·恢复)

  • 比如 跟随者S1节点挂了, 而之后领导者S5写入了2次数据. 然后跟随者S4超时没有收到领导者S5的心跳消息变为候选者发出选举请求, 得到半数以上同意后, 节点S4变成新的领导者 ,随后领导者S4又写入了两次数据, 假设现在跟随者S1又起来了, 现在跟随者S1节点跟其他节点的本地日志数据不一致了. 缺了4条日志.

在这里插入图片描述 上面动图有一个细微变化, 就是当领导者S4当选后, 所有跟随者的日志读取指针都会与领导者保持一致(都是指向日志ID为5), 其实应该说相对于领导者来说它要去读取跟随者节点日志的指针, 因为领导者需要知道每个跟随者当前日志的情况, 是不是一致了还是缺失了还是有错误的数据了, 为什么要与领导者日志读取指针保持一致呢?, 因为日志的完整性要与领导者的为准, 即使你的日志比领导者还多, 领导者也会从它的日志读取指针开始同步覆盖你的日志数据与它保持一致.

跟随者S1具体的数据恢复过程

在这里插入图片描述

  • 跟随者S1恢复后, 在第一次收到领导者S4的心跳后, 发现了领导者的任期编号4大于自己本地已通过的任期编号3, 就会把自己已通过任期编号改为4(以后只接受处理大于等于任期任期编号为4的请求), 同时跟随者S1发现它的日志和领导者S4的不一致了,那么跟随者就会拒绝接收新的日志复制,并返回失败信息给领导者.
  • 此时领导者S4的心跳消息里面是包含它当前本地最新日志的消息的, 比如有 <最新的日志ID为6, 最新的日志的任期编号为4>, 正常情况下, 如果跟随者本地日志最新日志也是 <最新的日志ID为6, 最新的日志的任期编号为4>, 那么就正常响应心跳请求. 但是比如跟随者S1节点此时日志指针的前一项(即最新日志位置)为空与领导者的对应位置的日志消息不一致了, 就会发送返回失败消息给领导者(如下图2.2).
  • 之后领导者收到失败消息后, 就会递减读取节点S1的日志读取指针, 比如递减为日志ID为4, 然后节点S1还是发现此时日志指针的前一项为空与领导者对应位置的日志消息还是不一致, 又返回失败消息给领导者, 领导者收到后又递减S1的日志读取指针了, 现在递减为3了(如下图2.3). 但是日志指针的前一项与领导者的对应位置的日志消息一致了, 就会返回成功消息给领导者S4
  • 之后领导者就会从日志指针3开始去覆盖恢复跟随者S1的日志数据了, 但是每次心跳只会恢复一条日志, 4次心跳之后, 最终就恢复了S1的数据了.

图2.2 图1.1

图2.3 在这里插入图片描述

5.3 日志完整性低的节点成为候选者问题

  • Raft还有一个原则就是 数据完整性原则, 只有数据完整性较高的候选者才能成为领导者, 因为领导者是需要去负责数据恢复的. 一个数据完整性低的候选者发出的选举请求会被拒绝.
  • 比如S1因为之前挂了, 导致本地日志数据缺失了两条. 刚好它先成为了候选者发起一轮投票请求, 跟随者收到后发现比自己的数据完整性低就拒绝了S1节点的选举请求, 并且更新本地已保证的任意编号为3

在这里插入图片描述 在这里插入图片描述

5.4 网络分区问题

  • 如图集群因为发生网络分区, 节点AB和节点CDE不可通信. 节点CDE由于收不到领导者B的心跳消息, 一旦倒计时结束就会开始选举出新的领导者. 比如节点C的3张选票超过半数成为领导者并且递增了任期编号为2. 此时Raft集群中就存在两个领导者了. 违背了Raft只有一个领导者的原则 在这里插入图片描述
  • 这时客户端先把写请求set 3提交到了 下分区的领导者B, 然后领导者B发送日志复制请求给其他节点, 但是只有节点C收到了并写入到本地日志但是并未提交. 领导者B收到2个写入成功请求并未超过半数以上, 所以领导者B返回写入失败给客户端
  • 这时又有一个客户端把写请求set 3提交到了 上分区的领导者C, 最终领导者C日志复制请求得到了半数以上节点的响应, 先把自己日志提交,再返回写入成功给客户端, 随后通知其他节点提交该条日志.
  • 此时网络分区恢复后, 领导者C收到领导者B的消息, 因为领导者B任期编号2小于自己所以不处理. 相反领导者B收到了领导者C的消息, 由于领导者C的任期编号大于自己, 所以领导者B会自动降级为跟随者, 随后按照5.2小节的数据恢复原理对跟随者B以领导者C为主进行日志数据的恢复. 在这里插入图片描述

5.5 成员变更问题

  • Raft集群节点数量的变更, 就可能存在新旧配置下大家对于集群节点的总数量判断的不一样. 最终导致每个节点的半数以上原则判断出现问题. , 可能会出现双领导者问题, 就像网络分区那样. 而Raft是通过单节点变更(single-server changes), 去解决这个问题

单节点变更

  • 比如说以前集群中有三个节点,现在需要将集群拓展为五个节点,那么就需要一个一个节点的添加,而不是一次添加两个节点。
  • 因为每次只增加和删除一个节点, 那么新旧两个配置间一定存在一个交集, 旧配置的“大多数”和新配置的“大多数”都会有一个节点是重叠的, 不会同时存在旧配置和新配置2个“大多数. 核心就是让大家”对大多数“保持一致的认知

在这里插入图片描述

具体变更的流程: 假设现在集群有ABC三个节点,现在 要新增加一个节点D

  • 1、领导者(节点B)先向新节点(节点D)进行数据同步.
  • 2、在完成数据同步后, 领导者(节点B)将新配置[A, B, C, D]集群列表作为一个日志项进行日志复制写到新配置中所有节点, 得到大多数响应后就提交到本地日志, 这样就保证它大家关于集群成员配置的一致性, 都是[A, B, C, D]了

6、总结

1、Raft的强领导模型是写要以领导者为主,类似与单机了。性能和吞吐量会受到限制

2、与Muti Paxos 共识算法的区别

  • 在Raft中,不是所有节点都能当选领导者,只有日志最完整的节点,才能当选领导者;第二,在Raft中,日志必须是连续的。
  • Raft算法通过任期、领导者心跳消息、随机选举超时时间、先来先服务的投票原则、大多数选票原则等,保证了一个任期只有一位领导者,也极大地减少了选举失败的情况。

3、随机倒计时机制 Raft算法巧妙地使用随机选举超时时间的方法,把超时时间都分散开来,在大多数情况下只有一个服务器节点先发起选举,而不是同时发起选举,这样就能减少因选票瓜分导致选举失败的情况。

[动画演示网站]