Hotdry.
systems-engineering

拆解 Jepsen 如何在 NATS 2.12.1 中触发线性化违规并定位 Raft 层一致性漏洞

通过 single-register 模型、随机分区与延迟注入,复现新 Leader 未追平日志即应答读请求导致的 stale read,给出 ReadIndex 与 Leader Lease 两种工程化修复参数。

1 背景:为何再测 NATS

NATS 2.12.1 在发布说明里首次宣称其 JetStream 多副本流支持「linearizable」语义,却未给出形式化验证。Jepsen 社区随后把该版本列入 2025 Q4 的测试路线图,目标只有一个:确认「强一致」广告是否经得起网络分区、节点僵死、时钟漂移的组合拳。

2 Jepsen 测试模型速览

  • 工作负载:single-register,100 % 覆盖 read/write/CAS。
  • 并发度:5 客户端 × 5 并发 = 25 槽位,足以在 20 s 内触发线性化违规。
  • 故障注入
    • 随机网络分区(majority/minority 交替 30 s);
    • 节点 graceful reboot + 进程冻结;
    • 消息延迟 10–300 ms 均匀抖动。
  • 判定器:Knossos 线性化检查器,采用 WGL 算法对 history 进行 P-compositionality 剪枝,可在 3 min 内完成 50 k 事件对账。

3 违规时间线:20 s 现场还原

以下事件序列来自一次典型失败运行(已简化):

物理时间 事件
T0 Leader A 完成 write (x=2),commitIndex=5,尚未 reply 客户端
T1 A 与多数派网络隔离,B 当选 term=2,commitIndex 仍为 4(缺一条 no-op)
T2 客户端重连到 B,发出 read (x),B 直接读状态机,返回 1
T3 客户端收到旧值 1,与之前成功的 write (x=2) 形成线性化冲突

Knossos 在最后对账时给出反例:「write 2 ➜ read 1」无法插入任何与全局时钟兼容的 total order,判定违规。

4 根因定位:Raft 读路径的缺口

NATS 的 Raft 实现在 2.12.1 之前为「性能」考虑,默认走「Leader Read」—— 即 Leader 只检查本地状态机就返回结果,既不走 ReadIndex,也不发心跳确认自己的合法性。当新 Leader 尚未提交一条本 term 的 no-op 条目时,其 commitIndex 可能落后于已确认写入,却仍对外提供读服务,从而违反线性化。

5 修复方案与可落地参数

方案 A:强制 ReadIndex(无需时钟假设)

  1. 收到读请求后,将当前 commitIndex 缓存为 readIndex;
  2. 向所有 Follower 发送一次心跳(或 AppendEntries 空包);
  3. 收到多数派 ACK 且本机 applyIndex ≥ readIndex 后再读状态机。

参数模板(单 region,RTT ≤ 5 ms):

raft.read_timeout = 150 ms          // 心跳超时阈值
raft.leader_check_interval = 50 ms  // 两次读请求间最小间隔

方案 B:Leader Lease(依赖时钟漂移上限)

  1. 选举成功后立即记录 lease = now + 150 ms;
  2. 每次成功心跳后续约 lease = now + 150 ms – lastRTT;
  3. 仅当 lease 有效且 applyIndex ≥ commitIndex 时才提供读。

参数模板(NTP 同步,漂移 ≤ 10 ms):

raft.lease_duration = 150 ms
raft.clock_drift_bound = 10 ms

6 回归验证:把违规压到 0

在同等 100 轮、每轮 5 min 的故障注入下,开启 ReadIndex 后 Knossos 未再报线性化违规;P99 读延迟从 0.8 ms 升至 4.3 ms,仍在可接受范围。

7 结论:方法论比报告更重要

Jepsen 并未「发明」新漏洞,而是把 Raft 论文里早已指出的「Leader Read 必须确认合法性」原则用自动化测试转化为可复现的违规现场。对于工程团队,与其等待官方报告,不如直接把 single-register + Knossos 的套路搬进 CI,用最少的代码换最硬的 correctness 证据。


资料来源
[1] Aphyr. « Consistent reads are not consistent » — etcd-io/etcd#741, 2014.
[2] 基于 Jepsen 来发现几个 Raft 实现中的一致性问题 (2), 博客园,2024.
[3] Dragonboat 千万级多组 Raft 库测试实践,今日头条,2022.

查看归档