参考Raft-paper、MIT 6.824-2020 video与一些辅助资料,按照paper目录顺序记录笔记

Raft lecture (Raft user study)

vedio:https://www.youtube.com/watch?v=YbZ3zDzDnrw

slides:https://ongardie.net/static/raft/userstudy/

文字叙述源自《条分缕析 Raft》,Youtube讲解内容

基本工作原理图

目标:保证 log 完全相同地复制到多台服务器上

  • replicated log => replicated state machine(所有服务器以相同的顺序执行相同的命令)
  • 共识模块确保合适的日志复制
  • 只要任何半数以上服务器达成一致系统正常运行
  • 失败模型:fail-stop(not Byzantine),delayed/lost messages,partition(网络分区)

共识方法(两类)

  • 对称的,leader-less(symmetric,leader-less)

    • 所有服务器有相同的地位
    • clients能与任何服务器通信
  • 非对称的,leader-based

    • 任何给定时刻,一个服务器控制(one server is in charge),其他服务器接收它的决策
    • clients与leader通信
  • Raft使用的后者(a leader)

    • 分解问题(normal operation,leader changes)
    • 简化正常操作(没有冲突)
    • 比许多leader-less方法高效

Raft Overview

分为六个部分讨论raft

  1. Leader election
    • 选择一个服务器充当leader
    • 检测crashes,选择新的leader
  2. Normal operation(basic log replication)
  3. Safety and consistency after leader changes
  4. Neutralizing old leaders(处理旧的leader:旧leader没有真的下线怎么办?)
  5. Client interactions
    • 实现线性化语义(linearizeable semantics)
  6. Configuration changes
    • 添加与删除服务器节点

关键术语

Server States

