1111
1212## 1 背景
1313
14- 当今的数据中心和应用程序在高度动态的环境中运行,为了应对高度动态的环境,它们通过额外的服务器进行横向扩展,并且根据需求进行扩展和收缩。同时,服务器和网络故障也很常见 。
14+ 在如今的互联网架构中,为了扛住海量流量,系统往往需要横向堆机器。机器一多,宕机、断网这些破事就成了家常便饭。怎么让这群随时可能掉线的服务器保持步调一致,不对外提供错乱的数据?这就轮到 ** 分布式共识算法 ** 出场了 。
1515
16- Raft 算法由 Diego Ongaro 和 John Ousterhout 于 2014 年在 Usenix ATC 会议论文《In Search of an Understandable Consensus Algorithm》中提出。Raft 通过复制日志来保证副本状态机的一致性与安全性;在配套正确的客户端交互与读实现(如 ReadIndex / Lease Read、请求去重)后,可实现线性一致(linearizable)的读写语义,旨在作为 Paxos 的更易理解替代。
17-
18- 相比 Paxos,Raft 通过分解为相对独立的子问题降低复杂度:
16+ 2014年,Diego Ongaro 等人发表了 Raft 算法。它的诞生有一个很明确的使命:** 拯救被 Paxos 算法折磨的程序员** 。Raft 主打一个“易于理解”,它将复杂的共识问题拆解成了几个独立的模块:
1917
2018- ** Leader 选举** :使用随机化选举超时(工程上常见如 150–300ms 或更大范围,具体取决于网络与故障模型)。
2119- ** 日志复制** :Leader 通过 AppendEntries RPC 广播日志。
@@ -34,43 +32,29 @@ Raft 在实际生产中得到了广泛应用,基于 Raft 的实现如 etcd、C
3432
3533### 1.1 非拜占庭条件下的"选主"类比
3634
37- Raft 工作在非拜占庭(Crash Fault Tolerance, CFT)假设下:节点可能宕机、重启、网络延迟或分区,但不会恶意伪造/篡改消息。下面用"多方通过投票选出指挥者"的类比,仅用于帮助理解 Leader 选举与重试机制,不涉及拜占庭容错(BFT)。
38-
39- > 假设多位将军需要选出一位指挥官,信使的信息可靠但有可能被暗杀(网络故障),将军们如何达成一致?
35+ Raft 有一个前提假设:** 非拜占庭容错(CFT)** 。说白了就是,兄弟们可能会死机、会断网,但绝对不会出内鬼传递假情报。
4036
41- 解决方案大致可以理解成:先在所有的将军中选出一个大将军,用来做出所有的决定 。
37+ 我们可以用“将军选帅”来粗略理解这个过程: 假设有 A、B、C 三个将军,目前群龙无首。每个人心里都有个随机的倒计时(选举超时)。谁的倒计时先结束,谁就站出来大喊:“我要当大将军,请给我投票!” 如果其他将军还没开始竞选,也没把票投给别人,就会顺水推舟同意他。当这位将军拿到 ** 过半数 ** 的赞成票,他就成了大当家(Leader)。以后打不打仗,全听他的。如果信使半路阵亡,大家都没收到回音,那就重置倒计时,再来一轮 。
4238
43- 举例如下:假如现在一共有 3 个将军 A,B 和 C,每个将军都有一个随机时间的倒计时器,倒计时一结束,这个将军就把自己当成大将军候选人,然后派信使传递选举投票的信息给将军 B 和 C,如果将军 B 和 C 还没有把自己当作候选人(自己的倒计时还没有结束),并且没有把选举票投给其他人,它们就会把票投给将军 A,信使回到将军 A 时,将军 A 知道自己收到了足够的票数,成为大将军。在有了大将军之后,是否需要进攻就由大将军 A 决定,然后再去派信使通知另外两个将军,自己已经成为了大将军。如果一段时间还没收到将军 B 和 C 的回复(信使可能会被暗杀),那就再重派一个信使,直到收到回复。
39+ ### 1.2 到底什么是共识算法?
4440
45- ### 1.2 共识算法
41+ 共识算法的核心目标,就是 ** 让一群机器看起来像一台机器 ** 。只要集群里超过半数的机器还活着,整个系统就能正常接客。
4642
47- 共识是可容错系统中的一个基本问题:即使面对故障,服务器也可以在共享状态上达成一致。
48-
49- 共识算法允许一组节点像一个整体一样一起工作,即使其中的一些节点出现故障也能够继续工作下去,其正确性主要是源于复制状态机的性质:一组` Server ` 的状态机计算相同状态的副本,即使有一部分的` Server ` 宕机了它们仍然能够继续运行。
43+ 这通常是通过** 复制状态机** 来实现的:给每个节点发一本一模一样的账本(日志)。只要大家按照同样的顺序去执行账本上的命令,最后得到的结果自然完全一样。所以,共识算法本质上干的就是一件事——** 保证所有节点的账本绝对一致** 。共识是可容错系统中的一个基本问题:即使面对故障,服务器也可以在共享状态上达成一致。
5044
5145![ 共识算法架构] ( https://oss.javaguide.cn/github/javaguide/paxos-rsm-architecture.png )
5246
53- 一般通过使用复制日志来实现复制状态机。每个` Server ` 存储着一份包括命令序列的日志文件,状态机会按顺序执行这些命令。因为每个日志包含相同的命令,并且顺序也相同,所以每个状态机处理相同的命令序列。由于状态机是确定性的,所以处理相同的状态,得到相同的输出。
54-
55- 因此共识算法的工作就是保持复制日志的一致性。服务器上的共识模块从客户端接收命令并将它们添加到日志中。它与其他服务器上的共识模块通信,以确保即使某些服务器发生故障,系统仍能在日志顺序上达成一致;最终每个日志都包含相同顺序的请求。一旦命令被正确地复制,它们就被称为已提交。每个服务器的状态机按照日志顺序处理已提交的命令,并将输出返回给客户端,因此,这些服务器形成了一个单一的、高度可靠的状态机。
56-
57- 适用于实际系统的共识算法通常具有以下特性:
58-
59- - 安全。确保在非拜占庭条件(也就是上文中提到的简易版拜占庭)下的安全性,包括网络延迟、分区、包丢失、复制和重新排序。
60- - 高可用。只要大多数服务器都是可操作的,并且可以相互通信,也可以与客户端进行通信,那么这些服务器就可以看作完全功能可用的。因此,一个典型的由五台服务器组成的集群可以容忍任何两台服务器端故障。假设服务器因停止而发生故障;它们稍后可能会从稳定存储上的状态中恢复并重新加入集群。
61- - 一致性不依赖时序。错误的时钟和极端的消息延迟,在最坏的情况下也只会造成可用性问题,而不会产生一致性问题。
62-
63- - 在集群中大多数服务器响应,命令就可以完成,不会被少数运行缓慢的服务器来影响整体系统性能。
47+ ## 2 基础概念
6448
65- ## 2 基础
49+ 在深入 Raft 之前,我们得先认识里面的三大核心角色、任期机制和日志结构。
6650
6751### 2.1 节点类型
6852
6953一个 Raft 集群包括若干服务器,以典型的 5 服务器集群举例。在任意的时间,每个服务器一定会处于以下三个状态中的一个:
7054
71- - ` Leader ` :负责发起心跳,响应客户端,创建日志,同步日志 。
72- - ` Candidate ` :Leader 选举过程中的临时角色,由 Follower 转化而来,发起投票参与竞选 。
73- - ` Follower ` :接受 Leader 的心跳和日志同步数据,投票给 Candidate 。
55+ - ** Leader(领导者) ** :大当家。全权负责接待客户端、写账本、并把账本同步给小弟。为了防止别人篡位,他必须不断地向全员发送心跳,宣告“我还活着” 。
56+ - ** Follower(跟随者) ** :安分守己的小弟。平时绝对不主动发起请求,只被动接收老大的心跳和账本同步 。
57+ - ** Candidate(候选人) ** :临时状态。如果小弟迟迟等不到老大的心跳,就会觉得自己行了,变身候选人开始拉票 。
7458
7559在正常的情况下,只有一个服务器是 Leader,剩下的服务器是 Follower。Follower 是被动的,它们不会发送任何请求,只是响应来自 Leader 和 Candidate 的请求。
7660
@@ -90,13 +74,12 @@ Raft 算法将时间划分为任意长度的任期(term),任期用连续
9074
9175### 2.3 日志
9276
93- - ` entry ` :每一个事件成为 entry,只有 Leader 可以创建 entry。entry 的内容为` <term,index,cmd> ` 其中 cmd 是可以应用到状态机的操作。
94- - ` log ` :由 entry 构成的数组,每一个 entry 都有一个表明自己在 log 中的 index。只有 Leader 才可以改变其他节点的 log。entry 总是先被 Leader 添加到自己的 log 数组中,然后再发起共识请求,获得同意后才会被 Leader 提交给状态机。Follower 只能从 Leader 获取新日志和当前的 commitIndex,然后把对应的 entry 应用到自己的状态机中。
77+ 只有 Leader 有资格往账本里追加记录(Entry)。一条日志包含三个核心要素:` <当前任期, 索引号, 具体操作指令> ` 。
9578
96- 补充两个常用指针 :
79+ 这里有两个非常关键的进度指针 :
9780
98- - ` commitIndex ` :已提交(committed)的最大日志索引;表示哪些日志已经被集群确认并可以安全地应用到状态机 。
99- - ` lastApplied ` :已被状态机应用(applied)的最大日志索引;通常 lastApplied ≤ commitIndex 。
81+ - ** commitIndex** :大家公认已经安全落地的日志进度(已经被复制到过半数节点) 。
82+ - ** lastApplied** :这台机器本地真正执行完的日志进度 。
10083
10184## 3 领导人选举
10285
@@ -189,17 +172,18 @@ entry[0] 一致 → entry[1] 一致 → entry[2] 一致 → ... → entry[N] 一
189172
190173### 4.2 日志不一致的恢复
191174
192- 一般情况下, Leader 和 Follower 的日志保持一致,但 Leader 的崩溃会导致日志出现差异。此时 AppendEntries 的一致性检查会失败,Leader 通过强制 Follower 复制自己的日志来处理日志的不一致。这就意味着,在 Follower 上的冲突日志会被领导者的日志覆盖 。
175+ 正常运作时,大当家( Leader)和小弟( Follower)的账本是完全同步的。然而,一旦老 Leader 突然宕机,新老交替之际往往会在集群中遗留大量未对齐的脏数据 。
193176
194- 为了使得 Follower 的日志和自己的日志一致, Leader 需要找到 Follower 与它日志一致的地方,然后删除 Follower 在该位置之后的日志,接着把这之后的日志发送给 Follower。
177+ 这时,新 Leader 发起 AppendEntries 同步请求就会触发“一致性检查报错”。Raft 解决数据冲突的逻辑非常霸道: ** 一切以现任 Leader 的账本为最高准则 ** , Follower 本地任何不一致的记录都必须被无情抹除并强行覆盖 。
195178
196- ` Leader ` 给每一个 ` Follower ` 维护了一个 ` nextIndex ` ,它表示 ` Leader ` 将要发送给该追随者的下一条日志条目的索引。当一个 ` Leader ` 开始掌权时,它会将 ` nextIndex ` 初始化为它的最新的日志条目索引数+1。如果一个 ` Follower ` 的日志和 ` Leader ` 的不一致, ` AppendEntries ` 一致性检查会在下一次 ` AppendEntries RPC ` 时返回失败 。
179+ 具体怎么做呢? Leader 会像“拉链”一样顺藤摸瓜,往前倒推寻找双方最后一次完美吻合的历史节点。找到这个“分叉点”后, Follower 会把分叉点之后的烂摊子全部咔嚓掉,老老实实地拷贝 Leader 提供的最新日志 。
197180
198- ** (朴素实现) ** 在失败之后, ` Leader ` 会将 ` nextIndex ` 递减然后重试 ` AppendEntries RPC ` ,直到找到 Leader 与 Follower 日志一致的位置。
181+ 在代码层面, Leader 会在内存里给每个 Follower 单独记一本账,核心指针叫 ` nextIndex ` (预估要发给该小弟的下一条日志位置)。新官上任三把火,Leader 刚接盘时,会盲目自信地把所有小弟的 ` nextIndex ` 都预设为自己最新日志的索引加一。如果小弟的数据其实比较落后或者有冲突,第一发 AppendEntries 必然惨遭拒绝。接下来就是找分叉点的两种流派:
199182
200- ** (工程优化)** 实际生产实现通常会加入快速回退(Fast Backup):Follower 在拒绝 AppendEntries 时返回冲突日志对应的任期(term)以及该任期的边界索引,Leader 据此一次性跳过整段冲突区间,显著减少重试次数。
183+ - ** 传统的朴素做法(逐条试探)** :撞了南墙就退一步。Leader 会把 ` nextIndex ` 减一,再发一次 RPC 试探。如果还不行,就继续减一,犹如乌龟漫步般逐条往前回退,直到彻底对上暗号。
184+ - ** 工业级提速优化(Fast Backup 快速回退)** :在真实的生产环境中,逐条回退绝对是性能灾难。因此,工业界引入了快速回退机制。小弟在拒绝同步时不再是单纯地摇摇头,而是直接亮出底牌:“我这批错乱日志属于哪个历史任期(term),以及这个任期的头尾边界在哪里”。Leader 拿到这份情报,直接大刀阔斧地一次性跨越整段错误任期,极大地削减了冗余的网络重试次数。
201185
202- 最终 ` nextIndex ` 会达到一个 ` Leader ` 和 ` Follower ` 日志一致的地方。这时, ` AppendEntries ` 会返回成功, ` Follower ` 中冲突的日志条目都被移除了,并且添加所缺少的上了 ` Leader ` 的日志条目。一旦 ` AppendEntries ` 返回成功, ` Follower ` 和 ` Leader ` 的日志就一致了,这样的状态会保持到该任期结束 。
186+ 经过这番拉扯, ` nextIndex ` 终将精准锚定双方的共识起点。此时, AppendEntries 终于收获成功回执, Follower 上的冲突数据被彻底清空,缺失的正统日志被严丝合缝地填补。一旦跨过这个坎,双方的账本就能在整个任期内保持如影随形、高度一致 。
203187
204188## 5 安全性
205189
0 commit comments