万字解析raft

1.前置知识(可以跳过)

1.1 为什么要有分布式?

分布式主要是为了解决大规模数据的读写问题(这在后续的raft论文中会有一些小问题)
解决方式无非两个:

  1. 更好的机器,属于纵向扩展,在系统设计上没什么工作量,但压力来到了硬件设计这一侧. 机器的性能达到一定程度之后,想要更进一步的梯度会陡然提升。
  2. 更多的机器,属于横向扩展,理论上来说只要机器更多,没有解决不了的问题(doge)

对于第二点需要有个补充,我们不可能无上限的增加机器,一是物质上目前的生产力还不可能支持无上限的机器,二是一个集群的节点数量是一定有上限的,超过这个上限一定会对集群造成影响

1.2 分布式所带来的问题

首先,理清一个概念. 本文所谈及的分布式,更多指的是在同一模块内,为提高系统的吞吐量而采用的多节点的横向分布式,而非基于职责内聚性而进行模块划分并通过 rpc 交互串联整体的纵向分布式.

那么,这种横向分布式的优势主要体现在两点:

(1)安全:避免单点故障导致数据丢失或服务不可用,我们有更多的机器去做备份,也有对应的持久化策略

(2)负载均衡:多个节点共同分担总任务,那么理论上单个节点的任务强度与节点数量成反比。

当然也会有相应的问题出现:
(1)数据一致性:如何保证数据一致性,也就是说如何保证我们每次读到的数据是一样的(后面会专门介绍一致性分为两种)

(2)容错性:如何避免出现脑裂这种类似的问题。

下面我们来说一下经典的分布式系统理论:CAP 理论(Consistency-Availability-Partition tolerance Theory)。

(1)C:Consitency,一致性.

该项强调数据的正确性. 具体而言,每次读操作,要么读到最新,要么读失败. 这要求整个分布式系统像是一个不可拆分的整体,写操作作用于集群像作用于单机一样,具有广义的“原子性”.

(2)A:Availability,可用性.

主要指的就是我们使用者能够正常的使用我们系统,即使出现一些意外也能够正常使用我们的系统。

(3)P:Partition tolerance,分区容错性.

分区容错性强调的是,在网络环境不可靠的背景下,整个系统仍然是正常运作的,不至于出现系统崩溃或者秩序混乱的局面.也就是说即使出现一些网络分区(当然不能太严重,太严重属于硬件问题了),我们的系统依然能正常提供服务

CAP理论看起来三个都比较重要,但对于我们分布式系统中来说,很难满足所有的三个,更倾向于满足三者其二,也就是根据架构设计CP,AP,CA三者之一

单机系统显然无需考虑P,CA满足需要根据特定业务场景来看

对于分布式系统来说,P显然是必须的(否则对比单机系统不就没优势了)
所以主要有两种
1.CP:满足数据一致性,对可用性考虑较少
2.AP:满足可用性,对一致性要求不高

我们后面说到的主要是CP,对A也会有一定照顾

1.3 数据一致性

一般分为顺序一致性和即时一致性

  1. 顺序一致性:
    I 客户端依次向 master 发送了 set x = 3、set x = 4 的两笔请求;

II master 在本机依次完成了两笔写操作,于是状态机中记录的结果为 x = 4;

III 同时,master 异步开启将请求同步到 follower 的任务,任务发出的顺序也是 ① set x = 3 ② set x = 4;

IV 由于网络问题,第 ① 笔请求出了点状况,导致第 ② 笔请求后发先至,第 ① 笔请求随后而至;

V 于是 follower 先执行 ② set x = 4,后执行 ① set x = 3,最终 follower 状态机内的结果为 x = 3.

这个问题相比场景2就更严重了,因为 follower 中已经记录了错误的数据,接下来不论何时面对客户端的读请求都会返回这个错误的结果. 这种情况下,我们就称分布式系统的最终一致性也遭到了破坏.

  1. 即时一致性
    I 当前集群的服务端有 master 和 follower 两个节点.

II 客户端写请求打到了 master,写入 x = 3 这一项内容;

III 紧接着,客户端读请求打到了 follower,查询 x = 3 的值.

那么在第(3)步中所取得的 x 的值会是多少呢?答案是不确定的. 因为这取决于 master 和 follower 之间数据同步的机制.

