浅谈Paxos和Raft算法

Paxos算法

属于共识算法,共识算法(Consensus Algorithm)就是用来做这个事情的,它保证即使在小部分(≤ (N-1)/2)节点故障的情况下,系统仍然能正常对外提供服务。共识算法通常基于状态复制机(Replicated State Machine)模型,也就是所有节点从同一个 state 出发,经过同样的操作 log,最终达到一致的 state。
共识算法是构建强一致性分布式系统的基石,Paxos 是共识算法的代表,而 Raft 则是其作者在博士期间研究 Paxos 时提出的一个变种,主要优点是容易理解、易于实现。

角色

Paxos将系统中的角色分为提议者 (Proposer),决策者 (Acceptor),和最终决策学习者 (Learner):

  • Proposer: 提出提案 (Proposal)。Proposal信息包括提案编号 (Proposal ID) 和提议的值 (Value)。
  • Acceptor: 参与决策,回应Proposers的提案。收到Proposal后可以接受提案,若Proposal获得多数Acceptors的接受,则称该Proposal被批准。
  • Learner: 不参与决策,从Proposers/Acceptors学习最新达成一致的提案(Value)。

在多副本状态机中,每个副本同时具有Proposer、Acceptor、Learner三种角色。
在这里插入图片描述

步骤在这里插入图片描述

在这里插入图片描述

第一阶段: Prepare阶段

Proposer向Acceptors发出Prepare请求,Acceptors针对收到的Prepare请求进行Promise承诺。

  • Prepare: Proposer生成全局唯一且递增的Proposal ID (可使用时间戳加Server ID),向所有Acceptors发送Prepare请求,这里无需携带提案内容,只携带Proposal ID即可。
  • Promise: Acceptors收到Prepare请求后,做出“两个承诺,一个应答”。
    • 承诺1: 不再接受Proposal ID小于等于(注意: 这里是<= )当前请求的Prepare请求;
    • 承诺2: 不再接受Proposal ID小于(注意: 这里是< )当前请求的Propose请求;
    • 应答: 不违背以前作出的承诺下,回复已经Accept过的提案中Proposal ID最大的那个提案的Value和Proposal ID,没有则返回空值。
第二阶段: Accept阶段

Proposer收到多数Acceptors承诺的Promise后,向Acceptors发出Propose请求,Acceptors针对收到的Propose请求进行Accept处理。

  • Propose: Proposer 收到多数Acceptors的Promise应答后,从应答中选择Proposal ID最大的提案的Value,作为本次要发起的提案。如果所有应答的提案Value均为空值,则可以自己随意决定提案Value。然后携带当前Proposal ID,向所有Acceptors发送Propose请求。
  • Accept: Acceptor收到Propose请求后,在不违背自己之前作出的承诺下,接受并持久化当前Proposal ID和提案Value。
第三阶段: Learn阶段

Proposer在收到多数Acceptors的Accept之后,标志着本次Accept成功,决议形成,将形成的决议发送给所有Learners。

缺点

活锁

当某—proposer提交的proposal被拒绝时,可能是因为acceptor 承诺返回了更大编号的proposal,因此proposer提高编号继续提交。如果2个proposer都发现自己的编号过低转而提出更高编号的proposal,会导致死循环,这种
情况也称为活锁。
解决方案:二进制指数退避算法
在这里插入图片描述

效率低

每个完整流程的提议都需要经过两大轮请求返回(Multi-paxos先选出Leader后续每次仅需一轮)

实现困难

这也是当前识算法普遍存在的问题

Multi-Paxos算法

是一种思想。
选取一个leader(可以通过一次basic paxos),对于读请求直接由leader返回,写请求先发给所有接受者,ack后再返回成功。
在这里插入图片描述

Raft算法

动画模拟:链接
consul、etcd、TIKV等都在使用raft算法。
在raft算法中,各节点是相等的,所以为了快速决策就需要选主,leader会负责接收客户端发过来的操作请求,将操作包装为日志同步给其它节点,在保证大部分节点都同步了本次操作后,就可以安全地给客户端回应响应了。在选主和日志操作(不删除、覆盖旧日志)要谨慎处理,即要注重安全性。
Raft 核心算法其实就是由三个子问题组成的:选主(Leader election)、日志复制(Log replication)、安全性(Safety)。这三部分共同实现了 Raft 核心的共识和容错机制。
在这里插入图片描述

