1 ポイント 投稿者 GN⁺ 2025-12-09 | 1件のコメント | WhatsAppで共有
  • 分散メッセージングシステム 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 件損失
  • 一部ノードだけが破損しても 大規模なデータ損失とスプリットブレイン が発生
    • 例: ノード n1n3n5 で最大 78% のメッセージ損失
  • NATS は当該問題を調査中

3.3 スナップショットファイル破損時の全データ削除(#7556)

  • data/jetstream/$SYS/_js_/ 内のスナップショットファイルが破損すると、ノードはストリームを 孤立(orphaned) と見なして データを全削除 する
  • 少数ノードの破損だけでも クラスタが過半数を構成できず、ストリームが永続的に利用不可 になる
  • 例: ノード n3n5 破損 → n3 がリーダーに選出され、jepsen-stream が全削除
  • Jepsen はリーダー選出時に 破損ノードがリーダーになるリスク を指摘

3.4 デフォルト fsync 設定によるデータ損失(#7564)

  • JetStream はデフォルトで 2分ごとにのみ fsync を実施し、メッセージは即時承認
    • 結果として、直近の承認済みメッセージがディスクに書き込まれない状態で残る
  • 電源障害やカーネルクラッシュ時に数十秒分の承認済みメッセージが失われる
    • 例: 930,005件中 131,418件損失
  • 単一ノード障害が連続して発生した場合でも全ストリーム削除が起こりうる
  • 仕様書にはこの動作がほとんど言及されていない
  • 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件のコメント

 
GN⁺ 2025-12-09
Hacker Newsの意見
  • 誰かが複雑な理論を飛ばしてこういうシステムを作るたびに、aphyrがそれを打ち砕くのを見ている
    そのうちAIがプロジェクトのドキュメントを読んで、データ損失の可能性をマーケティング文句だけで予測できるようになるのかも気になる
    • 長いひげを撫でながらうなずく気分だ
      人はいつも「理論は過大評価されている」とか「学校教育よりハッキングのほうがいい」と言うが、結局は文書化された問題空間で自分の足を引っ張ることになる
    • 自分もLLMに似たようなことをやらせてみたが、結果はかなり有用だった
  • NATSはCAP定理を無視しているように感じる
    • 過小評価された発言だと思う
  • 自分はNATSをin-memory pub/sub用途で使ってきたが、その点では素晴らしかった
    微妙なスケーリングのディテールもうまく処理していた
    ただ、persistenceは使ったことがなく、ここまで脆弱だとは思わなかった
    単一ビットのファイル破損にも弱いとは驚きだ
  • 関連資料として、JepsenとAntithesisが最近分散システム用語集を公開した
    参考にするのにとても良い資料だ → Jepsen Glossary
  • aphyr.com/tags/jepsen と jepsen.io/analyses のコンテンツの違いが気になっていた
    最近 aphyr.com を見つけたばかりで、洞察が多そうだと期待している
    • Jepsenはもともと個人ブログシリーズとして始まった
      その後 jepsen.io は専門プロジェクトへと発展し、約10年前から本格的に運営され始めた
  • 「Lazy fsync by Default」設定がなぜ存在するのか疑問だ
    ベンチマーク性能を上げるためなのか? 小規模クラスタではこうした設定が問題の原因になることが多い
    • 単にレイテンシだけでなく、スループット(throughput) の向上もある
      多くのアプリケーションは完全な耐久性を必要としないため、lazy fsync は有用になり得る
      ただし、デフォルト値にするのは議論の余地がある
    • fsync をなぜ必ず遅延させなければならないのか、ずっと疑問だった
      TCP corking のように**バッチ処理(batch)**で解決できそうに思える
    • 分散システムだからこそ可能なことの一つだ
      lazy fsync による失敗は、ほとんどのノードで同時には起きないからだ
    • パフォーマンス向上のための選択であることは確かだ
    • 複製と分散による耐久性と、lazy fsync で確保したスループットの両方を狙っている
  • JetStreamのサーバーレス代替として s2.dev を勧める
    利点: オブジェクトストレージ級の耐久性を持つ無制限ストリームをサポート
    欠点: まだconsumer group機能がない
    • Jepsenテストを回したことがあるのか気になる
  • NATSがデフォルトで2分ごとにしか fsyncを行わず、即座に ack を返す点が問題だ
    複数ノードが同時に障害を起こすと、コミット済みデータの損失が発生し得る
    MongoDB初期の「ウェブスケール」マーケティングを思い出す
    デフォルト値は常に最も安全な選択肢であるべきだと思う
    • NATSはクラスタ可用性だけを保証すると明確に述べている
      その点はむしろ好ましく、その上にシステムを設計できた
      2018年に使ったときは性能も良く、管理もしやすかった
    • たいていの現代的なDBも、デフォルト値が完全に安全というわけではない
      たとえば PostgreSQL のデフォルトのトランザクション分離レベルはread committed
      Redis もデフォルトでは1秒ごとに fsync する
    • Redisクラスタは多数ノードに複製された後にのみ ack を返す
      standalone Redis でも fsync 後 ack の設定は可能だが、OSバッファリングのため完全保証は難しい
      結局のところ、ackの意味を正確に理解することが重要だ
    • ほとんどのシステムは速度と耐久性のトレードオフを選ぶ
      安全なデフォルト値だけにこだわると性能が大きく落ち、ユーザーが自分でチューニングしなければならない負担も増える
      たとえば Postgres のデフォルト分離レベルも弱いため、race conditionが発生し得る
      参考: Hermitageテスト記事
    • fsync は極端な選択肢しかないのが問題だ
      SSD時代には group-commit のような中間段階が消え、今ではsyscall切り替えコストがボトルネックになっている
      2分は長すぎる周期だ(fdatasync と fsync の違いも考慮すべき)
  • 正直ある程度は予想していたが、ここまで深刻だとは思わなかった
    素直にRedpandaを使うほうがよさそうだ
  • NATSの fsync 性能上の警告を改善できないか気になっていた
    一定周期で**バッチフラッシュ(batch flush)**を行えば、レイテンシは増えるがスループットは維持できるのではないかと思う
    • 固定周期ではなく、fsync が進行中の間は書き込み要求をキューイングし、次のバッチでまとめて処理すればよい
      これはPaxosラウンドを束ねる方式に似ている
      1ラウンドが終わったらすぐ次のバッチを始める形で処理すべきだ