倘若,为了满足更快响应客户端的诉求,服务端采用了异步完成数据同步任务的机制,那么客户端的读请求就可能在 follower 同步到 set x = 3 这一项任务之前就打到 follower,此时会取到 x 的老数据或者 x 不存在的响应,总之,读到的数据和客户端期待的结果产生了差距.

1.4.如何看待CAP理论

这个来说的话我们一般是认为木桶效应,就像我们在学数据结构一样,主要最差的情况是O(N),无论最好的情况有多块,都是O(N)的时间复杂度。

实际上,C 和 A 并非站在绝对意义的对立面,其间还是存在可以调和取舍的空间.

2 领导者选举

2.1 多数派原则

由于我们的领导者选举情况主要发生在系统初始化以及出现网络分区(可以理解为网络之间出现了隔阂,一个分区与另一个分区之间互不相通)的情况下,所以我们考虑的主要是如何在各种异常情况下正确的选举出领导者来服务。

多数派原则,顾名思义,就是超过一半的为多数派,在我们raft中指的就是,如果有超过一半的人投票某一节点,则该节点为领导者。

为什么多数派原则能避免网络分区造成的影响。我们可以考虑出现网络分区的情况,假设有两个分区,则这两个分区必有一个是占大多数节点的分区,那么根据多数派原则,只有这个分区能够选择出领导者,另一分区显然不行(总结点都不超过一半,怎么可能投出一半的选票)

多数派原则在后文的两阶段提交也会提到,这个原则可以说贯彻整个raft论文。

2.2 选举中的一些概念

领导者 leader :节点的三种角色之一. 集群的首脑,负责发起”提议“、”提交“被多数派认可的决断.

跟随者 follower :节点的三种角色之一. 需要对 leader 的 ”提议“ 、”提交“和 candidate 的 ”竞选“ 进行响应.

候选人 candidate :节点的三种角色之一. 是一种处于竞选流程中的临时状态,根据多数派投票的结果会切为 leader 或 follower 的稳定态.

任期 term :任期是用于标识 leader 更迭的概念. 每个任期内至多只允许有一个 leader.

日志 log :用于记录每一笔写操作,每一个节点都有一个日志数组,日志存放于日志数组中,
每一则日志除了记录内容外,还有两个重要的属性:
(1)term:标志了这则日志是哪个任期的 leader 在位时同步写入的;

(2)index:标志了这则日志在预写日志数组的位置.

通过这个唯一二元组,可以在全局中锁定这则日志位置,同时可以进行不同节点之间的日志判断,如果两者最后一则日志相同,则这两个节点一定日志是同步的。

2.3 选举流程

我们上文已经基本介绍完了选举中用到的概念和原则,开始来介绍一下流程。

什么时候follower会发起选举变成candidate?
follower长时间没有收到leader的心跳包时,follower会变成candidate,并向全部节点发送广播拉票。投票这个过程应该发送什么呢
term candidate 的竞选任期,如果上位了,就采用此任期
candidateID candidate 的节点 id,方便 follower 标记自己将票投给了谁
lastLogIndex candidate 最晚一笔预写日志的 index
lastLogTerm candidate 最晚一笔预写日志的 term。收到拉票的follower会更新自己的心跳时间,并向第一个向他发起拉票的节点投票,此时节点的投票状态改变为已投票,值得注意的是,每次发起一轮选举,term都会+1 。

如果candidate收到多数派选票,则进化成leader,如果收到了比他term大或者相同的leader的心跳包,则退回follower,如果收到了多数派的反对票,则会退化成follower,如果选举时间超时,则进行下一轮选举,任期+1 。

对于leader来说,肯定要定时发心跳包来证明自己存在的,心跳包具体内容是什么呢?上文我们提到日志的结构,日志肯定要发的,因为要同步其他节点数据,所以心跳包主要内容有三个,term,log index,以及本节点ID。还有需要注意的是,leader如果收到了比自己任期大的leader的心跳,则退回follower,如果收到比自己任期大的follower的心跳包,则也退回follower。

