- 分散メッセージングシステム NATS JetStream の耐久性と一貫性を、Jepsen がさまざまな障害条件で検証
- テスト結果、ファイル破損(
.blk、スナップショット) および 電源障害のシミュレーション でデータ損失と スプリットブレイン現象 が発生
- JetStream はデフォルトで
fsync を2分ごとに実行するため、直近で承認されたメッセージがディスクに書き込まれない状態 で残る可能性がある
- 単一ノードの OSクラッシュ だけでもデータ損失とレプリカ不一致を引き起こす可能性がある
- Jepsen は NATS に対し
fsync=always のデフォルト変更、または データ損失リスクの明示的なドキュメント化を推奨
1. 背景
- NATS はメッセージをストリームとして発行・購読する人気のストリーミングシステム
- JetStream は Raft合意アルゴリズムを使ってデータを複製し、少なくとも1回(at-least-once) の配信を保証
- JetStream はドキュメント上で Linearizable一貫性と常時可用性を主張しているが、CAP定理上この2条件は同時には満たせない
- NATS のドキュメントでは3ノードストリームは1台、5ノードストリームは2台のサーバ損失に耐えられるとしている
- メッセージは、サーバが
publish リクエストをacknowledgeした時点で「正常に保存された」とみなされる
- データ一貫性には 過半数(クォーラム) ノードが必要で、5ノードクラスタでは最低3台が稼働している必要があることで新規メッセージの保存が可能
2. テスト設計
- Jepsen は JNATS 2.24.0 クライアントと Debian 12 LXC コンテナ環境でテストを実施
- 一部のテストでは Antithesis 環境で公式の NATS Docker イメージを使用
- 単一 JetStream ストリーム(レプリケーション5)を構成し、プロセス停止・クラッシュ・ネットワーク分断・パケットロス・ファイル破損などを注入
- LazyFS ファイルシステムを使って
fsync されていない書き込みを失う 電源障害のシミュレーションを実施
- 各プロセスはユニークなメッセージを発行し、テスト終了後に全ノードでacknowledged メッセージの存在有無を検証
- メッセージが一部ノードにしか存在しない場合は divergence(複製不一致) と分類
3. 主要結果
3.1 NATS 2.10.22 の全面データ損失(#6888)
- 単純なプロセスクラッシュだけで JetStream ストリーム全体が消える現象を発見
"No matching streams for subject" エラー発生後、数時間にわたり復旧しなかった
- 原因は リーダースナップショットの逆転、Raft状態削除など で、2.10.23 で修正済み
3.2 .blk ファイル破損時のデータ損失(#7549)
- JetStream の
.blk ファイルで 1ビットエラーや切り詰め(truncation) が起きると、数十万件の承認済み書き込みが失われる
- 例: 1,367,069 件中 679,153 件損失
- 一部ノードだけが破損しても 大規模なデータ損失とスプリットブレイン が発生
- 例: ノード
n1、n3、n5 で最大 78% のメッセージ損失
- NATS は当該問題を調査中
3.3 スナップショットファイル破損時の全データ削除(#7556)
data/jetstream/$SYS/_js_/ 内のスナップショットファイルが破損すると、ノードはストリームを 孤立(orphaned) と見なして データを全削除 する
- 少数ノードの破損だけでも クラスタが過半数を構成できず、ストリームが永続的に利用不可 になる
- 例: ノード
n3、n5 破損 → n3 がリーダーに選出され、jepsen-stream が全削除
- Jepsen はリーダー選出時に 破損ノードがリーダーになるリスク を指摘
3.4 デフォルト fsync 設定によるデータ損失(#7564)
- JetStream はデフォルトで 2分ごとにのみ
fsync を実施し、メッセージは即時承認
- 結果として、直近の承認済みメッセージがディスクに書き込まれない状態で残る
- 電源障害やカーネルクラッシュ時に数十秒分の承認済みメッセージが失われる
- 単一ノード障害が連続して発生した場合でも全ストリーム削除が起こりうる
- 仕様書にはこの動作がほとんど言及されていない
- Jepsen は
fsync=always のデフォルト変更、またはデータ損失リスクの明確な警告を推奨
3.5 単一 OS クラッシュによるスプリットブレイン(#7567)
- 単一ノードの 電源障害またはカーネルクラッシュ だけでも データ損失と複製不一致 が発生しうる
- リーダー・フォロワー構成で、あるノード群がメモリ上でのみコミットされた状態で承認後に障害が起きると、
多数のノードがその書き込みを失い、新しい状態へ進む
- テストでは、単一電源障害後に 継続的なスプリットブレイン が発生
- Jepsen は Kafka の類似事例を引用し、Raftベースのシステムでも同じリスクが存在することを強調
4. 議論と結論
- 2.10.22 の全面データ損失問題は 2.10.23 で解決
- 2.12.1 では ファイル破損および OS クラッシュ による データ損失・スプリットブレイン がなお発生
.blk とスナップショットファイル破損時に、一部ノードでメッセージ欠損または全ストリーム削除が起こる
- デフォルトの
fsync 周期が長いため、複数ノード同時障害時に承認済みデータを失うリスクがある
- Jepsen は
fsync=always 設定、またはドキュメント内の明確なリスク告知を提案
- JetStream の「常時可用」は CAP定理上不可能 で、ドキュメントの修正が必要
- Jepsen は バグの存在自体は確認できるが、安全性の欠如を証明することはできない と明記
4.1 LazyFS の役割
- LazyFS を使って
fsync されていない書き込み損失をシミュレート
- 電源障害時の 部分書き込み破損(torn write) など、多様なストレージエラーを再現可能
- 関連研究 When Amnesia Strikes(VLDB 2024) では PostgreSQL、Redis、ZooKeeper などでも類似バグが報告された
4.2 今後の課題
- 単一コンシューマレベルのメッセージ損失、メッセージ順序、Linearizable/Serializable 保証は未検証
- 正確に1回(exactly-once)配信保証も将来の研究対象
- ノード追加・削除時のドキュメントの誤りと、必須 health check 手順の欠落を発見(#7545)
- 安全なクラスタ再構成手順は依然として不明瞭
1件のコメント
Hacker Newsの意見
そのうちAIがプロジェクトのドキュメントを読んで、データ損失の可能性をマーケティング文句だけで予測できるようになるのかも気になる
人はいつも「理論は過大評価されている」とか「学校教育よりハッキングのほうがいい」と言うが、結局は文書化された問題空間で自分の足を引っ張ることになる
微妙なスケーリングのディテールもうまく処理していた
ただ、persistenceは使ったことがなく、ここまで脆弱だとは思わなかった
単一ビットのファイル破損にも弱いとは驚きだ
参考にするのにとても良い資料だ → Jepsen Glossary
最近 aphyr.com を見つけたばかりで、洞察が多そうだと期待している
その後 jepsen.io は専門プロジェクトへと発展し、約10年前から本格的に運営され始めた
ベンチマーク性能を上げるためなのか? 小規模クラスタではこうした設定が問題の原因になることが多い
多くのアプリケーションは完全な耐久性を必要としないため、lazy fsync は有用になり得る
ただし、デフォルト値にするのは議論の余地がある
TCP corking のように**バッチ処理(batch)**で解決できそうに思える
lazy fsync による失敗は、ほとんどのノードで同時には起きないからだ
利点: オブジェクトストレージ級の耐久性を持つ無制限ストリームをサポート
欠点: まだconsumer group機能がない
複数ノードが同時に障害を起こすと、コミット済みデータの損失が発生し得る
MongoDB初期の「ウェブスケール」マーケティングを思い出す
デフォルト値は常に最も安全な選択肢であるべきだと思う
その点はむしろ好ましく、その上にシステムを設計できた
2018年に使ったときは性能も良く、管理もしやすかった
たとえば PostgreSQL のデフォルトのトランザクション分離レベルはread committedだ
Redis もデフォルトでは1秒ごとに fsync する
standalone Redis でも fsync 後 ack の設定は可能だが、OSバッファリングのため完全保証は難しい
結局のところ、ackの意味を正確に理解することが重要だ
安全なデフォルト値だけにこだわると性能が大きく落ち、ユーザーが自分でチューニングしなければならない負担も増える
たとえば Postgres のデフォルト分離レベルも弱いため、race conditionが発生し得る
参考: Hermitageテスト記事
SSD時代には group-commit のような中間段階が消え、今ではsyscall切り替えコストがボトルネックになっている
2分は長すぎる周期だ(fdatasync と fsync の違いも考慮すべき)
素直にRedpandaを使うほうがよさそうだ
一定周期で**バッチフラッシュ(batch flush)**を行えば、レイテンシは増えるがスループットは維持できるのではないかと思う
これはPaxosラウンドを束ねる方式に似ている
1ラウンドが終わったらすぐ次のバッチを始める形で処理すべきだ