1 Punkte von GN⁺ 2025-12-09 | 1 Kommentare | Auf WhatsApp teilen
  • Jepsen validierte die Robustheit und Konsistenz des verteilten Messaging-Systems NATS JetStream unter verschiedenen Fehlerbedingungen
  • Die Testergebnisse zeigten bei Dateibeschädigung (.blk, Snapshot) und Power-Failure-Simulationen Datenverlust und Split-Brain-Zustände
  • JetStream führt standardmäßig fsync nur alle 2 Minuten aus, sodass zuletzt bestätigte Nachrichten möglicherweise nicht auf der Festplatte gespeichert sind
  • Ein einzelner OS-Crash eines Nodes kann bereits zu Datenverlust und Replikatsinkonsistenz führen
  • Jepsen empfiehlt für NATS, die Standardeinstellung auf fsync=always zu ändern oder die Datenverlustgefahr explizit in der Dokumentation zu kennzeichnen

1. Hintergrund

  • NATS ist ein beliebtes Streaming-System, das Nachrichten als Stream publiziert und abonniert
    • JetStream verwendet den Raft-Konsensalgorithmus, repliziert Daten und garantiert at-least-once-Zustellung
  • JetStream behauptet in der Dokumentation, Linearizable-Konsistenz und Always-Available-Verfügbarkeit zu bieten, doch laut CAP-Theorem können diese beiden Anforderungen nicht gleichzeitig erfüllt werden
  • Laut NATS-Dokumentation kann ein 3-Knoten-Stream 1 und ein 5-Knoten-Stream 2 Serverausfälle tolerieren
  • Eine Nachricht gilt als „erfolgreich gespeichert“, sobald der Server die publish-Anfrage acknowledged hat
  • Zur Wahrung der Datenkonsistenz wird ein Quorum von mehr als der Hälfte der Nodes benötigt; in einem 5-Knoten-Cluster sind mindestens 3 Nodes erforderlich, damit neue Nachrichten gespeichert werden können

2. Testdesign

  • Jepsen führte die Tests mit dem JNATS 2.24.0-Client und in einer Debian-12-LXC-Container-Umgebung durch
    • Einige Tests nutzten in der Antithesis-Umgebung das offizielle NATS-Docker-Image
  • Es wurde ein einzelner JetStream-Stream (Replikationsfaktor 5) aufgebaut und Prozessabbruch, Absturz, Netzwerktrennung, Paketverlust, Dateibeschädigung u. a. injiziert
  • Mit dem LazyFS-Dateisystem wurde eine Power-Failure-Simulation durchgeführt, die nicht fsync-gesicherte Schreibvorgänge verliert
  • Jeder Prozess veröffentlichte eindeutige Nachrichten; nach Testende wurde auf allen Nodes geprüft, ob acknowledgierte Nachrichten vorhanden sind
  • Wenn Nachrichten nur auf einigen Nodes vorhanden sind, wird das als Divergence (Replikationsabweichung) klassifiziert

3. Hauptbefunde