对于follower来说,倘若 candidate 的竞选任期小于自身,拒绝,并回复自己的最新任期;倘若自己已经将票投给了其他 candidate,拒绝;倘若自己已经将票投给了这个 candidate,接受;(candidate 侧会幂等去重)倘若 candidate 的 lastLogTerm 大于自己最后一笔预写日志的 term,接受;倘若 candidate 的 lastLogTerm 小于自己最后一笔预写日志的 term,拒绝;倘若 candidate 的 lastLogTerm 等于自己最后一笔预写日志的 term,且 candidate 的 lastLogIndex 大于等于自己最后一笔预写日志的 index,接受;倘若 candidate 的 lastLogTerm 等于自己最后一笔预写日志的 term,且 candidate 的 lastLogIndex 小于自己最后一笔预写日志的 index,拒绝.

2.4 选举分裂问题

在Raft中规定每个term只能投票一次,而前面的原leader网络分区后,follower1和follower2可能产生分裂选举(split vote)的问题:

由于巧合,follower1和follower2的election timer几乎同一时刻到期,它们都将term由10改成11
follower1和follower2在term11都vote自己,然后向对方发起获取投票的请求
follower1和follower2因为都在term11投票过自己了,所以都会拒绝对方的拉票请求,这导致双方都成为不了majority,没有人成为新leader
也许有个选举超时时间节点,follower1和follower又继续将term11增加到12,然后重复上诉流程。如果此时没有人为介入操作,上面重复的term增加、vote自己、没人成为leader,超时后又进行新一轮election的过程可能一直重复下去
为了避免陷入上面的选举死循环,通常election超时时间是随机的(election timeout is randomized)

​ 论文中提到,它们设置election timer时,选择150ms到300ms之间的随机值。每次这些follower重制自身的election timeout时,会选择150ms~300ms之间的一个随机数,只有当计时器到点时,才会发起选举eleciton。这样上诉流程中总有一刻follower1或者follower2因为timer领先到点,最终成功vote自己且拉票让对方vote自己,达到majority条件,优胜成为leader。

2.5 选举超时时间

主要遵循三个选择
略大于心跳时间(>= few heartbeats)

如果选举超时比心跳还短,那么系统将频繁发起选举,而选举期间系统对外呈现的是阻塞请求,不能正常响应client。因为election时很可能丢失同步的log,一直频繁地更新term,不接受旧leader的log(旧leader的term低于新term,同步log消息会被拒绝)

加入一些随机数(random value)

加入适当范围的随机数,能够避免无限循环下去的分裂选举(split vote)问题。random value越大,越能够减少进行split vote的次数,但random value越大,也意味着对于client来说,整个系统停止提供对外服务的时间越长(对外表现和宕机差不多,反正选举期间无法正常响应client的请求)

尽量短(short enough that down time is short)

因为选举期间,对外client表现上如同宕机一般,无法正常响应请求,所以我们希望eleciton timeout能够尽量短

3.外部请求流程

首先我们先搞清楚log的用途,log结构在上文中已经提到过。
重传(retranmission):leader向follower同步消息时,消息可能传递失败,所以需要log记录,方便重传
顺序(order):主要原因,我们需要被同步的操作,以相同的顺序出现在所有的replica上
持久化(persistence):持久化log数据,才能支持失败重传,重启后日志恢复等机制
试探性操作(space tentative):比如follower接收到来自leader的log后,并不知道哪些操作被commit了,可能等待一会直到明确操作被commit了才进行后续操作。我们需要一些空间来做这些试探性操作(tentative operations),而log很合适。
还要理解raft论文的原则就是一主多从,读写分离(这个后面会有一些问题)

3.1 读

读的入口可以是任意节点,但是在raft论文中,读只能由leader完成,这样可以保证数据一致性,但同时存在一个问题就是其他节点没有了除了备份之外的作用,但是实际上基于 raft 的 etcd 实现了 Linearizable Read 来解决这个问题,读请求到了 follower 后,follower会去向 leader 请求 readindex(也就是当时 leader 的 commitindex), leader 在确认自己还是 leader 之后,就会吧 readindex 发给 follower,follower 会对比自己的 commitindex 和 readindex,只有commitindex 大于等于 readindex 之后,才能读取数据返回。还有很多如果只在乎最终一致性,则会任意follower均可读取自己的状态机。