选主

角色
  • Leader: 所有请求的处理者,接收客户端发起的操作请求,写入本地日志后同步至集群其它节点。
  • Follower: 请求的被动更新者,从 leader 接收更新请求,写入本地文件。如果客户端的操作请求发送给了 follower,会首先由 follower 重定向给 leader。
  • Candidate: 如果 follower 在一定时间内没有收到 leader 的心跳,则判断 leader 可能已经故障,此时启动 leader election 过程,本节点切换为 candidate 直到选主结束。
任期

每开始一次新的选举,称为一个任期term),每个 term 都有一个严格递增的整数与之关联。
每当 candidate 触发 leader election 时都会增加 term,如果一个 candidate 赢得选举,他将在本 term 中担任 leader 的角色。但并不是每个 term 都一定对应一个 leader,有时候某个 term 内会由于选举超时导致选不出 leader,这时 candicate 会递增 term 号并开始新一轮选举。
跟随者在等待领导者心跳信息超时后,推举自己为候选人时,会增加自己的任期号,比如节点 A 的当前任期编号为 0,那么在推举自己为候选人时,会将自己的任期编号增加为 1。

  1. 如果一个服务器节点,发现自己的任期编号比其他节点小,那么它会更新自己的编号到较大的编号值。比如节点 B 的任期编号是 0,当收到来自节点 A 的请求投票 RPC 消息时,因为消息中包含了节点 A 的任期编号,且编号为 1,那么节点 B 将把自己的任期编号更新为 1。
  2. 在 Raft 算法中约定,如果一个候选人或者领导者,发现自己的任期编号比其他节点小,那么它会立即恢复成跟随者状态。比如分区错误恢复后,任期编号为 3 的领导者节点B,收到来自新领导者的,包含任期编号为 4 的心跳消息,那么节点 B 将立即恢复成跟随者状态。
  3. 还约定如果一个节点接收到一个包含较小的任期编号值的请求,那么它会直接拒绝这个请求。比如节点 C 的任期编号为 4,收到包含任期编号为 3 的请求投票 RPC 消息,那么它将拒绝这个消息。

Term 更像是一个逻辑时钟logic clock)的作用,有了它,就可以发现哪些节点的状态已经过期。每一个节点都保存一个 current term,在通信时带上这个 term 号。
节点间通过 RPC 来通信,主要有两类 RPC 请求:

  • RequestVote RPCs: 用于 candidate 拉票选举。
  • AppendEntries RPCs: 用于 leader 向其它节点复制日志以及同步心跳。
流程

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

  1. 首先,刚开始所有结点都是follower的状态,为了避免同时去竞争选票,raft给每个结点分配随机超时时间,这样会有一个率先称为candidate然后去拉选票。
  2. 如果收到了至少(N/2+1)个投票就将自己变成leader。如果此时收到了别的结点的心跳包,会判断别的结点的任期和此结点的任期,如果<别的结点的就将自己变成follower。如果同时有别的结点参与争抢,那么可能这一轮票数平局,都无法满足,且这个任期就跳过。因为每个结点都是随机超时,所以下一轮很大概率能选出主。

Raft 通过比较两个节点最后一条日志条目的索引值任期号来定义谁的日志比较新。

  1. 首先比较任期号,谁都任期号大,谁就新。
  2. 如果任期号相同,那谁的日志长,谁就新。

通过这个规则来选举出日志最新的节点作为 leader。

日志复制

共识算法通常基于状态复制机Replicated State Machine)模型,所有节点从同一个 state 出发,经过一系列同样操作 log 的步骤,最终也必将达到一致的 state。也就是说,只要我们保证集群中所有节点的 log 一致,那么经过一系列应用(apply)后最终得到的状态机也就是一致的。
Raft 负责保证集群中所有节点 log 的一致性

流程
  • Leader 为客户端提供服务,客户端的每个请求都包含一条即将被状态复制机执行的指令。
  • Leader 把该指令作为一条新的日志附加到自身的日志集合,然后向其它节点发起附加条目请求AppendEntries RPC),来要求它们将这条日志附加到各自本地的日志集合。
  • 当这条日志已经确保被安全的复制,即大多数(N/2+1)节点都已经复制后,leader 会将该日志 apply 到它本地的状态机中,然后把操作成功的结果返回给客户端。