任意时刻,每个服务是以下三种状态之一:

  • leader:处理所有的客户端交互,日志副本(一个时刻至多有一个leader
  • follower:完全消极的(issues no RPCs,responds to incoming RPCs)
  • candidate:用于选举新的leader

normal operation:1 leader,N-1 followers

Terms(任期)

  • 时间划分为任期terms:
    • 选举
    • 在一个leader下的normal operation
  • 每个任期至多一个leader
  • 一些任期没有leader(failed election即选举失败)
  • 每个服务器维持 current term 的值
  • terms的关键作用:identity obsolete information(识别过时信息)

Heartbeats and Timeouts

  • 在开始时 服务器作为follower
  • follower期望收到来自leader或candidate的RPCs
  • leaders必须发送 heartbeats(心跳)(空的AppendEntries RPCs)来维持权威
  • 如果electionTimeout(选举超时),即选举因没有RPCs而超时:
    • follower假设leader挂掉了
    • follower开始新的选举
    • 超时时间一般为 100-500 ms

两类RPC

Raft 中服务器之间所有类型的通信通过两个 RPC 调用:

  • RequestVote:用于选举;
  • AppendEntries:用于复制 log 和发送心跳;

1.Leader election

Election Basics

  • increment current term 每次服务器开始选举时当前任期增加
  • 服务器将自己从follower转变为candidate状态(目标是获取超过半数的选票,让自己成为leader)
  • 为自己投票
  • 任何一个candidate状态的服务器一直发送RequestVote RPCs给所有其他服务器,直到以下事件之一发生:
    1. 收到来自多数(过半)服务器的投票:
      • 成为leader
      • 发送AppendEntries 心跳包给所有其他服务器
    2. 收到有效leader的RPC
      • 当前服务器从candidate状态变为follower状态
    3. 没有服务器赢得选举(election timeout elapses)
      • 任期增加,开始新的选举

Elections,cont’d(属性)

  • Safety:每个任期至多一个winner(即leader)

    • 每个任期每一个服务器只能投一票(在磁盘持久化存储投票信息voteFor,以便宕机重启后恢复。实际上每个服务器将票投给第一个满足条件的投票请求服务器
    • 在相同任期不可能出现两个不同的candidate获得过半票数(服务器总数量必须是奇数个(最多容忍2个服务器故障),如Raft服务器数量为5)

  • Liveness:确保最终选出一个leader

    可能的问题:原则上我们可以无限重复分割选票,假如选举同一时间开始,同一时间超时,同一时间再次选举,如此循环。

    • 服务器随机选择超时时间,通常在[T, 2T] (其中T = electionTimeout,越小选举越快,Raft效率更高)
    • 节点不太可能再同时开始竞选,先竞选的节点有足够的时间来索要其他节点的选票
    • 当T » broadcast time时,效果更好

2.Normal operation

先了解下日志结构Log Structure

  • Log entry = index(记录在日志中的位置),term(记录首次创建时的任期号),command(取决于状态的客户端)
  • 日志存储在非易失性介质(即磁盘)上;便于从故障中恢复
  • 如果 一条日志记录被存储在超过半数的服务器上,则认为该记录 已提交(committed)。【Raft系统中一条重要属性】(如果一条记录已提交,意味着状态机可以安全的执行该记录。上图中记录1-7已提交,记录8没有提交)

再是正常操作(非常简单)

  • 客户端发送命令给leader
  • leader将命令添加到自己的日志中
  • leader发送AppendEntries RPCs给followers(并行)
  • 一旦新的日志记录已提交:
    • leader将命令传递给它自己的状态机,然后将结果返回给client
    • leader在后续的AppendEntries RPCs中通知followers已提交的记录
    • followers将已提交的记录传递给它们自己的状态机
  • 如果followers故障宕机/超时 怎么办呢?
    • leader反复尝试发送RPC直到它们成功
  • 性能优化:leader只需要超过半数的成功相应,而不必等待每个follower做出响应(确保日志记录已经存储在超过半数的服务器上)【这样系统效率不受慢节点所限制】

Log Consistency(日志一致性)

Raft 尝试在集群中保持日志较高的一致性

Raft 日志的 index 和 term 唯一标示一条日志记录。(这非常重要!!!)

  1. 如果两个节点的日志在相同的索引位置上的任期号相同,则认为他们具有一样的命令;从头到这个索引位置之间的日志完全相同
  2. 如果给定的记录已提交,那么所有前面的记录也已提交

AppendEntries 一致性检查

Raft 通过 AppendEntries RPC 来检测这两个属性。

  • 对于每个 AppendEntries RPC 包含新日志记录之前那条记录的索引(prevLogIndex)和任期(prevLogTerm);
  • Follower 检查自己的 index 和 term 是否与 prevLogIndexprevLogTerm 匹配,匹配则接收该记录;否则拒绝;

举例:

3.Leader更换

当新的 Leader 上任后,日志可能不会非常干净,因为前一任领导可能在完成日志复制之前就宕机了。Raft 对此的处理方式是:无需采取任何特殊处理。

当新 Leader 上任后,他不会立即进行任何清理操作,他将会在正常运行期间进行清理。

原因是当一个新的 Leader 上任时,往往意味着有机器故障了,那些机器可能宕机或网络不通,所以没有办法立即清理他们的日志。在机器恢复运行之前,我们必须保证系统正常运行。

**大前提是 Raft 假设了 Leader 的日志始终是对的。**所以 Leader 要做的是,随着时间推移,让所有 Follower 的日志最终都与其匹配。

但与此同时,Leader 也可能在完成这项工作之前故障,日志会在一段时间内堆积起来,从而造成看起来相当混乱的情况,如下所示:

因为我们已经知道 index 和 term 是日志记录的唯一标识符,这里不再显示日志包含的命令,下同。

如图,这种情况可能出现在 S4 和 S5 是任期 2、3、4 的 Leader,但不知何故,他们没有复制自己的日志记录就崩溃了,系统分区了一段时间,S1、S2、S3 轮流成为了任期 5、6、7 的 Leader,但无法与 S4、S5 通信以进行日志清理——所以我们看到的日志非常混乱。

唯一重要的是,索引 1-3 之间的记录是已提交的(已存在多数派节点),因此我们必须确保留下它们

其它日志都是未提交的,我们还没有将这些命令传递给状态机,也没有客户端会收到这些执行的结果,所以不管是保留还是丢弃它们都无关紧要。

安全性需求

一旦状态机执行了一条日志里的命令,必须确保其它状态机在同样索引的位置不会执行不同的命令。

Raft 安全性(Safety):如果某条日志记录在某个任期号已提交,那么这条记录必然出现在更大任期号的未来 Leader 的日志中。

这保证了安全性要求:

  • Leader 不会覆盖日志中的记录;
  • 只有 Leader 的日志中的记录才能被提交;
  • 在应用到状态机之前,日志必须先被提交;

这决定我们要修改选举程序:(两个限制,分别是限制leader的选举 与 延迟提交)

  • 如果节点的日志中没有正确的内容,需要避免其成为 Leader;
  • 稍微修改 committed 的定义:前面说多数派存储即是已提交的,但在某些时候,我们必须延迟提交日志记录,直到我们知道这条记录是安全所谓安全的,就是我们认为后续 Leader 也会有这条日志

延迟提交,选出最佳 Leader

问题:我们如何确保选出了一个很好地保存了所有已提交日志的 Leader ?

这有点棘手,举个例子:假设我们要在下面的集群中选出一个新 Leader,但此时第三台服务器不可用。

这种情况下,仅看前两个节点的日志我们无法确认是否达成多数派,故无法确认第五条日志是否已提交。

那怎么办呢?

通过比较日志,在选举期间,选择最有可能包含所有已提交的日志:

  • Candidate 在 RequestVote RPCs 中包含日志信息(最后一条记录的 index 和 term,记为 lastIndexlastTerm);
  • 收到此投票请求的服务器 V 将比较谁的日志更完整:(lastTermV > lastTermC) ||(lastTermV == lastTermC) && (lastIndexV > lastIndexC) 将拒绝投票;(即:V 的任期比 C 的任期新,或任期相同但 V 的日志比 C 的日志更完整);
  • 无论谁赢得选举,可以确保 Leader 和超过半数投票给它的节点中拥有最完整的日志——最完整的意思就是 index 和 term 这对唯一标识是最大的

举个例子

Case 1: Leader 决定提交日志

任期 2 的 Leader S1 的 index = 4 日志刚刚被复制到 S3,并且 Leader 可以看到 index = 4 已复制到超过半数的服务器,那么该日志可以提交,并且安全地应用到状态机。

现在,这条记录是安全的,下一任期的 Leader 必须包含此记录,因此 S4 和 S5 都不可能从其它节点那里获得选票:S5 任期太旧,S4 日志太短

只有前三台中的一台可以成为新的 Leader——S1 当然可以,S2、S3 也可以通过获取 S4 和 S5 的选票成为 Leader

Case 2: Leader 试图提交之前任期的日志

如图所示的情况,在任期 2 时记录仅写在 S1 和 S2 两个节点上,由于某种原因,任期 3 的 Leader S5 并不知道这些记录,S5 创建了自己的三条记录然后宕机了,然后任期 4 的 Leader S1 被选出,S1 试图与其它服务器的日志进行匹配。因此它复制了任期 2 的日志到 S3。

此时 index=3 的记录时是不安全的

因为 S1 可能在此时宕机,然后 S5 可能从 S2、S3、S4 获得选票成为任期 5 的 Leader。一旦 S5 成为新 Leader,它将覆盖 index=3-5 的日志,S1-S3 的这些记录都将消失。

我们还要需要一条新的规则,来处理这种情况。

新的 Commit 规则

新的选举不足以保证日志安全,我们还需要继续修改 commit 规则。

Leader 要提交一条日志:

  • 日志必须存储在超过半数的节点上;
  • Leader 必须看到:超过半数的节点上还必须存储着至少一条自己任期内的日志

如图,回到上面的 Case 2: 当 index = 3 & term = 2 被复制到 S3 时,它还不能提交该记录,必须等到 term = 4 的记录存储在超过半数的节点上,此时 index = 3 和 index = 4 可以认为是已提交。

此时 S5 无法赢得选举了,它无法从 S1-S3 获得选票。

结合新的选举规则和 commit 规则,我们可以保证 Raft 的安全性。

日志不一致

Leader 变更可能导致日志的不一致,这里展示一种可能的情况。

可以从图中看出,Raft 集群中通常有两种不一致的日志:

  • 缺失的记录(Missing Entries);
  • 多出来的记录(Extraneous Entries);

我们要做的就是清理这两种日志。

修复 Follower 日志

新的 Leader 必须使 Follower 的日志与自己的日志保持一致,通过:

  • 删除 Extraneous Entries;
  • 补齐 Missing Entries;

Leader 为每个 Follower 保存 nextIndex

  • 下一个要发送给 Follower 的日志索引;
  • 初始化为:1 + Leader 最后一条日志的索引;

Leader 通过 nextIndex 来修复日志。当 AppendEntries RPC 一致性检查失败,递减 nextIndex 并重试。如下图所示:

对于 a:

  • 一开始 nextIndex = 11,带上日志 index = 10 & term = 6,检查失败;
  • nextIndex = 10,带上日志 index = 9 & term = 6,检查失败;
  • 如此反复,直到 nextIndex = 5,带上日志 index = 4 & term = 4,该日志现在匹配,会在 a 中补齐 Leader 的日志。如此往下补齐。

对于 b: 会一直检查到 nextIndex = 4 才匹配。值得注意的是,对于 b 这种情况,当 Follower 覆盖不一致的日志时,它将删除所有后续的日志记录(任何无关紧要的记录之后的记录也都是无关紧要的)。如下图所示:

4. 处理旧 Leader

实际上,老的 Leader 可能不会马上消失,例如:网络分区将 Leader 与集群的其余部分分隔,其余部分选举出了一个新的 Leader。问题在于,如果老的 Leader 重新连接,也不知道新的 Leader 已经被选出来,它会尝试作为 Leader 继续提交日志。此时如果有客户端向老 Leader 发送请求,老的 Leader 会尝试存储该命令并向其它节点复制日志——我们必须阻止这种情况发生。

任期就是用来发现过时的 Leader(和 Candidates):

  • 每个 RPC 都包含发送方的任期;
  • 如果发送方的任期太老,无论哪个过程,RPC 都会被拒绝,发送方转变到 Follower 并更新其任期;
  • 如果接收方的任期太老,接收方将转为 Follower,更新它的任期,然后正常的处理 RPC;

由于新 Leader 的选举会更新超过半数服务器的任期,旧的 Leader 不能提交新的日志,因为它会联系至少一台多数派集群的节点,然后发现自己任期太老,会转为 Follower 继续工作。

这里不打算继续讨论别的极端情况。

5. 客户端协议

客户端只将命令发送到 Leader:

  • 如果客户端不知道 Leader 是谁,它会和任意一台服务器通信;
  • 如果通信的节点不是 Leader,它会告诉客户端 Leader 是谁;

Leader 直到将命令记录、提交和执行到状态机之前,不会做出响应。

这里的问题是如果 Leader 宕机会导致请求超时:

  • 客户端重新发出命令到其他服务器上,最终重定向到新的 Leader
  • 用新的 Leader 重试请求,直到命令被执行

这留下了一个命令可能被执行两次的风险——Leader 可能在执行命令之后但响应客户端之前宕机,此时客户端再去寻找下一个 Leader,同一个命令就会被执行两次——这是不可接受的!

解决办法是:客户端发送给 Leader 的每个命令都带上一个唯一 id

  • Leader 将唯一 id 写到日志记录中
  • 在 Leader 接受命令之前,先检查其日志中是否已经具有该 id
  • 如果 id 在日志中,说明是重复的请求,则忽略新的命令,返回旧命令的响应

每个命令只会被执行一次,这就是所谓的线性化的关键要素

6. 配置变更

随着时间推移,会有机器故障需要我们去替换它,或者修改节点数量,需要有一些机制来变更系统配置,并且是安全、自动的方式,无需停止系统。

系统配置是指:

  • 每台服务器的 id 和地址
  • 系统配置信息是非常重要的,它决定了多数派的组成

首先要意识到,我们不能直接从旧配置切换到新配置,这可能会导致**矛盾的多数派**。

如图,系统以三台服务器的配置运行着,此时我们要添加两台服务器。如果我们直接修改配置,他们可能无法完全在同一时间做到配置切换,这会导致 S1 和 S2 形成旧集群的多数派,而同一时间 S3-S5 已经切换到新配置,这会产生两个集群。

这说明我们必须使用一个两阶段(two-phase)协议

如果有人告诉你,他可以在分布式系统中一个阶段就做出决策,你应该非常认真地询问他,因为他要么错了,要么发现了世界上所有人都不知道的东西。

共同一致(Joint Consensus)

Raft 通过共同一致(Joint Consensus)来完成两阶段协议,即:新、旧两种配置上都获得多数派选票。

第一阶段:

  • Leader 收到 $C_{new}$的配置变更请求后,先写入一条$C_{old+new}$ 的日志,配置变更立即生效,然后将日志通过 AppendEntries RPC 复制到 Follower 中,收到该$C_{old+new}$ 的节点立即应用该配置作为当前节点的配置;
  • $C_{old+new}$日志复制到多数派节点上时,$C_{old+new}$ 的日志已提交

$C_{old+new}$ 日志已提交保证了后续任何 Leader 一定有$C_{old+new}$ 日志,Leader 选举过程必须获得旧配置中的多数派和新配置中的多数派同时投票。

PS:上图蓝色线条时间段表示新旧两种配置已达成Joint Consensus。

第二阶段:

  • $C_{old+new}$日志已提交后,立即写入一条 $C_{new}$的日志,并将该日志通过 AppendEntries RPC 复制到 Follower 中,收到 $C_{new}$的节点立即应用该配置作为当前节点的配置;
  • $C_{new}$日志复制到多数派节点上时,$C_{new}$ 的日志已提交;在$C_{new}$ 日志提交以后,后续的配置都基于 $C_{new}$了

Joint Consensus 还有一些细节:

  • 变更过程中,来自新旧配置的节点都有可能成为 Leader;
  • 如果当前 Leader 不在$C_{new}$ 配置里面,一旦 $C_{new}$提交,它**必须下台(step down)**。

如图所示,旧 Leader 不再是新配置的成员之后,还有可能继续服务一小段时间;即**旧 Leader 可能在$C_{new}$ 配置下继续当 Leader(虽然实质上并不是Leader),直到$C_{new}$ 的日志复制到多数派上而 committed**

[Raft]In Search of an Understandable Consensus Algorithm

1.Introduction

2.Replicated state machines

实际系统共识算法的属性:

  1. safety(绝不返回不正确结果) 在所有non-Byzantine条件下,包括网络实验、分区、包丢失、重复和重新排序。
  2. 只要大多数服务器都是可操作的,并且能够相互通信和与客户端通信,他们就可以完全发挥作用(可用的)。
  3. 它们不依赖时钟(timing)来确保logs的一致性:在最坏的情况下,错误的时钟和极端的消息延迟可能会导致可用性问题
  4. 在通常情况下,只要集群中的大多数对单个RPC做出了相应,命令就可以完成。少数慢服务器不影响整体系统性能

3.What’s wrong with Paxos?

4.Designing for understandability

Raft的设计目标

  1. [实用性]为系统搭建提供一个完整且实用的基础,大大减少开发人员所需的设计工作量
  2. [安全性]在所有情况下都是安全的
  3. [可用性]在经典的操作条件下是可用的
  4. [高效性]一般操作时是高效的
  5. [易理解]易于理解的(understandability):最重要且最困难的
  6. [可扩展]现实中是可扩展的

Raft设计方法评估

基于可理解性(how hard is it to explain each alternative (for example, how complex is its state space, and does it have subtle implications?), and how easy will it be for a reader to completely understand the approach and its implications?)评估Raft设计的可选方法

使用两种方法来评估:

  • 问题分解(problem decomposition):只要有可能,我们将问题划分为可以相对独立地解决、解释和理解的独立部分。如Raft中分为leader election、log replication、safety、membership changes
  • 通过减少考虑的状态数来简化状态空间,使系统更连贯,尽可能消除不确定性。具体来说,日志不允许有孔(holes),并且Raft限制了日志之间可能不一致的方式。随机化方法引入了不确定性,但它们倾向于通过以类似的方式处理所有可能的选择来减少状态空间(“选择任何;没关系”)。我们使用随机化来简化Raft leader election算法。

5.Raft consensus algorithm

服务器行为被描述为一组独立且重复触发的规则

Terms

State

  • Persistent state on all servers(在响应RPCs前更新稳定存储)
    • currentTerm:server最新的任期(server第一次启动时初始值为0,单调增加)
    • votedFor:当前任期内获得投票的candidate(没有则为null)
    • log[]:log entries日志条目,每个条目包含状态机的命令,以及leader接受条目时的term(第一个索引为1)
  • Volatile state on all servers(服务器上的不稳定状态)
    • commitIndex:已知提交的最高日志条目的索引(初始化为0,单调增加)
    • lastApplied:应用于状态机的最高日志条目的索引(初始值为0,单调增加)
  • Volatile state on leaders(选举后初始化)
    • nextIndex[]:对于每个服务器,发送到该服务器的下一个日志条目的索引(初始值为 leader最后一个日志索引+1)
    • matchIndex[]:对于每个服务器,已知在服务器上复制的最高日志条目的索引(初始值为0,单调增加)

Raft首先选择一个leader并赋予它管理复制日志的完整责任。leader接收来自clients的log entries,复制到其他服务器上,告诉服务器何时在它们的状态机上执行log entries的命令是安全的

leader能够在不询问其他服务器时决定哪里放置新的entries,并且数据以一种简单的方式从leader流向其他服务器。当选出新的leader时,旧的leader可能失败或者与其他服务器断开连接。 存在一个leader简化了replicated log的管理。

基于leader方法,Raft将共识算法分解为三个相对独立的子问题。

  • Leader election:当现存的leader失败时必须选择一个新的leader(section 5.2)
  • Log replication:leader必须接收来自clients的log entries并且在集群中复制他们,迫使其他日志与自己的日志一致(section 5.3)
  • Safety:Raft的关键安全属性是图3中的状态机安全属性如果任何服务器在它自己的状态机上应用(执行)特定的log entry,那么其他服务器就不能对同一个日志索引应用不同的命令。section 5.4描述Raft确保这个属性;解决方案涉及对section 5.2所述 选举机制的额外限制

5.1 Raft basic

图4:服务器状态

在任意时刻每台服务器是三种状态(leader, follower, candidate)之一。在正常运行时,恰好有一个leader,其他所有服务器都是follower。

  • follower是消极的(passive),即它们不发送任何请求,只响应leader与candidate的请求。

  • leader处理所有的客户端请求(如果客户端联系了一个follower,follower将它重定向到leader)。

  • candidate,用于选择一个新的leader

如图4所示,若follower没有收到请求通信(是超出最长通信时间),将变成candidate,然后初始化一个election。接收到超过所有服务器的一半的投票将成为新的leader。leader通常会一直工作到失败为止。

图5:将时间划分为任期(terms)

如图5所示,将时间划分为任期,每个任期以election开始。在一个成功选举后,一个leader一直管理集群直到任期结束。有些选举失败了,以没有选出一个leader结束该任期。在不同的服务器上,可以在不同的时间观察到任期之间的转换(如t3->t4)。

两类RPCs:1)选举过程candidate初始化RequestVote RPCs(5.2);2)leaders初始化AppendEntries RPCs复制日志条目和发送心跳包(5.3节)