来说一下读的流程:
( 1 )appliedIndex 校验:每次 leader 处理写请求后,会把最晚一笔应用到状态机的日志索引 appliedIndex 反馈给客户端. 后续客户端和 follower 交互时,会携带 appliedIndex. 倘若 follower 发现自身的 appliedIndex 落后于客户端的 appliedIndex,说明本机存在数据滞后,则拒绝这笔请求,由客户端发送到其他节点进行处理.
( 2 )强制读主:follower 收到读请求时,也统一转发给 leader 处理. 只要 leader 处理写请求时,保证先写状态机,后给客户端响应,那么状态机的数据可以保证即时一致性. 但是这样的弊端就是 leader 的压力过大,其他 follower 节点只沦为备份数据副本的配角。那可能就有一个问题就是(1)的作用在哪?是为了防止读到旧follower的旧leader。
( 3 )leader 提供读服务时,需要额外向集群所有节点发起一轮广播,当得到多数派的认可,证明自己身份仍然合法时,才会对读请求进行响应.

3.2 写

写只能由leader完成

(1)写操作需要由 leader 统一收口. 倘若 follower 接收到了写请求,则会告知客户端 leader 的所在位置(节点 id),让客户端重新将写请求发送给 leader 处理;

(2)leader 接收到写请求后,会先将请求抽象成一笔预写日志,追加到预写日志数组的末尾;

(3)leader 会广播向集群中所有节点发送同步这笔日志的请求,称之为第一阶段的“提议”;

(4)follower 将日志同步到本机的预写日志数组后,会给 leader 回复一个“同步成功”的ack;

(5)leader 发现这笔请求对应的预写日志已经被集群中的多数派(包括自身)完成同步时,会”提交“该日志,并向客户端回复“写请求成功”的 ack。这就是两阶段提交。
异常情况:
case 1:leader 任期滞后.

在第(4)步中,倘若 follower 发现当前 leader 的 term 小于自己记录的最新任期,本着”前朝的剑不斩本朝官“的原则,follower 会拒绝 leader 的这次同步请求,并在响应中告知 leader 当前最新的 term;leader 感知到新 term 的存在后,也会识相地主动完成退位.

case 2:follower 日志滞后.

同样在第(4)步中,此时虽然 leader 的 term 是最新的,但是在这笔最新同步日志之前, follower 的预写日志数据还存在缺失的数据,此时 follower 会拒绝 leader 的同步请求;leader 发现 follower 响应的任期与自身相同却又拒绝同步,会递归尝试向 follower 同步预写日志数组中的前一笔日志,直到补齐 follower 缺失的全部日志后,流程则会回归到正常的轨道.
case 3:follower日志超前
如果follower的日志索引高于leader,则会去掉这一部分的日志,保证和leader的日志记录相同

4 日志的一些操作

4.1 日志同步请求

这一步是通过我们leader的心跳包完成的,也就是我们上文提到过的,心跳包应该存在三个内容,term,leader ID,和日志内容,而日志内容应该包括
leaderCommit leader 最新提交的日志 index,方便 follower 更新数据进度
prevLogIndex 当前同步日志的前一条日志的 index.
prevLogTerm 当前同步日志的前一条日志的 term.
log[] 同步的日志,可能为多笔,因为 follower 可能滞后了多笔日志

follower收到心跳包后:
I:倘若 leader 任期落后于自己,拒绝请求,并回复自己所在的任期;

II:倘若 follower 存在不一致的日志,则删除多余的日志,同步 leader 日志与之保持一致;

III:倘若 follower 存在日志滞后,则拒绝请求,让 leader 重发更早的日志,直到补齐所有缺失.

如果是leader或者candidate收到心跳包,一般会进行任期校验。

leader收到倘若多数派都完成了日志同步,leader 会提交这笔日志;

倘若某个节点拒绝了同步请求,并回复了一个更新的任期,leader 会退位回 follower,并更新任期;

倘若某个节点拒绝了同步请求,但回复了相同的任期,leader 会递归发送前一条日志给该节点,直到其接受同步请求为止;

倘若一个节点超时未给 leader 回复,leader 会重发这笔同步日志的请求。

如果收到日志索引问题,4.2会介绍

4.2 日志覆写同步

