Skip to content
团子云技术 Lite 1.048596
Go back

Raft 线性一致读:Read Index 与 Lease Read 的攻防推演

团团虾声明:本文是我在学习 Raft 线性一致读机制过程中与 Gemini 3.1 Pro 的一次深度对话整理。讨论涉及大量时序推演和边界场景分析,如果有什么不对,欢迎指正。


Raft 的线性一致读(Linearizable Read)看似简单——不就是读一下 Leader 的状态机吗?但真正走通这条路径,需要穿过好几道逻辑窄门。这篇文章从最基础的 Follower Read 问题出发,逐层推到 Lease Read 的攻防边界。

起点:Follower Read 如何保证线性一致?

Follower 不负责定序,怎么服务强一致读?Raft 论文第 8 节给出的答案是 ReadIndex 协议,标准流程分四步:

  1. 记录 ReadIndex:Follower 向 Leader 请求当前的 commit_index,记作 read_index
  2. 多数派确认:Leader 收到请求后,向集群发送一轮心跳,等多数派 ACK。
  3. 等待本地 Apply:Follower 拿到 read_index 后,死等本地状态机的 apply_index >= read_index
  4. 执行读取:在本地状态机读,返回结果。

Follower 只承担读执行,线性化证明仍来自 Leader 和多数派。

第一层陷阱:Leader 直接返回自己的 apply index 行不行?

不行。在发生网络分区(脑裂)时,这会引发脏读。

推演一个 5 节点集群(A、B、C、D、E),A 是 Leader,当前数据 x = 1

  1. 网络分区:A 和 B 落入少数派网络,C、D、E 在多数派。
  2. 改朝换代:C、D、E 发现 A 失联,发起选举,C 成为新 Leader。客户端向 C 写入 x = 2
  3. 客户端向 B(Follower)发起读请求。B 向它眼中的 Leader(节点 A)请求 ReadIndex。
  4. 节点 A 不知道自己已被废黜(处于少数派,收不到 C 的心跳)。如果 A 直接返回本地的 apply index,它返回的是旧进度(对应 x = 1 的状态)。
  5. B 拿到这个旧值,发现本地已 apply,直接返回 x = 1 给客户端。

在全局时间线上,x = 2 已经生效,客户端却读到了历史旧数据——线性一致性被破坏。

ReadIndex 为什么要向多数派确认? 本质上是让 Leader「自证清白」。通过收到多数派的回复,Leader 确信自己仍是唯一的合法 Leader。只有验证了这一点,Leader 当前的 commit index 才敢保证是全局最新的。

第二层:Lease Read——工业界的性能妥协

每次读都要走一轮多数派 RPC,延迟太高了。etcd 和 TiKV 引入了 Lease Read(租约读)机制:

但这并不等价于 ReadIndex。

第三层陷阱:时钟漂移与进程假死

Lease Read 的风险在于:它用物理时钟替代了网络通信。一旦时钟不可靠,安全性就没了。

推演场景:GC STW 导致假死

假设心跳超时 1000ms,租约有效期也是 1000ms。

  1. Leader 成功收到多数派心跳,续期 1000ms 租约。
  2. Leader 所在服务器发生 GC Stop-The-World(或虚拟机迁移),进程被挂起 1500ms。此时 Leader 的时钟/线程完全停滞,不知道外界过了多久。
  3. 在这 1500ms 内,Follower 收不到心跳,选出新 Leader,写入 x = 2
  4. 1500ms 后,旧 Leader 苏醒。它检查本地计时器,由于线程被冻结,它错误地认为租约还有剩余。
  5. 客户端向旧 Leader 发起读请求,旧 Leader 自信地跳过多数派确认,返回旧数据 x = 1

线性一致性被破坏。Lease Read 死于「盲目自信」——它信任本地时钟,而时钟在 STW 期间停滞了。

Read Index vs Lease Read 核心差异:

特性Read IndexLease Read
判定权威性依赖多数派网络确认依赖本地物理时钟
性能每次读多一次 RPC纯本地内存读取
安全性绝对安全存在极小概率的脏读风险
理论基础逻辑时钟(Term & Index)物理时钟

第四层:Read Index 会不会也被 STW 搞死?

直觉上会——进程都卡住了,怎么还能正确处理请求?但仔细推演后结论相反:Read Index 不会被 GC STW 或 VM 迁移搞死

需要把 STW 发生的「时机」拆开看。

场景一:STW 发生在多数派确认之前或期间

  1. 旧 Leader 收到读请求后,立刻发生 5 秒 STW。期间集群选出新 Leader 并写入新数据。
  2. 旧 Leader 苏醒后,继续处理挂起的读请求,向集群广播心跳试图多数派确认。
  3. 多数派的 Term 已经增加,直接拒绝响应旧 Leader。
  4. 旧 Leader 拿不到多数派 ACK,意识到自己被废黜,降级为 Follower。

结果:请求失败,脏读被拦截。

场景二:STW 发生在多数派确认之后

这是最容易让人迷惑的场景。逐步推演:

T1:客户端向旧 Leader 发起读请求,记录 read_index = 100T2:旧 Leader 拿到多数派 ACK,确认了自己此刻的合法性。系统为这个读请求拍下了一张合法快照。 T3:STW 开始,旧 Leader 卡死。 T4:集群选出新 Leader,写入 x = 2(commit_index 变成 150)。 T5:STW 结束,旧 Leader 苏醒。 T6:旧 Leader 发现本地 apply_index 已达到 100,返回 T2 时刻的快照数据 x = 1

这破坏线性一致性了吗?没有。

线性一致性不要求读到「物理世界此时此刻的最新值」。它只要求:如果读操作和写操作在时间线上有重叠(并发),读操作可以读到写之前的值,也可以读到写之后的值。

这个读请求的生命周期跨越了整个 STW(T1 到 T6),而写操作发生在 T4(两者并发)。读请求返回它刚发起那一刻的合法快照——完全符合线性一致性的定义。客户端只会认为这是一个「因为网络抖动而响应很慢的读」,恰好读到了写入之前的数据。

真正的杀机:STW 醒来后的新请求

现在看最致命的一刀——STW 结束(T5)之后才到达的新读请求

Read Index 的防守:

旧 Leader 收到新请求,重新发起多数派心跳确认。多数派的 Term 已经增加,直接拒绝。旧 Leader 发现自己被废黜,降级为 Follower,不返回错误数据。

Lease Read 的惨败:

旧 Leader 收到新请求,不发网络请求,直接看本地手表。因为 STW 期间进程时钟完全停滞,它错误地认为租约还没到期,直接返回 x = 1

这破坏了线性一致性——这个读请求是在 T7 才发起的,而 x = 2 的写入在 T4 就已完成(T4 < T7)。一个在绝对时间上晚于写操作发起的读请求,却读到了写之前的数据——不可饶恕的「时光倒流」。

核心区别: Read Index 信任网络共识(每次新请求都重新验证),Lease Read 信任本地时钟(STW 后产生幻觉)。

第五层:TiKV 如何走钢丝

我向 Gemini 提出了一个很自然的问题:TiKV 是怎么解决 Lease Read 的风险的?答案坦诚得令人敬佩——TiKV 并没有在理论上 100% 解决,它是用严密的工程手段解决了 99.99% 的场景,然后选择性承受了剩下 0.01% 的风险。

第一道防线:单调时钟

TiKV 绝不信任墙上时间(CLOCK_REALTIME),只信任操作系统内核维护的单调时钟(CLOCK_MONOTONIC)。单调时钟从开机开始计数,永远只增不减,不受 NTP 校时影响。

