Featured image of post Warum Daten einen Stromausfall beim Speichern überleben: Die Architektur der Crash-Konsistenz

Warum Daten einen Stromausfall beim Speichern überleben: Die Architektur der Crash-Konsistenz

Technische Analyse der Crash-Konsistenz in Speichersystemen: Warum ein einziger 4-KB-Schreibvorgang mehrere nicht-atomare Festplattenzugriffe erfordert, wie OS-Page-Cache und SSD-Schreibpuffer I/O umsortieren, die Grenzen von fsck und Soft Updates, Write-Ahead Logging (WAL) und ARIES, ext4-Journaling-Modi, Copy-on-Write (CoW) Merkle-Bäume in ZFS/Btrfs sowie die berüchtigten fsync()-Fallen.

1. Einleitung: Das Minenfeld hinter dem Anhängen eines einzigen 4-KB-Blocks

Wenn wir in einem Texteditor Strg+S drücken oder eine Banküberweisung bestätigen, führt das Anwendungsprogramm einen write()-Systemaufruf aus. Aus Sicht des Anwendungsprogrammierers wirkt das Speichern wie ein einziger, unteilbarer (atomarer) Schritt.

Blickt man jedoch in die Tiefen des Betriebssystemkerns, des Dateisystems, der Datenbank-Engine und der physischen SSD- oder HDD-Hardware, offenbart sich eines der schwierigsten Probleme der Systeminformatik: das Crash-Konsistenz-Problem (Crash Consistency Problem).

Betrachten wir die einfachste denkbare Operation in einem klassischen UNIX-Dateisystem (wie UFS oder ext2): das Anhängen eines einzigen neuen $4\,\text{KB}$-Datenblocks $D_{\text{new}}$ an eine bestehende Datei. Selbst für diesen trivialen Vorgang müssen mindestens drei räumlich getrennte Bereiche auf dem Speichermedium aktualisiert werden:

  1. Die Daten-Bitmap ($B$): Das Zuordnungsbit für den physischen Block $k$ wird von $0$ (frei) auf $1$ (belegt) gesetzt.
  2. Der Inode ($I$): Die Metadatenstruktur der Datei wird aktualisiert (Dateigröße size, Zeitstempel mtime sowie der Zeiger auf die physische Blockadresse $k$).
  3. Der Datenblock ($D_{\text{new}}$): Die eigentlichen $4\,\text{KB}$ Nutzdaten des Benutzers werden in den Sektor von Block $k$ geschrieben.

Hier prallen zwei fundamentale Eigenschaften der Hardware aufeinander: Der Hauptspeicher (DRAM) ist schnell, aber flüchtig (volatile); Festplatten und NAND-Flash-SSDs sind nichtflüchtig (non-volatile), können aber räumlich getrennte Blöcke $(B, I, D_{\text{new}})$ niemals gleichzeitig in einer einzigen atomaren physikalischen Aktion überschreiben.

Was passiert, wenn mitten während dieser drei Schreibvorgänge der Strom ausfällt (Power Loss) oder der Kernel abstürzt? Je nachdem, welche Teilmenge der drei Blöcke vor dem Stromausfall die physische Platte erreicht hat ($2^3 - 2 = 6$ mögliche Zwischenzustände), erwacht das Dateisystem beim Neustart in einem Zustand schwerer Inkonsistenz:

Auf Platte gespeichertIm RAM verlorenResultierende Crash-Inkonsistenz & Schweregrad
Nur $D_{\text{new}}$$B,\; I$Gutartiger Datenverlust: Die Nutzdaten liegen auf der Platte, aber weder Inode noch Bitmap verweisen darauf. Die Dateisystemstruktur bleibt intakt.
Nur $B$$I,\; D_{\text{new}}$Speicherleck (Space Leak / verwaister Block): Block $k$ ist in der Bitmap als belegt ($1$) markiert, wird aber von keinem Inode referenziert – die Kapazität geht dauerhaft verloren.
Nur $I$ (oder $I + D_{\text{new}}$)$B$Doppelbelegung (Double Allocation – Katastrophale Korruption): Inode $I$ zeigt auf Block $k$, aber die Bitmap $B$ meldet ihn als frei ($0$). Später belegt eine zweite Datei denselben Block $k$ und beide Dateien zerstören sich gegenseitig!
$I$ und $B$$D_{\text{new}}$Offenlegung veralteter Daten (Stale Data Exposure – Sicherheitslücke): Inode und Bitmap sind aktualisiert, aber $D_{\text{new}}$ fehlt. Beim Lesen der Datei werden fremde, zuvor gelöschte Daten (z. B. Passwörter oder private Schlüssel) aus Sektor $k$ zurückgegeben!