3.1 Vollständiger Datenverlust in NATS 2.10.22 (#6888)

  • Es wurde ein vollständiger Verlust eines gesamten JetStream-Streams durch einen einfachen Prozessabsturz festgestellt
  • Nach dem Fehler "No matching streams for subject" gab es über Stunden keine Wiederherstellung
  • Ursache war Leader-Snapshot-Inversion, Raft-Statuslöschung u. a.; behoben in Version 2.10.23

3.2 Datenverlust bei Beschädigung von .blk-Dateien (#7549)

  • Bei einem einzigen Bitfehler oder Trunkierung in der JetStream-.blk-Datei ging eine große Anzahl bestätigter Schreibvorgänge verloren
    • Beispiel: 679.153 von 1.367.069 gingen verloren
  • Auch bei Beschädigung nur einzelner Nodes kam es zu massivem Datenverlust und Split-Brain
    • Beispiel: Bis zu 78 % Nachrichtenverlust auf den Nodes n1, n3, n5
  • NATS untersucht den Fehler derzeit noch

3.3 Gesamtdatenverlust bei Snapshot-Dateibeschädigung (#7556)

  • Wird die Snapshot-Datei unter data/jetstream/$SYS/_js_/ beschädigt, stuft der Node den Stream als orphaned ein und löscht sämtliche Daten
  • Bereits die Beschädigung weniger Nodes verhindert das Quorum und macht den Stream dauerhaft unbenutzbar
  • Beispiel: Beschädigung der Nodes n3, n5 führte dazu, dass n3 zum Leader gewählt wurde und jepsen-stream vollständig gelöscht wurde
  • Jepsen weist darauf hin, dass bei der Leader-Wahl ein beschädigter Node zum Leader werden kann

3.4 Datenverlust durch die Standard-fsync-Einstellung (#7564)

  • JetStream führt standardmäßig nur alle 2 Minuten fsync aus, bestätigt Nachrichten jedoch sofort
    • Dadurch bleiben kürzlich bestätigte Nachrichten ungespeichert auf der Platte
  • Bei Stromausfall oder Kernel-Crash gehen innerhalb von Sekundenbereichs bestätigte Nachrichten verloren
    • Beispiel: 131.418 von 930.005 gingen verloren
  • Auch bei aufeinanderfolgenden Ausfällen einzelner Nodes ist ein vollständiges Löschen des Streams möglich
  • Das Verhalten wird in der Dokumentation kaum erwähnt
  • Jepsen empfiehlt die Umstellung auf fsync=always oder einen ausdrücklichen Warnhinweis zum Datenverlust-Risiko

3.5 Split-Brain durch einen einzelnen OS-Crash (#7567)

  • Ein Stromausfall oder Kernel-Crash eines einzelnen Nodes kann bereits zu Datenverlust und Replikatsinkonsistenz führen
  • In einem Leader-Follower-Setup kann es passieren, dass Knoten einen Write nur im Speicher committen und bestätigen, aber bei einem Ausfall danach
    verlieren mehrere Nodes diesen Write und arbeiten in einem neuen Zustand weiter
  • In den Tests trat nach einem einzelnen Stromausfall ein anhaltendes Split-Brain auf
    • Pro Node wurden unterschiedliche Intervalle fehlender acknowledged Nachrichten beobachtet
  • Jepsen verweist auf ähnliche Fälle bei Kafka und betont, dass dasselbe Risiko auch in Raft-basierten Systemen existiert

4. Diskussion und Fazit

  • Das Problem des vollständigen Datenverlusts in 2.10.22 wurde in 2.10.23 behoben
  • In 2.12.1 treten weiterhin Datenverlust und Split-Brain durch Dateibeschädigung und OS-Crashs auf
  • Bei .blk- und Snapshot-Dateibeschädigungen fehlen auf einzelnen Nodes Nachrichten oder der gesamte Stream wird gelöscht
  • Durch den langen Standard-fsync-Intervall besteht bei gleichzeitigen Ausfällen mehrerer Nodes weiterhin das Risiko, dass acknowledged Daten verloren gehen
  • Jepsen empfiehlt fsync=always oder eine klare Risikodokumentation in den Unterlagen
  • Die Aussage „Always Available“ von JetStream ist im Rahmen des CAP-Theorems unmöglich, die Dokumentation muss angepasst werden
  • Jepsen betont, dass ein Bug nachgewiesen werden kann, das Fehlen von Sicherheit jedoch nicht beweisbar ist

4.1 Rolle von LazyFS

  • Mit LazyFS wurden die Verluste nicht fsync-gesicherter Schreibvorgänge simuliert
  • Bei Stromausfall können so verschiedene Storage-Fehler, darunter partielle Schreibfehler (torn writes), nachgestellt werden
  • In der zugehörigen Arbeit When Amnesia Strikes (VLDB 2024) werden ähnliche Bugs auch bei PostgreSQL, Redis und ZooKeeper berichtet

4.2 Nächste Schritte

  • Der Verlust auf Single-Consumer-Ebene, die Reihenfolge von Nachrichten sowie die Prüfung von Linearizable/Serializable-Garantien wurden nicht durchgeführt
  • Die exactly-once-Zustellung wird ebenfalls als Thema für künftige Untersuchungen genannt
  • Bei Node-Hinzufügung und -Entfernung wurden Dokumentationsfehler sowie das Fehlen von Pflicht-Health-Check-Schritten festgestellt (#7545)
  • Ein sicheres Verfahren zur Änderung der Cluster-Konfiguration ist weiterhin unklar

1 Kommentare

 
GN⁺ 2025-12-09
Hacker-News-Kommentare
  • Jedes Mal, wenn jemand die komplexe Theorie überspringt und so ein System baut, sieht man, wie aphyr es auseinander nimmt
    Inzwischen frage ich mich, ob eine KI vielleicht die Projektdokumentation lesen und allein anhand der Marketingtexte ein Risiko für Datenverlust vorhersagen könnte
    • Fühlt sich an, als würde ich mir den langen Bart streichen und wissend nicken
      Die Leute sagen immer, „Theorie wird überbewertet“ oder „Hacken ist besser als schulische Ausbildung“, aber am Ende stellen sie sich im dokumentierten Problemraum selbst ein Bein
    • Ich habe etwas Ähnliches auch von einem LLM machen lassen, und das Ergebnis war ziemlich nützlich
  • Es wirkt, als würde NATS die CAP-Theorie ignorieren
    • Klingt nach einer unterschätzten Aussage
  • Ich habe NATS bisher für In-Memory-Pub/Sub verwendet, und dafür war es hervorragend
    Auch die subtilen Scaling-Details hat es gut gehandhabt
    Persistence habe ich allerdings nie genutzt, und ich hätte nicht gedacht, dass es so verwundbar ist
    Es ist erschreckend, dass schon eine einzelne Bit-Beschädigung einer Datei problematisch sein kann
  • Als passendes Material dazu haben Jepsen und Antithesis kürzlich ein Glossar für verteilte Systeme veröffentlicht
    Eine sehr gute Referenz → Jepsen Glossary
  • Ich habe mich gefragt, worin der inhaltliche Unterschied zwischen aphyr.com/tags/jepsen und jepsen.io/analyses besteht
    Ich habe aphyr.com erst vor Kurzem entdeckt und erwarte dort viele Einsichten
    • Jepsen begann ursprünglich als persönliche Blogserie
      Später entwickelte sich jepsen.io zu einem professionellen Projekt und wird seit etwa zehn Jahren ernsthaft betrieben
  • Ich frage mich, warum es die Einstellung „Lazy fsync by Default“ überhaupt gibt
    Soll damit die Benchmark-Performance erhöht werden? In kleinen Clustern ist so eine Einstellung oft die Ursache von Problemen
    • Es verbessert nicht nur die Latenz, sondern auch den Durchsatz (throughput)
      Viele Anwendungen brauchen keine vollständige Dauerhaftigkeit, daher kann lazy fsync nützlich sein
      Ob es der Standardwert sein sollte, ist aber umstritten
    • Ich habe mich immer gefragt, warum fsync unbedingt verzögert werden muss
      So etwas müsste sich doch wie bei TCP corking mit Batching lösen lassen
    • Das ist eines der Dinge, die in verteilten Systemen möglich sind
      Ausfälle durch lazy fsync treten auf den meisten Knoten nicht gleichzeitig auf
    • Es ist eine Entscheidung zugunsten der Performance
    • Man zielt gleichzeitig auf Dauerhaftigkeit durch Replikation und Verteilung und auf den mit lazy fsync gewonnenen Durchsatz
  • Als serverlose Alternative zu JetStream wird s2.dev empfohlen
    Vorteil: unbegrenzte Streams mit Dauerhaftigkeit auf Objekt-Storage-Niveau
    Nachteil: noch keine Consumer-Group-Funktion
    • Würde mich interessieren, ob darauf schon einmal Jepsen-Tests gelaufen sind
  • Problematisch ist, dass NATS standardmäßig nur alle zwei Minuten fsync ausführt und sofort ein Ack zurückgibt
    Wenn mehrere Knoten gleichzeitig ausfallen, kann es zum Verlust bereits committeter Daten kommen
    Das erinnert an das „Web-Scale“-Marketing aus den frühen MongoDB-Tagen
    Ich finde, der Standardwert sollte immer die sicherste Option sein
    • NATS sagt klar, dass es nur Cluster-Verfügbarkeit garantiert
      Genau das fand ich sogar gut, weil man darauf aufbauend sein System entwerfen konnte
      Als ich es 2018 verwendet habe, war es performant und einfach zu betreiben
    • Die meisten modernen DBs sind standardmäßig auch nicht vollständig sicher
      Bei PostgreSQL ist das Standard-Transaktionsisolationsniveau zum Beispiel read committed
      Auch Redis macht standardmäßig nur einmal pro Sekunde fsync
    • Ein Redis-Cluster gibt ein Ack erst zurück, nachdem auf mehrere Knoten repliziert wurde
      Auch bei standalone Redis kann man Ack nach fsync konfigurieren, aber wegen OS-Buffering ist eine vollständige Garantie schwierig
      Am Ende ist es wichtig, die Bedeutung eines Acks genau zu verstehen
    • Die meisten Systeme wählen einen Kompromiss zwischen Geschwindigkeit und Dauerhaftigkeit
      Wenn man nur auf sichere Standardwerte besteht, leidet die Performance stark, und die Nutzer müssen alles selbst tunen
      Zum Beispiel ist auch das Standard-Isolationsniveau von Postgres schwach genug, dass Race Conditions auftreten können
      Siehe: Hermitage-Artikel
    • Das Problem bei fsync ist, dass es nur extreme Optionen gibt
      Im SSD-Zeitalter sind Zwischenstufen wie Group Commit verschwunden, und jetzt ist der Syscall-Wechselaufwand der Flaschenhals
      Zwei Minuten sind ein viel zu langes Intervall (auch der Unterschied zwischen fdatasync und fsync sollte berücksichtigt werden)
  • Ehrlich gesagt hatte ich es erwartet, aber nicht, dass es so schlimm ist
    Vermutlich wäre es besser, einfach Redpanda zu verwenden
  • Ich habe mich gefragt, ob sich die fsync-Performance-Warnung von NATS verbessern lässt
    Wenn man in festen Intervallen einen Batch-Flush ausführt, würde die Latenz zwar steigen, aber der Durchsatz könnte erhalten bleiben
    • Nicht mit einem festen Intervall, sondern indem man Schreibanfragen während eines laufenden fsync in eine Queue stellt und sie dann im nächsten Batch gemeinsam verarbeitet
      Das ähnelt der Art, wie Paxos-Runden gebündelt werden
      Sobald eine Runde abgeschlossen ist, sollte direkt der nächste Batch gestartet werden