掘金 后端 ( ) • 2024-04-14 17:56

raft-zh_cn/raft-zh_cn.md at master · maemual/raft-zh_cn (github.com)

Raft 分布式共识算法动画演示 (kailing.pub)

动画:Raft算法Leader选举、脑裂后选举、日志复制、修复不一致日志和数据安全_哔哩哔哩_bilibili

官方给的动画网站Raft Consensus Algorithm

raft 算法流程比较复杂,纯文字难以理解;动画比较直观,但是容易留有疑问为什么这么做?

把各个节点之间想象成各路诸侯,带着好奇来看微服务王朝更替时的精彩争斗吧😀

一致性算法是什么?

一致性,即达成共识

一致性算法是用于在分布式系统中确保不同节点之间数据的一致性的一类算法。

通过协调多个节点的操作,确保在面对网络分区、节点故障等情况下,系统依然能够保持一致的状态。

实际系统中使用的一致性算法通常含有以下特性:

  • 安全性保证(绝对不会返回一个错误的结果):在非拜占庭错误情况下,包括网络延迟、分区、丢包、重复和乱序等错误都可以保证正确。
  • 可用性:集群中只要有大多数的机器可运行并且能够相互通信、和客户端通信,就可以保证可用。因此,一个典型的包含 5 个节点的集群可以容忍两个节点的失败。
  • 不依赖时序来保证一致性:物理时钟错误或者极端的消息延迟只有在最坏情况下才会导致可用性问题。
  • 迅速响应客户端请求:一致性算法通常会尽可能快地响应客户端的请求,即使一小部分节点的响应速度较慢,也不会影响系统整体的性能。这意味着系统的性能不会受到个别节点的影响,而是整个集群的响应速度。

非拜占庭错误:因为不可预测的行为,导致系统无法正常运行,类似有内鬼节点。

管理复制日志:保持一致性的方案名称

Raft 一致性算法

Raft 通过选举一个杰出的领导人,然后给予他全部的管理复制日志的责任来实现一致性。

领导人从客户端接收日志条目,把日志条目复制到其他服务器上,并告诉其他的服务器什么时候可以安全地将日志条目应用。

过领导人的方式,Raft 将一致性问题分解成了三个相对独立的子问题

  1. 领导选举
  2. 日志复制
  3. 安全性

Raft 基础

image.png 在任何时刻,每一个服务器节点都处于这三个状态之一:领导人、跟随者或者候选人

  • 领导人: 系统中只有一个,并且其他的节点全部都是跟随者。

    领导人处理所有的客户端请求(如果一个客户端和跟随者联系,那么跟随者会把请求重定向给领导人)

  • 候选人: 选举新领导人时使用

  • 跟随者: 被动的,简单的响应来自领导人或者候选人的请求。

领导人选举

当服务器程序启动时,所有人都是跟随者

跟随者响应来自其他服务器的请求。如果跟随者(限定时间内)接收不到消息,那么他就会变成候选人并发起一次选举。Raft 使用一种心跳机制来触发领导人选举。

选举时,候选人首先增加自己的任期号,并调用RPC让其他跟随者投票。

获得集群中大多数选票的候选人将成为领导人,他会立即通知其他人,并让他们回到跟随者的状态,阻止发生选举。在一个任期内,领导人一直都会是领导人,直到自己宕机了。

如果有多个候选人让跟随者投票怎么办?

每一个跟随者最多会对一个任期号投出一张选票,按照先来先服务的原则。

候选人如果在候选时接受到了其他人的领导命令,怎么处理?

根据发送来的任期号,如果小于等于自己就拒绝;反之,则承认。

如果选举后没有选举出领导人该怎么办?

image.png 这个问题也叫脑裂投票结果分裂,有多个同票的候选人。

Raft 把时间分割成任意长度的任期,如果一个候选人赢得选举,然后他就在接下来的任期内充当领导人的职责。

在某些情况下,一次选举过程选票被多个节点瓜分,导致没有节点超过半数票而选举出领导,这一任期会以没有领导人结束;

当这种情况发生的时候,每一个候选人都会超时,然后通过增加当前任期号来开始一轮新的选举。

接下来选举后还是没有选举出领导人该怎么办?

当没有选举出领导人的问题发生的时候,每一个候选人都会超时,然后通过增加当前任期号来开始一轮新的选举。

如果没有一个良好的机制,确实会存在一直选举的可能性