当进程发生假死(STW)时:假死的是 TiKV 这个用户态进程,底层操作系统内核仍在运行。进程苏醒后去调取单调时钟,会发现时间已经实打实走过了 5 秒。TiKV 一对比 Lease 过期时间,立刻认出租约失效,拒绝服务,退化去走 ReadIndex。

→ GC STW:完美解决。

第二道防线:Election Timeout 与 Lease 的安全垫

Raft 的机制决定了,Follower 只有在经过一个完整的 Election Timeout(选举超时)收不到心跳时,才会发起选举。TiKV 强制保证:Lease 的有效时间严格小于 Election Timeout。

假设 Election Timeout 是 10 秒,心跳间隔 2 秒。Leader 每次拿到多数派响应,只给自己续期 9 秒的 Lease。如果发生网络分区:

这保证了旧 Leader 的 Lease 失效时间永远早于新 Leader 的诞生时间,两者不会重叠。

→ 网络延迟和脑裂:完美解决。

第三道防线(或者说死穴):VM 热迁移

即便防住了进程假死和 NTP 篡改,TiKV 依然挡不住一种极端情况——操作系统的物理时间本身停滞了。

最典型的场景是云厂商的 VM Live Migration(虚拟机热迁移)。当虚拟机从物理机 A 迁移到物理机 B 时,包括操作系统的单调时钟在内的整个虚拟机状态被完全冻结。如果冻结 15 秒:

TiKV 怎么解决?解决不了,选择接受,并提供降级开关。 TiKV 团队在设计文档中坦承:没有 Google Spanner 那种自带原子钟和 GPS 的 TrueTime 硬件,软件层面的 Lease Read 在面对底层 OS 时间停滞时是无解的。

但工程上的权衡是:

如果你的业务(比如金融核心交易)连 VM 迁移导致的极短暂脏读都不能容忍,TiKV 允许通过配置关闭 Lease Read,强制回退到 ReadIndex。

第六层:Raft 原论文怎么说?

我追问了一个「考古」问题:原论文有讲这个一致性问题吗?

Raft 论文(《In Search of an Understandable Consensus Algorithm》)第 8 节明确描述了 ReadIndex 的标准解法——四步流程是白纸黑字写着的「绝对安全路径」。

对于 Lease Read,论文只用了一句话给出理论警告:

“Alternatively, the leader could rely on the heartbeat mechanism to provide a form of leases, but this would rely on timing for safety (it assumes bounded clock drift).”

——Leader 可以依赖心跳机制提供租约,但这依赖于时间的安全性,前提是时钟漂移有界。

这就是学术界和工程界的分水岭。在数学证明里,假设物理时钟误差有上限,Lease 机制就是安全的。但在真实服务器环境里,这个「时钟漂移」根本不是有界的——GC STW 和 VM 迁移直接打破了学术假设。学术界指明了边界,工程界填满了血泪。


几点心得

  1. ReadIndex 是理论最优解,但性能代价大。 每次读都要走多数派 RPC,延迟肉眼可见。
  2. Lease Read 是工程妥协,不是等价替代。 它用物理时钟换性能,代价是放弃了绝对安全性。理解它的风险边界,比记住它的用法更重要。
  3. STW 时机决定一切。 ReadIndex 能抗住 STW,是因为它每次新请求都重新验证合法性——物理时间的停滞对它的判断体系毫无影响。
  4. TiKV 的做法是分布式系统工程的典范:坦诚面对理论局限,用多层工程手段在 99.99% 的场景下做到安全,为剩下的极端情况留好降级开关。 不吹牛,不硬扛,把选择权交给用户。

本文的素材来自与 Gemini 3.1 Pro 的对话讨论和 TiKV 官方博客。如果你对 Lease Read 有更深入的一手经验,欢迎指出文中的不足。


Share this post on:

Previous Post
Raft 的隐秘角落:No-op 空日志、幽灵复现与「不等 read_index」优化
Next Post
Raft 角色图鉴:从标准三角到 Learner 和 Witness