AppendEntries RPC

(由leader调用来复制日志条目5.3节,也被用作心跳5.2节

5.2 Leader election

随机化去除节点之间的同步特性:即不同的服务器选取随机的超时时间,总会有一个选举定时器先超时,另一个后超时。从而可以大概率避免分割选票(Split Vote)。

PS:选举定时器的超时时间需要至少大于leader的心跳间隔时间。

从实际系统角度考虑,1)最大超时时间影响了系统能够多快从故障中恢复;超时上限越大,系统恢复时间也越长。最大超时时间的重要程度取决于我们需要达到多高的性能以及故障出现的频率。2)不同节点的选举定时器的超时时间差(S2和S3之间)必须足够长,使得第一个开始选举的节点能够完成一轮选举。这里至少需要大于发送一条RPC所需的往返(Round-Trip)时间。3)每一次一个节点重置自己的选举定时器时,需要重新选择一个(不同的)随机超时时间。

img

5.3 Log replication

5.4 Safety

  • Election restriction
  • Committing entries from previous terms
  • Safety argument

5.5 Follower and candidate crashes

5.6 Timing and availability

6.Cluster membership changes

7.Log compaction

8. Client interaction

9.Implementation and evaluation

  • Understandability
  • Correctness
  • Performance

10.Related work

11.Conclusion

References

  • Raft(extended):http://nil.csail.mit.edu/6.824/2020/papers/raft-extended.pdf
  • Raft lecture (Raft user study):https://www.youtube.com/watch?v=YbZ3zDzDnrw
  • Paxos:http://courses.cs.washington.edu/courses/csep590/04wi/papers/lamport-part-time-parliament.pdf
  • Paxos Made Simple:https://lamport.azurewebsites.net/pubs/paxos-simple.pdf
  • Paxos lecture (Raft user study):https://www.youtube.com/watch?v=JEpsBg0AO6o
  • 《条分缕析 Raft》
  • 《条分缕析 Raft 算法(续):日志压缩和性能优化》