于是 Raft 算法使用随机超时时间的方法来确保很少会发生选票瓜分的情况,就算发生也能很快的解决。这样减少了在新的选举中另外的选票瓜分的可能性。

为什么用随机设置超时时间?

经过大量的实验得出的结果,随机超时是最优的。比按照某些优先级进行重新选举更可靠(优先级高的挂了,会导致再次选举)

日志复制

领导人被选举出来后,他就开始为客户端提供服务。

客户端的每一个请求或指令都被附加到领导人的日志中去。同时,领导人并行地通过RPC将附加的内容向跟随者同步,跟随者同步成功后返回当前的状态告知领导人。

image.png

领导人决定日志条目到哪里之前是安全且可用的;这种日志条目被称为已提交

Raft 算法保证所有已提交的日志条目都是持久化的。领导人跟踪最大的将会被提交的日志项的索引,并通知追随者提交。

如果跟随者挂了,没有能力返回,或者返回的结果同步不成功,怎么办?

领导者会一直尝试向跟随者同步,直到同步信息成功

关于在不同的节点日志中的两个条目拥有相同的索引和任期号日志匹配特性总结为以下两点,

  • 存储了相同的指令 原因是领导者创建日志后,不会改变日志,只是向跟随者同步
  • 之前的所有日志条目也全部相同 由附加日志 RPC 的一致性检查所保证,每次检查后领导者就知道跟随者是不是和自己的信息同步了

实际的日志情况可能很复杂,每个节点都有可能不同,“群雄并起”,“天下大乱”。那么在一次新的任期开始后,领导者是怎么“一统天下”呢?

——“今天下三分,益州疲敝,此诚危急存亡之秋也。”

在 Raft 算法中,领导人是通过强制跟随者直接复制自己的日志来处理不一致问题的。

这意味着在跟随者中的冲突的日志条目会被领导人的日志覆盖

要使得跟随者的日志进入和自己一致的状态,领导人必须找到最后两者达成一致的地方,然后删除跟随者从那个点之后的所有日志条目,并发送自己在那个点之后的日志给跟随者。

领导人对每一个跟随者维护了一个 nextIndex,这表示下一个需要发送给跟随者的日志条目的索引地址。

当一个领导人刚获得权力的时候,他初始化所有的 nextIndex 值为自己的最后一条日志的 index 加 1。如果跟随者收到信息后发现不一致,就会拒绝该日志。领导人发现被拒绝后就尝试用更小的 index 继续尝试同步,直到同步成功。

为什么是领导者尝试使用更小的 index,而不是跟随者直接发送当前能匹配上的最后index,或者是更为详细的冲突长度信息给领导者呢?

别问,问就是在实践中,这种优化带来的好处十分有限,因为失败是很少发生的并且也不大可能会有这么多不一致的日志。

跟随者的 index 信息和领导者相同即为同步成功,此时跟随者冲突的日志条目全部删除并且加上领导人的日志(假设:蜀国北伐成功,统一天下,需要再次书同文车同轨)

通过这种机制,领导人在获得权力的时候就不需要任何特殊的操作来恢复一致性。他只需要进行正常的操作,然后日志就能在 RPC 的一致性检查失败的时候自动趋于一致。领导人从来不会覆盖或者删除自己的日志。

Raft 能够接受,复制并应用新的日志条目只要大部分的机器是工作的;

在通常的情况下,新的日志条目可以在一次 RPC 中被复制给集群中的大多数机器;

并且单个的缓慢的跟随者不会影响整体的性能。

安全性

领导人如果提交了新的信息后宕机,此时又有之前从宕机中恢复的追随者进入了候选者名单。

如果该候选者成功当选,他就会用自己的选期覆盖其他人的选期,让后日志复制同步后就会丢失其他人的数据,怎么办?

为了限制选举者,有两种解决问题的办法

  1. 保证任何的候选人对于接下来的任期号,都拥有了之前领导者任期的所有被提交的日志条目
  2. 忽视问题,候选者选上领导者后,之前拥有所有日志的追随者再把日志向领导者同步

显然第一种方法比较简单,大家选择一个当前最优秀的人继任总比优秀的人辅佐托孤强,而raft算法也是这么做的

选举限制

Raft 使用投票的方式来阻止一个候选人赢得选举,除非这个候选人包含了跟随者所有已经提交的日志条目,候选人才会获得跟随者的投票。

如果候选人的日志至少和大多数的服务器节点一样新,那么他一定持有了所有已经提交的日志条目。