tips:这里的(N/2+1)个节点包含leader自身为一个节点。

日志一致性保证

使用(term,index)的方式发送消息
Raft 保证:如果不同的节点日志集合中的两个日志条目拥有相同的 term 和 index,那么它们一定存储了相同的指令。
为什么可以作出这种保证?因为 Raft 要求 leader 在一个 term 内针对同一个 index 只能创建一条日志,并且永远不会修改它。
同时 Raft 也保证:如果不同的节点日志集合中的两个日志条目拥有相同的 term 和 index,那么它们之前的所有日志条目也全部相同。
这是因为 leader 发出的 AppendEntries RPC 中会额外携带上一条日志的 (term, index),如果 follower 在本地找不到相同的 (term, index) 日志,则拒绝接收这次新的日志

可能出现的不一致场景

在这里插入图片描述

场景a~b. Follower 日志落后于 leader
这种场景其实很简单,即 follower 宕机了一段时间,follower-a 从收到 (term6, index9) 后开始宕机,follower-b 从收到 (term4, index4) 后开始宕机。这里不再赘述。
场景c. Follower 日志比 leader 多 term6
当 term6 的 leader 正在将 (term6, index11) 向 follower 同步时,该 leader 发生了宕机,且此时只有 follower-c 收到了这条日志的 AppendEntries RPC。然后经过一系列的选举,term7 可能是选举超时,也可能是 leader 刚上任就宕机了,最终 term8 的 leader 上任了,成就了我们看到的场景 c。
场景d. Follower 日志比 leader 多 term7
当 term6 的 leader 将 (term6, index10) 成功 commit 后,发生了宕机。此时 term7 的 leader 走马上任,连续同步了两条日志给 follower,然而还没来得及 commit 就宕机了,随后集群选出了 term8 的 leader。
场景e. Follower 日志比 leader 少 term5 ~ 6,多 term4
当 term4 的 leader 将 (term4, index7) 同步给 follower,且将 (term4, index5) 及之前的日志成功 commit 后,发生了宕机,紧接着 follower-e 也发生了宕机。这样在 term5~7 内发生的日志同步全都被 follower-e 错过了。当 follower-e 恢复后,term8 的 leader 也刚好上任了。
场景f. Follower 日志比 leader 少 term4 ~ 6,多 term2 ~ 3
当 term2 的 leader 同步了一些日志(index4 ~ 6)给 follower 后,尚未来得及 commit 时发生了宕机,但它很快恢复过来了,又被选为了 term3 的 leader,它继续同步了一些日志(index7~11)给 follower,但同样未来得及 commit 就又发生了宕机,紧接着 follower-f 也发生了宕机,当 follower-f 醒来时,集群已经前进到 term8 了。

如何解决日志不一致

Raft 强制要求 follower 必须复制 leader 的日志集合来解决不一致问题。
follower的日志一定是未提交的,就不会被客户端感知到,所以不会有重大影响。要和leader保持一致,就要先找到最后一次一致的地方,然后根据检查机制那么前面一定相同。
Leader 针对每个 follower 都维护一个 next index,表示下一条需要发送给该follower 的日志索引。第一次同步时next index=leader的index+1,如果有follower不一致,那么就会减小next index然后去和follower匹配,直到找到为止。follower 会删除掉现存的不一致的日志,保留 leader 最新同步过来的。
leader 从来不会覆盖或者删除自己的日志,而是强制 follower 与它保持一致。

安全性及正确性

存在一种情况,每个leader同步时却宕机了,然后下一个同步也宕机…造成日志混乱,所以需要一些安全措施保证状态机的安全性。

对选举的措施

每个 candidate 必须在 RequestVote RPC 中携带自己本地日志的最新 (term, index),如果 follower 发现这个 candidate 的日志还没有自己的新,则拒绝投票给该 candidate。
Candidate 想要赢得选举成为 leader,必须得到集群大多数节点的投票,那么它的日志就一定至少不落后于大多数节点。又因为一条日志只有复制到了大多数节点才能被 commit,因此能赢得选举的 candidate 一定拥有所有 committed 日志
因此前一篇文章我们才会断定地说:Follower 不可能比 leader 多出一些 committed 日志。

