一、目标与边界
NATS 2.12.1 将 JetStream 的 Raft 组从 5 节点缩减到 3 节点即可提供强一致写,并默认把读也路由到 leader,从设计上已满足线性一致性的「实时顺序」前提。但设计≠实现:leader 切换、网络分区、follower 读等场景仍可能打破线性化。本文把 Jepsen 的验证方法拆成「可复现模板」,让你用 1 人日就能跑完第一轮冒烟测试,并能在 CI 中每日回归。
验证范围限定在单 Stream 单 Key 的寄存器语义(single-register),即:
- 操作仅两种:
write(value)/read()→value - 一致性模型:Linearizable(非 Serializability)
- 故障维度:kill/pause/partition/clock-skew
二、模型选择:为什么 single-register 够用
JetStream 的 KV Bucket 底层就是一条 Stream,每条消息对同一个 Key 的更新天然形成顺序日志。把 Key 抽象成寄存器,即可直接套用 Jepsen 内置的 knossos.model/register;若需要多 Key 交叉,则换 multi-register 即可,无需重复写 Checker。
寄存器模型约束:
- 读必须返回「最近一次写」的值;
- 写必须原子生效;
- 操作之间实时顺序不能颠倒。
任何违反都会被 Knossos 在 History 里找到不可线性化的序列,从而给出最小反例。
三、测试环境 3 节点最小闭环
硬件:本地 Docker-Compose 即可,CPU 2 核 / 节点,内存 512 MB。生产 CI 建议用 3 台 4C8G 云主机,避免因 CPU 抢跑导致假阳性。
镜像:
nats:2.12.1-alpine
启动参数:
--cluster_name js --cluster nats://0.0.0.0:6222 --http_port 8222 --js
Jepsen 侧:
- Clojure 1.11 + Leiningen 2.10
- jepsen "0.3.3"
- 客户端用 nats.java 2.19.0,连接串
nats://node1:4222,node2:4222,node3:4222, failover 自动切 leader。
四、脚手架:30 行代码跑出 History
(defn nats-register-client
"返回一个 jepsen.client/Client 实现"
[conn-bucket]
(reify client/Client
(setup! [_ test node]
(let [nc (Nats/connect ^String (get-in test [:nats :urls]))
kv (.getKeyValueClient nc (KeyValueOptions/builder
.bucket (:bucket test))
.createOrReplace())]
{:conn nc :kv kv}))
(invoke! [_ test op]
(try
(case (:f op)
:read (let [v (.get (:kv (:conn-bucket @state)) "k")]
(assoc op :type :ok :value (when v (.value v))))
:write (do (.put (:kv (:conn-bucket @state)) "k" (:value op))
(assoc op :type :ok)))
(catch Exception e
(assoc op :type :fail :error (.getMessage e)))))
(teardown! [_ test]
(.close (:conn @state)))))
Generator 用 50% 读 + 50% 写,并发 5 线程,限长 200 操作,即可在 30 s 内生成一段可检查的历史。
五、故障注入节奏:让违反能 “冒” 出来
Nemesis 采用「周期 30 s」的轮次:
- 10 s 健康基线,无任何故障;
- 5 s partition-majority,把 leader 隔离到少数派;
- 5 s pause-leader,SIGSTOP 挂起 leader 进程;
- 5 s kill-follower,强制 follower 重启;
- 5 s clock-skew +100 ms,验证 lease-read 是否安全。
每轮结束后休眠 5 s,让集群自愈,再进入下一轮。经验表明,pause-leader 最容易触发「旧值回退」;若 200 操作内未报 violation,可把操作长度提到 400 再跑一次,复杂度仍可控。
六、Knossos 调优:别让 NP 完全拖垮你
| 参数 | 推荐值 | 说明 |
|---|---|---|
:max-depth |
200 | 单序列最大操作数,再长会 OOM |
:concurrency |
5 | 客户端并发,再多会指数爆炸 |
:model |
register | 单寄存器即可覆盖 90% 场景 |
:anomalies-only |
true | 只输出违反,日志瘦身 |
若出现「timeout after 300 s」说明深度过大,可切成两段 History 分段检查;若「OutOfMemory」则把 JVM 堆提到 4 GB,或把并发降到 3。
七、常见违反模式与根因
| 现象 | 典型日志 | 根因 | 修复提示 |
|---|---|---|---|
| stale read | read k=1, expect 2 |
新 leader 未同步就响应读 | 启用 max_apply_timeout 或 lease-read |
| split-brain write | write k=2 ok, write k=3 ok |
双主同时接受写 | 检查 Raft 选举是否带 pre-vote |
| lost update | read k=nil after write |
写入未落盘即 ack | 开启 sync_always 并压测磁盘 fsync |
八、CI 集成脚本
#!/bin/bash
set -e
lein run test \
--nodes n1,n2,n3 \
--concurrency 5 \
--time-limit 120 \
--ops-per-key 200 \
--nemesis partition,pause,kill,clock \
--bucket jepsen-Linear 2>&1 | tee jepsen.log
if grep -q "Linearizability violations" jepsen.log; then
echo "❌ Failed"; exit 1
else
echo "✅ Passed"; exit 0
fi
每日定时跑, violations=0 才进主干。若失败,Knossos 会输出最小不可线性化序列,直接定位到 ns 级时间戳,方便回放。
九、落地 checklist
- 三节点硬件时钟漂移 < 5 ms, Chrony 每日校正
- JetStream 配置
replicas:3, sync:true, max_apply_timeout: 2s - 客户端开启
retry_on_failed_leader:true,超时 1 s 重试 - 磁盘 fsync 延迟 < 10 ms,避免「ack 后掉电」
- 上线前跑满 7 天 Jepsen 零违反,再逐步切流
十、小结
Jepsen 无法证明 NATS「绝对正确」,但能在工程置信区间内快速证伪。把 single-register 模板、参数上限与故障节奏固化到 CI 后,任何引入 follower 读、lease 优化或 Raft 参数调整的可疑提交,都会被 30 min 内捕获。把这套模板直接拷贝到你的仓库,就能在明天上午跑出第一份「NATS 线性一致性」体检报告。
参考资料
[1] 阿里云技术公众号.《分布式系统一致性测试框架 Jepsen 在女娲的实践应用》2021-10
[2] CSDN.《Xline Jepsen 测试分析》2024-02