Raft 通过比较两份日志中最后一条日志条目的索引值和任期号定义谁的日志比较新。如果两份日志最后的条目的任期号不同,那么任期号大的日志更加新。如果两份日志最后的条目任期号相同,那么日志比较长的那个就更加新。

提交之前任期里面未完全同步的日志

image.png

如图的时间序列展示了为什么领导人无法决定对老任期号的日志条目进行提交

  • 在 (a) 中,S1 是领导人,部分的(跟随者)复制了索引位置 2 的日志条目。

  • 在 (b) 中,S1 崩溃了,然后 S5 在任期 3 里通过 S3、S4 和自己的选票赢得选举,然后从客户端接收了一条不一样的日志条目放在了索引 2 处。

  • 然后到 (c),S5 又崩溃了;S1 重新启动,选举成功,开始复制日志。在这时,来自任期 2 的那条日志已经被复制到了集群中的大多数机器上,但是还没有被提交。

  • 如果 S1 在 (d) 中又崩溃了,S5 可以重新被选举成功(通过来自 S2,S3 和 S4 的选票,因为S5 的候选号码是3,大于他们的2),然后覆盖了他们在索引 2 处的日志。

  • 反之,如果在崩溃之前,S1 把自己主导的新任期里产生的日志条目,在上图中也就是4,复制到了大多数机器上,就如 (e) 中那样,那么在后面任期里面这些新的日志条目就会被提交(因为 S5 就不可能选举成功)。 这样在同一时刻就同时保证了,之前的所有老的日志条目就会被提交。

这里和 e 中的情况差不多,e 中领导者在S4先把别的跟随者(从前的跟随者可以直接接着复制日志)同步到最新的选期号。

之后,即使领导者 S1 宕机了,拥有最大日志号码的跟随者明显在成为候选者后,当选领导者的可能性更大,就保留了之前的未提交的记录可以继续执行。

S5 当选的可能就决定于 S1 这位先前领导者把日志复制的占比了。

Raft 永远不会通过计算副本数目的方式去提交一个之前任期内的日志条目。只有领导人当前任期里的日志条目通过计算副本数目可以被提交;一旦当前任期的日志条目以这种方式被提交,那么由于日志匹配特性,之前的日志条目也都会被间接的提交。

计算副本数目:计算,或者是数出来有多少个和当前日志条目相同的节点,即检查该日志条目是否已经被复制到了大多数服务器上

小故事(不太准确,图一乐,这里文字有点难理解,可以看动画Raft Consensus Algorithm):

为了让大明江山的继承人能够坐稳皇位,朱元璋在临终前削藩(暗杀各地觊觎皇位的封王),并为太子增加势力,培养了很多忠心耿耿的托孤重臣——这就是之前的领导者为了维护一致性,复制新的版本号给所有追随者。也是上面 e 的情况,接下来就是需要强行将自己的日志复制给其他追随者,实现集群一致性。

后来朱棣被逼无奈,选择造反,还打着清君侧的名号,当上了新皇帝——这就是上面 d 的情况,上一任领导者没有复制足够多的新选号,未拿到新选号的追随者居多,所以成为候选者后可以成为。成为领导者后,会强行将自己的日志复制给其他追随者,实现集群一致性。

下面是自己通过动画的演示

假设上面图片现在的局面是13(朱允炆刚刚继位),没有获得足够的支持,只有两个13,然后自己down了,没有将自己的13推广到全局

下面图片的12,S3就是朱允炆的皇叔朱标,成功篡位(因为有两个节点仍处于8的时代,加上自己总共3个,占了多数)

然后立刻相全局推广自己的政策,并把先帝的政策13全部去掉

跟随者和候选人崩溃

如果跟随者或者候选人崩溃了,那么后续发送给他们的 RPCs 都会失败。

Raft 中处理这种失败就是简单地通过无限的重试;如果崩溃的机器重启了,那么这些 RPC 就会完整的成功。

如果一个服务器在完成了一个 RPC,但是还没有响应的时候崩溃了,那么在他重新启动之后就会再次收到同样的请求。

Raft 的 RPCs 都是幂等的,所以这样重试不会造成任何问题。例如一个跟随者如果收到附加日志请求但是他已经包含了这一日志,那么他就会直接忽略这个新的请求。

幂等:是指一个操作或函数被重复执行多次,其结果与单次执行的结果相同。换句话说,无论执行多少次,幂等操作的结果都是一致的。