对提交的限制

当 leader 得知某条日志被集群过半的节点复制成功时,就可以进行 commit,committed 日志一定最终会被状态机 apply。
所谓 commit 其实就是对日志简单进行一个标记,表明其可以被 apply 到状态机,并针对相应的客户端请求进行响应。
然而 leader 并不能在任何时候都随意 commit 旧任期留下的日志,即使它已经被复制到了大多数节点。因为这种提交可能会造成日志冲突,覆盖等。
添加限制:Leader 只允许 commit 包含当前 term 的日志。

关键参数

1. 跟随者和候选人崩溃

相比较 leader 崩溃,这个要简单的多,当 follower 崩溃,leader 只需要不断重试即可,当 follower 重启,RPC 重试会成功,注意:RPC 重试是幂等的,因此重试不会造成任何问题。

2. 时间和可用性

在 Raft 中,关于时间,有 3 种类型:

  1. 广播时间(broadcastTime):指一个节点并行的发送 RPC 给其他节点并收到响应的平均时间。
  2. 选举超时时间(electionTimeout):如果一个跟随者在一段时间里没有接收到任何消息,也就是选举超时。
  3. 平均故障间隔时间(MTBF):平均故障间隔时间就是对于一台服务器而言,两次故障之间的平均时间。

Raft 规定,只要系统满足下面的时间要求,就可以选举并维持一个稳定的领导人。

广播时间(broadcastTime) << 选举超时时间(electionTimeout) << 平均故障间隔时间(MTBF)

通常广播时间比选举超时时间小一个数量级,这样 leader 才能发送稳定的心跳来阻止跟随者进入选举状态;
选举超时时间一般比 MTBF 小几个数量级,这样整个系统才能正常运行。当 leader 崩溃后,整个系统的不可用时间,大约相当于选举时间超时时间。
广播时间和 MTBF 是由系统决定的,只有选举超时时间是我们自己选择的,通常广播时间大约是 0.5 毫秒到 20 毫秒(持久化需要时间),那么,选举超时时间通常在 10 毫秒 到 500 毫秒之间。
而 MTBF 时间,通常平均故障时间在几个月甚至更长,君不见美团的服务器可用性达到 7 个九 :)因此很容易满足时间的需求。

集群成员变更与日志压缩

集群成员变更

如果直接增加节点可能会导致脑裂问题
在这里插入图片描述

两阶段切换集群成员配置
在这里插入图片描述

阶段一

  1. 客户端将 C-new 发送给 leader,leader 将 C-old 与 C-new 取并集并立即 apply,我们表示为 C-old,new
  2. Leader 将 C-old,new 包装为日志同步给其它节点。
  3. Follower 收到 C-old,new 后立即 apply,当 C-old,new 的大多数节点(即 C-old 的大多数节点和 C-new 的大多数节点)都切换后,leader 将该日志 commit。

阶段二

  1. Leader 接着将 C-new 包装为日志同步给其它节点。
  2. Follower 收到 C-new 后立即 apply,如果此时发现自己不在 C-new 列表,则主动退出集群。
  3. Leader 确认 C-new 的大多数节点都切换成功后,给客户端发送执行成功的响应。

为什么该方案可以保证不会出现多个 leader?我们来按流程逐阶段分析。
阶段1. C-old,new 尚未 commit
该阶段所有节点的配置要么是 C-old,要么是 C-old,new,但无论是二者哪种,只要原 leader 发生宕机,新 leader 都必须得到大多数 C-old 集合内节点的投票

在这里插入图片描述

以上图场景为例,S5 在阶段d根本没有机会成为 leader,因为 C-old 中只有 S3 给它投票了,不满足大多数。

阶段2. C-old,new 已经 commit,C-new 尚未下发
该阶段 C-old,new 已经 commit,可以确保已经被 C-old,new 的大多数节点(再次强调:C-old 的大多数节点和 C-new 的大多数节点)复制。
因此当 leader 宕机时,新选出的 leader 一定是已经拥有 C-old,new 的节点,不可能出现两个 leader。