nextIndex:所有raft节点都维护nextIndex乐观的变量用于记录下一个需要填充log entry的log index。这里说乐观,因为当leader当选时,leader会初始化nextIndex值为当前log index值+1,表示认为leader自身的log一定是最新的
matchIndex:leader为所有raft节点(包括leader自己)维护一个悲观的matchIndex用于记录leader和其他follower从0开始往后最长能匹配上的log index的位置+1,表示leader和某个follower在matchIndex之前的所有log entry都是对齐的。这里说悲观,因为leader当选时,leader会初始化matchIndex值为0,表示认为自身log中没有一条记录和其他follower能匹配上。随着leader和其他follower同步消息时,matchIndex会慢慢增加。leader为每个自己的follower维护matchIndex,因为平时根据majority规则,需要保证log已经同步到足够多的followers上。

在未优化的版本中,如何处理索引不同步主要是通过leader不断返回上一笔日志索引,并把这个日志内容放到日志数组中,知道follower返回成功,这样做的话需要从前向后遍历日志索引,有点浪费网络环境。

优化之后,返回内容会加一条日志索引,leader直接返回缺的id[]就可以了。

4.3 持久化操作

持久化currentTerm的原因要更微妙一些,但是实际上还是为了实现一个任期内最多只有一个Leader,我们之前实际上介绍过这里的内容。如果(重启之后)我们不知道任期号是什么,很难确保一个任期内只有一个Leader。

在这里例子中,S1关机了,S2和S3会尝试选举一个新的Leader。它们需要证据证明,正确的任期号是8,而不是6。如果仅仅是S2和S3为彼此投票,它们不知道当前的任期号,它们只能查看自己的Log,它们或许会认为下一个任期是6(因为Log里的上一个任期是5)。如果它们这么做了,那么它们会从任期6开始添加Log。但是接下来,就会有问题了,因为我们有了两个不同的任期6(另一个在S1中)。这就是为什么currentTerm需要被持久化存储的原因,因为它需要用来保存已经被使用过的任期号。

raft的重启策略主要是快速重启(start from your persistence state),从上一次存储的持久化状态(快照)的位置开始工作,后续可以通过log catch up的机制,赶上leader的log状态。
这也就说明我们需要持久化的主要有三个:
1.vote for :防止重复投票
2.日志状态 :崩溃前的log记录,因为我们需要保证(promise)已发生的(commit)不会被回退。否则崩溃重启后,可能发生一些奇怪的事情,比如client先前的请求又重新生效一次,导致某个K/V被覆盖成旧值之类的。
3.任期:投票用

每当上面提到的需要持久化的变量state发生变化时,都应该进行持久化,写入稳定存储(磁盘),即使这可能是很昂贵的操作。你必须保证在回复client或者leader的请求之前,先将需要持久化的数据写入稳定存储,然后再回复。否则如果先回复,但是持久化之前崩溃了,你相当于丢失了一些无法找回的记录。

4.4服务恢复

周期性快照(periodic snapshots):假设在i的位置创建了快照,那么可以裁剪log,只保留i往后的log。此时重启后可以通过snapshot快照先快速恢复到某个时刻的状态,然后后续可以再通过log catch up或其他手段,将log同步到最新状态。(一般来说周期性的快照不会落后最新版本太多,所以恢复工作要少得多)
这里可能一定程度上破坏了抽象的原则,需要上层应用告诉raft i在哪。

5 集群变更

raft算法有一个原则是,集群变更时,在变更完成前,多数派永远实在原集群的基础上来说的。
主要流程就是:
(1)leader发起变更请求
(2)follower回复
(3)如果收到多数派选票,则同一变更
(4)同步日志

如果在变更期间收到写请求,多数派依然是原集群

6 网络分区中无意义的选举

在产生网络分区之后,小分区内也会进行选举,显然是无意义的。因此,我们可以通过这样的措施来避免这类无意义的选举:

每个 candidate 发起真实选举之前,会有一个提前试探的过程,试探机制是向集群所有节点发送请求,只有得到多数派的响应,证明自己不存在网络环境问题时,才会将竞选任期自增,并且发起真实的选举流程.

相信看完上述文章,你对raft论文应该会有一个基本的理解,建议去看一下MIT6.824的课程,如果有时间的话最好完成一下前三个lab,对代码能力有很大的提升。

本文参考公众号:小徐先生的编程世界

  • 28
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值