- NotebookLM 生成的双人播客
- Gemini 2.5 Pro 生成的 Raft 论文解析
https://notebooklm.google.com/notebook/5338c666-781b-4164-a510-7d4e3f50ab82/audio
微信链接: https://mp.weixin.qq.com/s/xaC-1rIrlC3tDNuFVEyRnA
给我留下的较深的印象:
- 防止日志堆积,定期 install snapshot , follower 也可以自行 install snapshot
- 对于客户端而言, Raft 系统是“线性一致性”的,即客户端对 Raft 系统的访问是原子的
- 客户端总是向 leader 发送请求
- 每个命令有唯一的序列号,防止命令重复执行
- 只读操作可以不写日志,但是可能读到旧数据(刚读完 leader 就不是 leader 了),需要用心跳 + No op 空日志条目确认自己仍然是 leader
我们继续探讨 Raft 论文的后半部分,重点关注 Section 7(日志压缩)至文末的内容,并结合讲座和 FAQ 中的相关信息,进行一个有条理的详细阐述。
Raft 深入探讨:日志压缩、客户端交互、评估与相关工作
在对 Raft 算法的领导者选举、日志复制和安全性机制有了基本了解后,本文将深入探讨 Raft 论文的后半部分。我们将重点分析 Section 7 至文末的内容,涵盖日志压缩、客户端交互、算法的实现与评估,以及与其他相关工作的比较。这些内容对于理解 Raft 在实际系统中的应用和优势至关重要。
Raft 的日志在正常运行期间会持续增长,以包含更多的客户端请求。然而,在实际系统中,日志不能无限增长,否则会占用过多存储空间,并且在节点重启时需要更长的回放时间,这最终可能导致可用性问题。因此,必须有一种机制来丢弃日志中累积的过时信息。
随着系统的长时间运行,日志会变得非常庞大,远超状态机当前的状态所占空间。如果服务器重启,回放整个日志会非常耗时。此外,向新加入的服务器或落后较多的服务器发送整个日志也是不切实际的。
最简单的压缩方法是 快照 (snapshotting)。其基本思想是,系统的整个当前状态被写入一个持久化的快照文件,然后该时间点之前的所有日志条目都可以被丢弃。
每个服务器独立地创建快照,仅覆盖其日志中已提交的条目。主要工作包括状态机将其当前状态写入快照。例如,对于一个键值存储服务,快照将包含当前的键值表。Raft 在快照中也包含少量元数据:
lastIncludedIndex: 快照所取代的日志中的最后一个条目的索引(即状态机已应用的最后一个条目)。lastIncludedTerm:lastIncludedIndex条目所属的任期号。 这两个元数据用于支持快照之后第一个日志条目的AppendEntries一致性检查。- 最新配置: 为了支持集群成员变更(Section 6),快照还会包含截至
lastIncludedIndex时日志中的最新配置信息。
一旦服务器完成了快照的写入,它可以删除 lastIncludedIndex 及其之前的所有日志条目,以及任何更早的快照。
虽然服务器通常独立创建快照,但领导者偶尔需要向落后太多的追随者发送快照。当领导者已经丢弃了需要发送给某个追随者的下一个日志条目时,就会发生这种情况。此时,领导者会使用一个名为 InstallSnapshot 的新 RPC 将快照发送给该追随者。
InstallSnapshot RPC 的参数包括领导者的任期、leaderId、快照的 lastIncludedIndex 和 lastIncludedTerm、快照数据块的偏移量 (offset) 以及数据本身,还有一个 done 标记指示是否为最后一个数据块。快照可能会被分割成多个块(chunks)进行传输,offset 字段指明了当前数据块在完整快照中的位置。
当追随者接收到 InstallSnapshot RPC 时:
- 如果 RPC 中的任期
term < currentTerm,则立即回复。 - 如果是第一个数据块 (
offset为 0),则创建新的快照文件。 - 在指定偏移量处写入数据。
- 如果
done为false,则回复并等待更多数据块。 - 如果
done为true,则保存快照文件,丢弃任何具有较小索引的现有或部分快照。 - 检查其现有日志。如果其日志中存在与快照的
lastIncludedIndex和lastIncludedTerm相同的条目,则保留该条目之后的所有日志条目并回复。这意味着快照描述了其日志的一个前缀,该前缀所包含的操作效果已在快照中,可以安全丢弃。这种情况可能因为网络消息乱序送达导致,例如领导者先发送了索引为 100 的快照,再发送索引为 110 的快照,但网络先投递了后者。 - 否则(通常情况,快照包含新信息),丢弃整个日志。
- 使用快照内容重置状态机(并加载快照的集群配置)。
追随者必须拒绝过时的快照,例如,如果它已经拥有一个比接收到的快照更新的快照。InstallSnapshot 的实现必须是原子的,领导者重发快照是无害的。
这种快照方法偏离了 Raft 的强领导者原则,因为追随者可以在领导者不知情的情况下创建快照。但这被认为是合理的,因为在快照创建时,相关的日志条目已经达成共识,不会产生决策冲突。数据流仍然是从领导者到追随者。
- 何时创建快照 :过于频繁会浪费磁盘带宽和能源;过于不频繁则有耗尽存储的风险,并增加重启时的日志回放时间。一个简单的策略是当日志达到固定大小时进行快照。如果快照的大小远小于日志大小,那么快照带来的磁盘带宽开销会比较小。但如果快照和日志大小相当(例如,每次更新都插入唯一的键),快照的价值可能不大,除非快照以更易于访问的方式组织数据(如排序表),从而加速重启。
- 写入快照的耗时 :写入快照可能耗时较长,不应阻塞正常操作。解决方案是使用 写时复制 (
copy-on-write, COW) 技术。例如,基于函数式数据结构的状态机天然支持此功能。或者,操作系统的 COW 支持(如 Linux 上的fork())可用于创建整个状态机的内存快照。fork()通常不会立即复制所有内存,而是将页面标记为“写时复制”,并在父进程或子进程尝试写入页面时才实际复制该页面。 - 网络带宽 :如果状态非常大(如数据库),
InstallSnapshotRPC 会产生高昂的带宽成本。领导者应保留足够的日志以覆盖追随者滞后或临时离线的常见情况,从而减少发送快照的频率。也可以考虑只传输状态差异。 - 数据压缩 :快照的压缩方案取决于服务存储的数据类型。例如,图像可以使用 JPEG 压缩。如果快照间共享大量内容,可以使用树状结构共享节点。
本节描述客户端如何与 Raft 交互,包括客户端如何找到集群领导者以及 Raft 如何支持 线性化语义 (linearizable semantics)。
Raft 的客户端将所有请求发送给领导者。当客户端首次启动时,它会连接到一个随机选择的服务器。如果该服务器不是领导者,它将拒绝客户端的请求,并提供它所知道的最近领导者的信息(AppendEntries 请求包含领导者的网络地址)。如果领导者崩溃,客户端请求将超时;客户端随后会重试其他随机选择的服务器。
Raft 的目标是实现线性化语义,即每个操作都表现为在调用和响应之间的某个点瞬时发生,且仅发生一次。然而,如果领导者在提交日志条目后但在响应客户端之前崩溃,客户端会向新的领导者重试该命令,导致命令被执行两次。
解决方案是让客户端为每个命令分配唯一的序列号。然后,状态机跟踪每个客户端已处理的最新序列号以及相关的响应。如果收到一个序列号已被执行的命令,状态机将立即使用先前保存的响应进行回复,而不会重新执行该请求。
为了维护这个客户端序列号和响应的表(通常称为 会话表 或 重复请求检测表 ):
- 当新的领导者产生时,由于所有副本都会在执行命令时更新它们的这个表,因此新领导者自然拥有了该信息。
- 如果服务器崩溃并重启,通过日志回放可以重建这个表。如果使用了快照,快照必须包含这个表的副本。
即使这个表返回的是一个“旧”的值(例如,在两次 Get 请求之间有一个 Put),只要它符合线性化顺序(例如,旧的 Get 操作在时间上可以被视为在 Put 之前完成),这也是正确的。
只读操作(如 Get(key))理论上可以不写入任何日志就进行处理。然而,若无额外措施,这会有返回陈旧数据的风险。因为响应请求的领导者可能已经被它不知道的新领导者取代,而新领导者可能已经处理了对该键的 Put 操作,导致旧领导者的数据过时。返回陈旧数据违反了线性化语义。
Raft 需要两个额外的预防措施来保证只读操作的线性化,而不必将它们写入日志:
- 领导者必须拥有关于哪些条目已提交的最新信息 : 领导者完整性 (
Leader Completeness) 属性保证领导者拥有所有已提交的条目,但在其任期开始时,它可能不知道哪些条目是(在其看来)已提交的。为了解决这个问题,每个领导者在其任期开始时,会向日志提交一个空的 无操作 (no-op) 条目。一旦这个no-op条目被提交,领导者就能知道其之前的所有条目也都提交了。这个no-op操作仅在领导者任期开始时发生一次,而不是针对每个只读请求。这是为了解决图 8 中描述的情况:一个旧的日志条目可能已复制到多数服务器,但仍可能被未来的领导者覆盖,除非当前领导者提交了其当前任期的一个条目(例如no-op)。 - 领导者在处理只读请求前必须检查自己是否已被罢黜 :它的信息可能因为一个更新的领导者已被选举出来而变得陈旧。Raft 通过让领导者在响应只读请求前,与集群中的多数派交换一次心跳消息(例如,发送空的
AppendEntriesRPCs 并等待回复)来处理此问题。这可以确认它仍然是领导者。
另一种方法是,领导者可以依赖心跳机制来提供一种形式的 租约 (lease)。在获得 AppendEntries 的多数确认后,领导者被授权在一个租约期内响应只读请求,而无需将这些只读请求提交到日志。新的领导者在执行写入操作前,必须等待前一个领导者的租约到期。然而,这种方法依赖于时钟同步来保证安全性(例如,它假设有界的时钟偏斜)。在教学实验中,通常要求将 Get() 操作提交到日志中,而不实现租约机制。
Raft 算法已经在 RAMCloud 项目中作为复制状态机的一部分被实现,用于存储配置信息和协助协调器故障转移。该 C++ 实现大约有 2000 行代码(不包括测试、注释或空行)。此外,还有许多第三方的开源实现。
为了衡量 Raft 相对于 Paxos 的可理解性,研究者们进行了一项用户研究。结果显示,学生们在 Raft 上的表现明显优于 Paxos 。大多数参与者认为 Raft 更容易实现和解释。Raft 的设计侧重于分解问题(如领导者选举、日志复制、安全性)和减少状态空间,这使得算法更易于理解。
Raft 的核心共识机制(Section 5)已经被形式化规约并证明了其安全性。使用 TLA+ 语言编写的正式规约约 400 行,并基于此规约,使用 TLA 证明系统机械证明了 日志完整性 (Log Completeness) 属性。此外,还编写了一份完整的关于 状态机安全性 (State Machine Safety) 属性的非正式证明。
Raft 的性能与其他共识算法(如 Paxos)相当。在最重要的场景——已建立的领导者复制新的日志条目时,Raft 使用最少数量的消息(从领导者到集群半数成员的一次往返)。
领导者选举过程通过随机化的选举超时能够快速收敛。实验表明,即使是很小范围的随机化(例如,150-155ms 的选举超时对比固定的 150ms)也能显著减少分裂投票,从而加快选举。通过降低选举超时可以减少领导者崩溃后的停机时间,但过低的超时(例如低于广播时间一个数量级)会导致不必要的领导者变更,降低系统整体可用性。论文推荐使用保守的选举超时,如 150-300ms 。
Raft 与许多现有的共识算法和系统有相似之处,但也存在关键区别。
- 与 Paxos 的比较 :Raft 与 Paxos 最显著的区别在于其 强领导者 (
strong leader) 模型。Raft 将领导者选举作为共识协议的核心部分,并将尽可能多的功能集中在领导者身上。这使得算法更简单、更易于理解。相比之下,Paxos 中的领导者选举是正交的,仅作为性能优化,导致了额外的机制(两阶段协议 + 独立选举机制)。Raft 将领导者选举整合为共识的第一阶段,机制更少。此外,Paxos 的原始描述侧重于单决策 Paxos,扩展到多决策 (multi-Paxos) 缺乏统一的、详尽的描述,给实践带来了困难。 - 与 Viewstamped Replication (VR) 和 ZooKeeper (Zab) 的比较 :VR 和 ZooKeeper 也是基于领导者的方法,因此共享 Raft 相对于 Paxos 的许多优点。然而,Raft 通过最小化非领导者节点的功能,使得其机制比 VR 或 ZooKeeper 更少。例如,Raft 中的日志条目仅单向从领导者流向追随者;而在 VR 中,日志条目可以在选举期间流向领导者,增加了复杂性。Raft 的消息类型也相对较少。
- 性能优化与其他方法 :一些无领导者的方法,如 Egalitarian Paxos (EPaxos),在特定条件下(如利用状态机命令的交换律且并发冲突少时)可以获得比 Raft 更高的性能和更好的负载均衡,尤其是在广域网环境中。但 EPaxos 显著增加了 Paxos 的复杂性。
- 成员变更机制 :Raft 的 联合共识 (
joint consensus) 成员变更机制利用了共识协议的其余部分,所需额外机制很少。与 VR 和 SMART 等机制相比,Raft 的配置变更可以在不限制正常请求处理的情况下进行,而 VR 在配置变更期间会停止所有正常处理。
Raft 通过其独特的设计,在可理解性和实践性之间取得了良好的平衡,并对后续的复制状态机研究和实现产生了积极影响。
算法设计通常以正确性、效率和简洁性为主要目标。然而,论文作者认为 可理解性 (understandability) 同等重要。开发者需要深刻理解算法才能将其可靠地实现并扩展。
Raft 作为 Paxos 的一个更易于理解的替代方案被提出。它不仅被证明比 Paxos 更易学习,同时也为构建实际系统提供了更好的基础。将可理解性作为首要设计目标,改变了 Raft 的设计方法,使得设计者能够反复运用如问题分解和状态空间简化等技术。这些技术不仅提升了 Raft 的可理解性,也使其正确性更易于得到保证。
通过对 Raft 论文后半部分的深入分析,我们可以看到 Raft 不仅仅是一个理论上的共识算法,更是一个充分考虑了实际系统需求和开发者体验的完整方案。其在日志压缩、客户端交互、成员变更等方面的设计,都体现了对简洁性和可理解性的追求,同时保证了系统的正确性和高效性。