2. Warum Schreibvorgänge nicht in Programmreihenfolge auf der Platte landen

In modernen Rechnersystemen stimmt die Reihenfolge von write()-Aufrufen fast nie mit der physischen Schreibreihenfolge auf dem Speichermedium überein, da vier flüchtige Pufferschichten die Aufträge umsortieren (Reordering):

  flowchart TD
    APP["1. Anwendungsraum (User Space)<br/>Laufzeitpuffer (stdio fwrite) / DB Buffer Pool"]
    PC["2. OS-Kernel Page Cache (Dirty DRAM Pages)<br/>Write-Back-Caching: Verzögert & bündelt Schreibzugriffe über Sekunden"]
    SCHED["3. OS-Block-Layer & I/O-Scheduler (mq-deadline / BFQ)<br/>Sortiert Anfragen nach LBA-Sektoradresse zur Minimierung der Suchzeit"]
    DEV["4. Flüchtiger DRAM-Schreibcache im Laufwerk & NCQ/NVMe-Queues<br/>SSD-FTL-Controller sortiert Schreibvorgänge über NAND-Kanäle um"]
    MEDIA[("5. Nichtflüchtiges physisches Medium<br/>NAND-Flash-Zellen / Magnetplatten (Nur dies überlebt Stromausfall!)")]

    APP -->|"write()-Syscall"| PC
    PC -->|"Hintergrund-Flush / fsync()"| SCHED
    SCHED -->|"NVMe- / SATA-Befehle"| DEV
    DEV -->|"FLUSH CACHE / FUA-Barriere"| MEDIA

Erst durch explizite Hardware-Speicherbarrieren (NVMe Flush, SATA FLUSH CACHE EXT oder FUA: Force Unit Access) lässt sich erzwingen, dass auch der flüchtige DRAM-Puffer auf der SSD-Platine in die nichtflüchtigen NAND-Zellen geleert wird.


3. Generation 1: Optimistisches Überschreiben mit fsck und Soft Updates

  1. fsck (File System Consistency Check) und die Skalierungsmauer: Frühe Dateisysteme (FFS, ext2) führten nach einem Absturz vor dem Mounten einen vollständigen Scan aller Inodes, Bitmaps und Verzeichnisse durch (fsck). Da die Laufzeit von fsck proportional zur Gesamtkapazität der Festplatte $O(N_{\text{disk}})$ wächst, dauert die Prüfung auf modernen Multi-Terabyte-Systemen Stunden oder Tage – selbst wenn beim Stromausfall nur eine einzige $4\,\text{KB}$-Datei geschrieben wurde!
  2. Soft Updates (Ganger & McKusick 1994): FreeBSD UFS vermied ein Journal durch strikte Abhängigkeitsregeln beim Zurückschreiben aus dem RAM und löste zyklische Abhängigkeiten innerhalb desselben Blocks durch temporäres Roll-back und Roll-forward einzelner Zeiger im Hauptspeicher.

4. Das Rückgrat moderner Speicher: Write-Ahead Logging (WAL) und Journaling

Um die Wiederherstellungszeit von $O(N_{\text{disk}})$ auf die Menge der im Absturzmoment aktiven Transaktionen $O(N_{\text{active}})$ (wenige Millisekunden!) zu reduzieren, nutzen moderne Dateisysteme (ext3/ext4, XFS, NTFS) und Datenbanken Write-Ahead Logging (WAL) / Journaling:

  1. Journal Write: Vor dem Ändern der eigentlichen Speicherorte (Home Locations) schreibt das Dateisystem einen Transaktions-Startblock TxB (mit TID) sowie die neuen Blöcke $(I, B, D_{\text{new}})$ sequenziell in einen reservierten Ringpuffer auf der Platte (das Journal).
  2. Journal Commit (Atomarer Schaltpunkt): Nach einer Hardware-Flush-Barriere wird ein einzelner Commit-Block TxE geschrieben. Mit dem Schreiben dieses einzigen Sektors TxE gilt die gesamte Transaktion atomar als abgeschlossen!
  3. Checkpoint: Anschließend werden $(I, B, D_{\text{new}})$ asynchron an ihre regulären Speicherorte (Home Locations) kopiert.
  flowchart LR
    subgraph Journal ["On-Disk-Ringjournal (Sequenzielles Anhängen)"]
        TXB["1. TxB (TID=42)<br/>Deskriptorblock"] --> JB["2. Geloggte Bitmap B"]
        JB --> JI["3. Geloggter Inode I"]
        JI --> JD["4. Geloggte Daten D_new"]
        JD ==>|"★Hardware-Flush-Barriere"| TXE["5. TxE (TID=42)<br/>Atomarer Commit-Block"]
    end

    subgraph Home ["Reguläre Speicherorte (Home Locations)"]
        HB["Reguläre Bitmap"]
        HI["Reguläre Inode-Tabelle"]
        HD["Regulärer Datenblock"]
    end

    TXE ==>|"6. Checkpoint (Asynchron)"| HB
    TXE ==>|"6. Checkpoint"| HI
    TXE ==>|"6. Checkpoint"| HD
  • Stromausfall VOR TxE: Die Home Locations sind unberührt; beim Neustart wird der unvollständige Log-Eintrag einfach verworfen.
  • Stromausfall NACH TxE: Beim Neustart werden die Blöcke aus dem Journal idempotent in die Home Locations zurückkopiert (Redo-Replay). Modernes ext4 nutzt zudem Journal-Prüfsummen (CRC32C) in TxE, um sich die erste Flush-Barriere vor TxE zu sparen.