阶段3. C-new 已经下发但尚未 commit
该阶段集群中可能有三种节点 C-old、C-old,new、C-new,但由于已经经历了阶段2,因此 C-old 节点不可能再成为 leader。而无论是 C-old,new 还是 C-new 节点发起选举,都需要经过大多数 C-new 节点的同意,因此也不可能出现两个 leader。

阶段4. C-new 已经 commit
该阶段 C-new 已经被 commit,因此只有 C-new 节点可以得到大多数选票成为 leader。此时集群已经安全地完成了这轮变更,可以继续开启下一轮变更了。
在这里插入图片描述

以上便是对该两阶段方法可行性的分步验证,Raft 论文将该方法称之为共同一致(Joint Consensus)
raft论文提出的三个问题

  1. 新的服务器在初始化时,没有存储任何日志条目。
    这个带来的问题是:由于没有存储任何日志条目,那么,就需要时间来补充日志条目,这实际上,是会影响可用性的。
    Raft 使用了一种方法来避免:这个阶段的服务器是没有投票权力的。只有当机器补充完日志条目了,才会加入集群。

  2. 集群的领导人可能不是新配置的一员
    什么意思?
    答: 当旧集群的 leader 提交了新配置,Leader 需要变成 follower。
    为什么?
    答:当新的配置生效时,旧 leader 还使用的老的配置,例如新配置的服务器数量是 5 台,而老的 leader 仍然应用的是 3 台服务器。
    新集群应该使用新的配置重新进行选举(不然使用新配置干嘛?)。

  3. 移除不在新配置中的服务器可能会扰乱集群
    当移除他们时,他们会重新进行选举,导致当前 leader 回退到 follower 状态。虽然新的 leader 会被选出来,但被移除的服务器会再次请求重新选举,循环反复,影响可用性。
    Raft 的解决方式:

  4. 当节点确认当前领导人存在时,则忽略请求投票的 RPC 请求。

  5. 当节点在最小选举超时时间里收到请求投票请求,他不会更新当前的任期号或者投出选票(从而扰乱集群)。

日志压缩

Raft 核心算法维护了日志的一致性,通过 apply 日志我们也就得到了一致的状态机,客户端的操作命令会被包装成日志交给 Raft 处理。然而在实际系统中,客户端操作是连绵不断的,但日志却不能无限增长,首先它会占用很高的存储空间,其次每次系统重启时都需要完整回放一遍所有日志才能得到最新的状态机。因此raft提供了日志压缩功能。
**快照(Snapshot)**是一种常用的、简单的日志压缩方式,ZooKeeper、Chubby 等系统都在用。简单来说,就是将某一时刻系统的状态 dump 下来并落地存储,这样该时刻之前的所有日志就都可以丢弃了。只能为 committed 日志做 snapshot。
在这里插入图片描述

上图展示了一个节点用快照替换了 (term1, index1) ~ (term3, index5) 的日志。
快照一般包含以下内容:

  • 日志的元数据:最后一条被该快照 apply 的日志 term 及 index
  • 状态机:前边全部日志 apply 后最终得到的状态机

当 leader 需要给某个 follower 同步一些旧日志,但这些日志已经被 leader 做了快照并删除掉了时,leader 就需要把该快照发送给 follower。
同样,当集群中有新节点加入,或者某个节点宕机太久落后了太多日志时,leader 也可以直接发送快照,大量节约日志传输和回放时间。
同步快照使用一个新的 RPC 方法,叫做 InstallSnapshot RPC

线性一致性与读性能优化

什么是线性一致性?

在分布式系统中,为了消除单点提高系统可用性,通常会使用副本来进行容错,但这会带来另一个问题,即如何保证多个副本之间的一致性。而强一致性就是线性一致性。就是CAP理论的C。

所谓的强一致性(线性一致性)并不是指集群中所有节点在任一时刻的状态必须完全一致,而是指一个目标,即让一个分布式系统看起来只有一个数据副本,并且读写操作都是原子的,这样应用层就可以忽略系统底层多个数据副本间的同步问题。也就是说,我们可以将一个强一致性分布式系统当成一个整体,一旦某个客户端成功的执行了写操作,那么所有客户端都一定能读出刚刚写入的值。即使发生网络分区故障,或者少部分节点发生异常,整个集群依然能够像单机一样提供服务。

