Hotdry.
systems-engineering

用 Jepsen 验证 NATS JetStream 线性一致性:可复现的测试模型与调参经验

基于 NATS 2.12.1 与 Jepsen 框架,给出验证 JetStream 线性一致性的完整工程流程、模型选择、参数调优与故障注入 checklist。

一、目标与边界

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。

寄存器模型约束:

  1. 读必须返回「最近一次写」的值;
  2. 写必须原子生效;
  3. 操作之间实时顺序不能颠倒。

任何违反都会被 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」的轮次:

  1. 10 s 健康基线,无任何故障;
  2. 5 s partition-majority,把 leader 隔离到少数派;
  3. 5 s pause-leader,SIGSTOP 挂起 leader 进程;
  4. 5 s kill-follower,强制 follower 重启;
  5. 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

查看归档