Um eine Verdoppelung der Schreibdatenmenge ($2\times$ Write Amplification) zu vermeiden, nutzt ext4 standardmäßig den Modus data=ordered: Datenblöcke $D_{\text{new}}$ werden nicht ins Journal geschrieben, sondern direkt an ihren Zielort, aber das Dateisystem garantiert, dass $D_{\text{new}}$ vollständig auf der Platte liegt, bevor das Metadaten-Commit TxE ins Journal geschrieben wird! In relationalen Datenbanken kombiniert das ARIES-Protokoll (Mohan et al., 1992) unter einer Steal/No-Force-Strategie pageLSN-Zeitstempel mit den drei Phasen Analysis, Redo (Repeating History) und Undo (mit Compensation Log Records, CLRs).


5. Generation 3: Copy-on-Write (CoW) und Merkle-Bäume (ZFS / Btrfs)

Dateisysteme wie ZFS, Btrfs und APFS verbieten das Überschreiben bestehender Blöcke (In-Place Update) vollständig und setzen auf Copy-on-Write (CoW / Shadow Paging):

  1. Geänderte Datenblöcke $D_{2,\text{new}}$ und ihre übergeordneten Inodes $I_{v2}$ werden stets in neuen freien Speicherbereichen angelegt, während die alten Blöcke $D_{2,\text{old}}$ unangetastet bleiben.
  2. Erst wenn alle neuen Kindknoten sicher auf dem Medium liegen, schaltet ein einziger atomarer Schreibvorgang den Wurzelblock (Uberblock in ZFS) von $U_1$ auf $U_2$ um.
  3. Vorteile: $0$ Sekunden Recovery-Zeit ohne Journal-Replay, verzögerungsfreie $O(1)$-Snapshots sowie vollständige Selbstheilung gegen Bit Rot durch kryptografische Merkle-Bäume (256-Bit-Prüfsummen im Elternzeiger).

6. Die fsync()-Fallen in der Anwendungsentwicklung und Fault-Injection-Tests

Selbst auf einem Journaling- oder CoW-Dateisystem führt ein bloßes rename("config.json.tmp", "config.json") ohne vorheriges fsync() unter Delayed Allocation (ext4) nach einem Stromausfall zu leeren $0$-Byte-Dateien, weil die Metadaten-Umbenennung vor den gepufferten Dateidaten auf die Platte geschrieben wird!

Das vollständige 6-Schritte-Protokoll für absturzsicheres Speichern lautet: (1) write() in temporäre Datei $\to$ (2) fsync(fd) auf die temporäre Datei $\to$ (3) close(fd) $\to$ (4) Atomares rename() $\to$ (5) Öffnen des Elternverzeichnisses und fsync(dir_fd), um den Verzeichniseintrag selbst dauerhaft zu sichern $\to$ (6) close(dir_fd).

Wie das berüchtigte PostgreSQL-fsyncgate (2018) zeigte – bei dem der Linux-Kernel nach einem fehlgeschlagenen fsync() (EIO) die fehlerhafte RAM-Seite fälschlich als Clean markierte (behoben durch errseq_t ab Linux 4.17) –, lassen sich subtile Crash-Konsistenz-Fehler nur durch systematische Fault-Injection-Frameworks wie ALICE und CrashMonkey aufdecken, die alle erlaubten Hardware-Umsortierungen bei Stromausfall systematisch simulieren.

comments powered by Disqus