写主读从缺陷分析

写操作并不是我们关注的重点,所有写操作都要作为提案从 leader 节点发起,当然所有的写命令都应该简单交给 leader 处理。真正关键的点在于读操作的处理方式,这涉及到整个系统关于一致性方面的取舍
在该方案中我们假设读操作直接简单地向 follower 发起,那么由于 Raft 的 Quorum 机制(大部分节点成功即可),针对某个提案在某一时间段内,集群可能会有以下两种状态:

  • 某次写操作的日志尚未被复制到一少部分 follower,但 leader 已经将其 commit。
  • 某次写操作的日志已经被同步到所有 follower,但 leader 将其 commit 后,心跳包尚未通知到一部分 follower。

以上每个场景客户端都可能读到过时的数据,整个系统显然是不满足线性一致的。

写主读主缺陷分析

问题一:状态机落后于 committed log 导致脏读
所谓 commit 其实就是对日志简单进行一个标记,表明其可以被 apply 到状态机,并针对相应的客户端请求进行响应。
也就是说一个提案只要被 leader commit 就可以响应客户端了,Raft 并没有限定提案结果在返回给客户端前必须先应用到状态机。所以从客户端视角当我们的某个写操作执行成功后,下一次读操作可能还是会读到旧值。
这个问题的解决方式很简单,在 leader 收到读命令时我们只需记录下当前的 commit index,当 apply index 追上该 commit index 时,即可将状态机中的内容响应给客户端。

问题二:网络分区导致脏读
假设集群发生网络分区,旧 leader 位于少数派分区中,而且此刻旧 leader 刚好还未发现自己已经失去了领导权,当多数派分区选出了新的 leader 并开始进行后续写操作时,连接到旧 leader 的客户端可能就会读到旧值了。
因此,仅仅是直接读 leader 状态机的话,系统仍然不满足线性一致性。

Raft Log Read
为了确保 leader 处理读操作时仍拥有领导权,我们可以将读请求同样作为一个提案走一遍 Raft 流程,当这次读请求对应的日志可以被应用到状态机时,leader 就可以读状态机并返回给用户了。
这种读方案称为 Raft Log Read,也可以直观叫做 Read as Proposal
为什么这种方案满足线性一致?因为该方案根据 commit index 对所有读写请求都一起做了线性化,这样每个读请求都能感知到状态机在执行完前一写请求后的最新状态,将读写日志一条一条的应用到状态机,整个系统当然满足线性一致。但该方案的缺点也非常明显,那就是性能差,读操作的开销与写操作几乎完全一致。而且由于所有操作都线性化了,我们无法并发读状态机。Raft Log 存储、复制带来刷盘开销、存储开销、网络开销,走 Raft Log不仅仅有日志落盘的开销,还有日志复制的网络开销,另外还有一堆的 Raft “读日志” 造成的磁盘占用开销,导致 Read 操作性能是非常低效的,所以在读操作很多的场景下对性能影响很大,在读比重很大的系统中是无法被接受的,通常都不会使用。
在 Raft 里面,节点有三个状态:Leader,Candidate 和 Follower,任何 Raft 的写入操作都必须经过 Leader,只有 Leader 将对应的 Raft Log 复制到 Majority 的节点上面认为此次写入是成功的。所以如果当前 Leader 能确定一定是 Leader,那么能够直接在此 Leader 上面读取数据,因为对于 Leader 来说,如果确认一个 Log 已经提交到大多数节点,在 t1 的时候 apply 写入到状态机,那么在 t1 后的 Read 就一定能读取到这个新写入的数据。
也就是说,这样相比Raft Log read来说,少了一个Log复制的过程,取而代之的是只要确认自己的leader身份就可以直接从leader上面直接读取数据,从而保证数据一定是准确的。
那么如何确认 Leader 在处理这次 Read 的时候一定是 Leader 呢?在 Raft 论文里面,提到两种方法:

  • ReadIndex Read
  • Lease Read
Raft 读性能优化
ReadIndex Read

第一种是 ReadIndex Read,当 Leader 需要处理 Read 请求时,Leader 与过半机器交换心跳信息确定自己仍然是 Leader 后可提供线性一致读:

  1. Leader 将自己当前 Log 的 commitIndex 记录到一个 Local 变量 ReadIndex 里面;
  2. 接着向 Followers 节点发起一轮 Heartbeat,如果半数以上节点返回对应的 Heartbeat Response,那么 Leader就能够确定现在自己仍然是 Leader;
  3. Leader 等待自己的 StateMachine 状态机执行,至少应用到 ReadIndex 记录的 Log,直到 applyIndex 超过 ReadIndex,这样就能够安全提供 Linearizable Read,也不必管读的时刻是否 Leader 已飘走;
  4. Leader 执行 Read 请求,将结果返回给 Client。

使用 ReadIndex Read 提供 Follower Read 的功能,很容易在 Followers 节点上面提供线性一致读,Follower 收到 Read 请求之后:

  1. Follower 节点向 Leader 请求最新的 ReadIndex;
  2. Leader 仍然走一遍之前的流程,执行上面前 3 步的过程(确定自己真的是 Leader),并且返回 ReadIndex 给 Follower;
  3. Follower 等待当前的状态机的 applyIndex 超过 ReadIndex;
  4. Follower 执行 Read 请求,将结果返回给 Client。

ReadIndex Read 使用 Heartbeat 方式代替了日志复制,省去 Raft Log 流程。相比较于走 Raft Log 方式,ReadIndex Read 省去磁盘的开销,能够大幅度提升吞吐量。虽然仍然会有网络开销,但是 Heartbeat 本来就很小,所以性能还是非常好的。

Lease Read

虽然 ReadIndex Read 比原来的 Raft Log Read 快很多,但毕竟还是存在 Heartbeat 网络开销,所以考虑做更进一步的优化。
Raft 论文里面提及一种通过 Clock + Heartbeat 的 Lease Read 优化方法,也就是 Leader 发送 Heartbeat 的时候首先记录一个时间点 Start,当系统大部分节点都回复 Heartbeat Response,由于 Raft 的选举机制,Follower 会在 Election Timeout 的时间之后才重新发生选举,下一个 Leader 选举出来的时间保证大于 Start+Election Timeout/Clock Drift Bound,所以可以认为 Leader 的 Lease 有效期可以到 Start+Election Timeout/Clock Drift Bound 时间点。Lease Read 与 ReadIndex 类似但更进一步优化,不仅节省 Log,而且省掉网络交互,大幅提升读的吞吐量并且能够显著降低延时。
Lease Read 基本思路是 Leader 取一个比 Election Timeout(选举超时) 小的租期(最好小一个数量级),在租约期内不会发生选举,确保 Leader 不会变化,所以跳过 ReadIndex 的发送Heartbeat的步骤,也就降低了延时。
由此可见 Lease Read 的正确性和时间是挂钩的,依赖本地时钟的准确性,因此虽然采用 Lease Read 做法非常高效,但是仍然面临风险问题,也就是存在预设的前提即各个服务器的 CPU Clock 的时间是准的,即使有误差,也会在一个非常小的 Bound 范围里面,时间的实现至关重要,如果时钟漂移严重,各个服务器之间 Clock 走的频率不一样,这套 Lease 机制可能出问题。

Follower Read

在前边两种优化方案中,无论我们怎么折腾,核心思想其实只有两点:

  • 保证在读取时的最新 commit index 已经被 apply。
  • 保证在读取时 leader 仍拥有领导权。

其实无论是 Read Index 还是 Lease Read,最终目的都是为了解决第二个问题。换句话说,读请求最终一定都是由 leader 来承载的。
那么读 follower 真的就不能满足线性一致吗?其实不然,这里我们给出一个可行的读 follower 方案:Follower 在收到客户端的读请求时,向 leader 询问当前最新的 commit index,反正所有日志条目最终一定会被同步到自己身上,follower 只需等待该日志被自己 commit 并 apply 到状态机后,返回给客户端本地状态机的结果即可。这个方案叫做 Follower Read
注意:Follower Read 并不意味着我们在读过程中完全不依赖 leader 了,在保证线性一致性的前提下完全不依赖 leader 理论上是不可能做到的。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值