Ratgeber

Lokaler NVMe-Speicher vs. Netzwerk-Storage: der ehrliche Vergleich

Bei der Wahl eines Servers entscheidet der Storage oft mehr über die gefühlte Geschwindigkeit als die CPU. Dieser Ratgeber erklärt zuerst, was lokaler NVMe -Speicher überhaupt ist, vergleicht ihn dann mit Netzwerk-Storage (SAN, Ceph, Cloud-Volumes) — nach Latenz, fsync-Verhalten, Ausreißern unter Last und Persistenz — und gibt dir am Ende die fio-Kommandos, mit denen du jede Anbieter-Behauptung auf deinem eigenen Cloud-Server mit lokalem NVMe selbst nachmisst.

Cloud-Server mit lokalem NVMe

Lokales NVMe ist eine SSD, die direkt über PCIe im selben Host steckt, auf dem auch deine VM läuft. Netzwerk-Storage — ob SAN, verteiltes Ceph oder ein Cloud-Volume — liegt dagegen auf separaten Systemen und wird über ein Netzwerk angebunden. Dieser eine Unterschied im Datenpfad erklärt fast alle Vor- und Nachteile, die folgen. Und er erklärt auch, warum die beiden Varianten in einem Marketing-Datenblatt identisch aussehen können und sich im Betrieb trotzdem völlig unterschiedlich anfühlen.

Lokaler NVMe-Speicher: was der Begriff wirklich bedeutet

Lokaler NVMe-Speicher ist Flash-Speicher, der physisch in derselben Maschine sitzt wie die CPU, die deinen Workload ausführt, und über PCIe mit dem NVMe-Protokoll angesprochen wird. Kein Lese- oder Schreibvorgang verlässt dabei das Gehäuse: Der Kernel legt den Befehl in eine Submission Queue im RAM, klingelt per Doorbell-Register am Controller, der Controller holt sich den Befehl über PCIe und legt die Antwort in die Completion Queue. Es gibt keinen Netzwerk-Stack, keinen Switch, keine fremde Storage-Software und keinen zweiten Rechner, der ebenfalls antworten muss.

Die Verwirrung entsteht, weil „NVMe" im Marketing oft nur die Bauart der Datenträger im Storage-Cluster beschreibt — nicht die Topologie. Ein Volume kann vollständig aus NVMe-SSDs bestehen und trotzdem drei Netzwerk-Hops entfernt liegen. Für deine Latenz zählt aber nicht, welche Bauform die Zellen haben, sondern wie weit der Weg zu ihnen ist.

  • Lokal ist es, wenn der Datenträger im selben Host steckt und höchstens der Hypervisor dazwischen liegt — sonst nichts.
  • Nicht lokal ist es bei Formulierungen wie „NVMe-backed", „NVMe-basiertes Block-Storage", „High-Performance-Volume" oder „verteiltes NVMe-Cluster" — das sind Beschreibungen der Medien, nicht des Pfads.
  • Ebenfalls nicht lokal: alles, was du im laufenden Betrieb an eine andere VM anhängen kannst. Losgelöste Verfügbarkeit ist genau das Merkmal, das Netzwerk-Storage auszeichnet — und lokales NVMe per Definition nicht hat.
  • Grauzone: NVMe over Fabrics mit RDMA. Das ist technisch Netzwerk-Storage, kommt lokalem NVMe aber deutlich näher als klassisches iSCSI. Der Abstand ist eine Eigenschaft des Pfads und der Auslastung, kein Naturgesetz.

So prüfst du, ob dein Speicher wirklich lokal ist

Von innen ist die Frage schwerer zu beantworten, als sie klingt — aber ein paar Kommandos grenzen sie stark ein:

  1. lsblk -d -o NAME,MODEL,TRAN,ROTA,SIZE — zeigt Transport und Modell. Ein echtes NVMe-Gerät heißt nvme0n1 und meldet TRAN=nvme.
  2. nvme list (Paket nvme-cli) — listet NVMe-Namespaces samt Modellbezeichnung. Liefert nur dann etwas, wenn dir der Namespace tatsächlich durchgereicht wird.
  3. ls -l /dev/disk/by-path/ — ein pci-Pfad deutet auf ein echtes PCIe-Gerät hin (Bare Metal oder Passthrough), ein virtio-pci-Pfad auf eine Hypervisor-Abstraktion.
  4. lsblk | grep -E 'rbd|nbd|dm-' und iscsiadm -m session — findet Ceph-RBD-, NBD- und iSCSI-Anbindungen, die eindeutig über das Netz gehen.
  5. Nur auf Bare Metal aussagekräftig: ip -s link vor und nach einem schweren Schreib-Benchmark. Skalieren die NIC-Zähler mit deinem Schreibdurchsatz, liegt der Speicher im Netz.

Und jetzt die ehrliche Einschränkung: In einer typischen KVM-VM siehst du bei virtio-blk nur ein Gerät namens vda und kannst von innen nicht sicher feststellen, was dahinter liegt. Der Storage-Traffic verlässt den Host über dessen eigene NIC, für dich unsichtbar. Deshalb bleiben genau zwei belastbare Wege: den Anbieter direkt fragen — und messen. Das Messen ist der ehrlichere Weg, weil es dir nicht die Topologie verrät, sondern das, was du eigentlich wissen willst: wie sich der Pfad unter deiner Last verhält.

Der Datenpfad: PCIe gegen den Netzwerk-Hop

Beim lokalen NVMe ist der Weg zu den Daten kurz: CPU, RAM und Flash sitzen in derselben Maschine, der Befehl wandert über PCIe, die Antwort denselben Weg zurück. Bei Netzwerk-Storage kommt vor jedem einzelnen IO eine ganze Kette dazu — die Anfrage wird serialisiert, geht durch den Netzwerk-Stack, über die NIC, durch mindestens einen Switch, trifft auf dem Storage-Knoten auf dessen eigenen IO-Scheduler und dessen eigene Datenträger, und dieselbe Kette muss für die Bestätigung rückwärts durchlaufen werden.

Bei replizierten Systemen kommt eine weitere Runde dazu: Ein Schreibvorgang geht erst an den primären Knoten, der ihn an seine Repliken weiterreicht, und wird erst bestätigt, wenn die Repliken bestätigt haben. Jede dieser Stationen hat eine eigene Warteschlange, und Wartezeit in einer Warteschlange ist nicht konstant, sondern schwankt mit der Auslastung. Genau daher stammen die Ausreißer, die man im Mittelwert nie sieht.

Latenz, Durchsatz und Warteschlangentiefe

Für viele kleine, zufällige Zugriffe zählt vor allem die Latenz — und die ist bei lokalem NVMe um Größenordnungen niedriger, weil keine Netzwerk-Hops dazwischenliegen. Beim sequenziellen Durchsatz kann gut angebundenes Netzwerk-Storage deutlich näher herankommen, wird aber durch den Uplink begrenzt.

Der wichtigste und am seltensten verstandene Punkt ist die Warteschlangentiefe. Bei hoher Queue Depth laufen viele Anfragen gleichzeitig — die Round-Trips überlappen sich, und der Netzwerkweg lässt sich weitgehend verstecken. Genau deshalb sieht Netzwerk-Storage in Durchsatz-Benchmarks oft hervorragend aus. Bei Queue Depth 1 ist dieses Verstecken unmöglich: Es ist genau eine Anfrage unterwegs, und du wartest auf die volle Umlaufzeit. Ein Datenbank-Commit ist ein Ereignis bei Queue Depth 1. Wer nur mit tiefer Queue misst, misst nie das, was seine Nutzer spüren.

fsync, Dauerhaftigkeit und der Commit-Pfad einer Datenbank

Hier wird es konkret. Eine Datenbank darf ein COMMIT erst bestätigen, wenn ihr Write-Ahead-Log dauerhaft auf dem Medium liegt. PostgreSQL schreibt das WAL und ruft anschließend fdatasync auf; MySQL/InnoDB tut mit innodb_flush_log_at_trx_commit=1 dasselbe. Dieser Aufruf ist keine Formalität: Er zwingt den Cache-Flush bis auf stabiles Medium und kehrt erst zurück, wenn die unterste Schicht bestätigt hat.

Bei lokalem NVMe ist diese unterste Schicht ein Flush-Kommando über PCIe an den Controller. Bei Netzwerk-Storage muss der Flush über das Netz zum Storage-Knoten, dort dauerhaft werden, gegebenenfalls von allen Repliken dauerhaft werden, und die Bestätigung muss den ganzen Weg zurück. Und weil jede Transaktion eines einzelnen Clients hinter diesem Aufruf serialisiert ist, gilt eine unangenehme Regel: Die Transaktionsrate eines einzelnen Clients ist durch seine Commit-Latenz begrenzt. Keine schnellere CPU und kein größerer RAM ändern daran etwas.

Deshalb greifen viele Teams irgendwann zu synchronous_commit=off oder innodb_flush_log_at_trx_commit=2. Das macht die Anwendung sofort schneller — und tauscht dafür Dauerhaftigkeit gegen Latenz ein: Bei einem Absturz gehen die zuletzt bestätigten Transaktionen verloren. Das kann eine bewusste, richtige Entscheidung sein. Eine gute Entscheidung ist sie selten, wenn man sie unbewusst trifft, nur weil der Speicher zu langsam war.

Tail-Latenz: warum p99 wichtiger ist als der Mittelwert

Der Mittelwert einer IO-Messung wird von der großen Masse unauffälliger Anfragen bestimmt. Deine Nutzer erleben aber nicht den Mittelwert, sondern die Ausreißer. Und die Ausreißer entstehen dort, wo eine Warteschlange voll ist: Je näher ein geteiltes System an seine Sättigung kommt, desto überproportionaler wächst die Wartezeit darin — und bei einem geteilten Storage-Cluster gehört zu dieser Auslastung auch der Anteil von Nachbarn, die du nicht siehst und nicht steuerst.

Dazu kommt ein Verstärkungseffekt: Wenn ein einzelner Seitenaufruf zwanzig Storage-Operationen auslöst, wartet er auf die langsamste davon. Das p99 des Geräts wird dadurch zur gefühlten Normalität einer Seite. Genau deshalb ist der Abstand zwischen p50 und p99.9 die aussagekräftigste Zahl einer Messung — nicht der Durchschnitt.

Selbst messen: die fio-Kommandos

Alles bisher Gesagte kannst du auf jedem Host in einer halben Stunde selbst überprüfen. Installiere fio (apt install fio), sorge für genug freien Platz und lege eine Testdatei an einem Ort an, an dem nichts Wichtiges liegt. Wichtig: Richte --filename niemals auf ein Rohgerät mit Daten — die Schreibtests überschreiben, worauf du sie zeigen lässt.

1. Testdatei einmal anlegen

  1. fio --name=layout --filename=/var/lib/fio.test --size=8G --rw=write --bs=1M --ioengine=libaio --direct=1 --iodepth=16

Die Datei muss größer sein als jeder Cache im Pfad — Page Cache der VM, Page Cache des Hosts, Cache des Storage-Controllers. Sonst misst du RAM und nicht Speicher.

2. Random Read, 4k, Queue Depth 1 — die reine Latenzfrage

  1. fio --name=randread-qd1 --filename=/var/lib/fio.test --size=8G --rw=randread --bs=4k --ioengine=libaio --direct=1 --iodepth=1 --numjobs=1 --time_based --runtime=300 --ramp_time=30 --percentile_list=50:90:99:99.9:99.99 --status-interval=10 --group_reporting

3. Dasselbe mit tiefer Queue — die Durchsatzfrage

  1. fio --name=randread-qd32 --filename=/var/lib/fio.test --size=8G --rw=randread --bs=4k --ioengine=libaio --direct=1 --iodepth=32 --numjobs=4 --time_based --runtime=300 --ramp_time=30 --percentile_list=50:90:99:99.9 --status-interval=10 --group_reporting

4. Random Write, 4k, Queue Depth 1

  1. fio --name=randwrite-qd1 --filename=/var/lib/fio.test --size=8G --rw=randwrite --bs=4k --ioengine=libaio --direct=1 --iodepth=1 --numjobs=1 --time_based --runtime=300 --ramp_time=30 --percentile_list=50:90:99:99.9:99.99 --status-interval=10 --group_reporting

5. Der fsync-gebundene Test — der Commit-Pfad

  1. fio --name=commit --filename=/var/lib/fio.test --size=1G --rw=randwrite --bs=4k --ioengine=psync --fdatasync=1 --iodepth=1 --numjobs=1 --time_based --runtime=300 --ramp_time=30 --percentile_list=50:90:99:99.9 --group_reporting
  2. Für viele parallele Clients dasselbe mit --numjobs=16 wiederholen — das modelliert 16 gleichzeitig committende Verbindungen.
  3. Als Gegenprobe speziell für PostgreSQL: pg_test_fsync — es misst dieselbe Operation für jede verfügbare wal_sync_method.

Hier fehlt --direct=1 mit Absicht: Eine Datenbank schreibt ihr WAL gepuffert und erzwingt die Dauerhaftigkeit anschließend mit fdatasync. Genau diesen Ablauf bildet --ioengine=psync --fdatasync=1 nach. fio weist die Zeiten für die Sync-Aufrufe separat aus — das ist der Block, der dich interessiert, nicht die reine Schreib-Latenz darüber.

Die Ausgabe richtig lesen

  • clat ist die Completion Latency, slat die Submission Latency, lat die Summe. Für Storage-Vergleiche zählt clat — slat sagt vor allem etwas über deinen eigenen Kernel-Pfad aus.
  • Lies die clat-Perzentile bei Queue Depth 1. Das ist die Zeit, die ein synchroner Commit tatsächlich erlebt. Der IOPS-Wert bei Queue Depth 32 beantwortet eine andere Frage.
  • Vergleiche p50 mit p99.9, nicht mit dem Mittelwert. Ein enger Abstand bedeutet einen berechenbaren Pfad, ein weiter Abstand bedeutet Konkurrenz um eine Warteschlange irgendwo unterwegs.
  • Eine hohe Standardabweichung ist dasselbe Signal in kompakter Form: Der Pfad ist nicht exklusiv.
  • numjobs und iodepth modellieren Unterschiedliches. numjobs sind Threads, also Clients; iodepth sind ausstehende IOs pro Thread, also Batching. Datenbanken sehen aus wie viele Jobs bei geringer Tiefe, nicht wie ein Job bei Tiefe 64.
  • Ein einzelner Lauf über 30 Sekunden verbirgt laute Nachbarn vollständig. Konkurrenz tritt schubweise auf: Sie verschiebt p99.9 und die Zwischenausgaben von --status-interval, den Mittelwert kaum. Lauf lange, wiederhole zu verschiedenen Tageszeiten, und vergleiche die Läufe untereinander.
  • Viele Plattformen arbeiten mit Burst-Guthaben. Ein kurzer Test läuft komplett im Guthaben und misst deshalb den besten statt den typischen Fall. --ramp_time hilft beim Einschwingen, gegen Burst-Budgets hilft nur eine lange Laufzeit.
  • Miss nicht auf einem leeren System, sondern während deine echte Last läuft. Interessant ist, was der Speicher unter Realbedingungen tut — nicht im Labor.
  • Und die wichtigste Lesehilfe für fremde Datenblätter: Eine IOPS-Zahl ohne Blockgröße, Queue Depth, Lese-/Schreib-Mischung und die Angabe, ob --direct=1 gesetzt war, ist keine Aussage.
Lokaler NVMe-Speicher gegenüber Netzwerk-Storage
KriteriumLokales NVMeNetzwerk-Storage (SAN/Ceph)
DatenpfadPCIe im selben HostNetzwerk-Stack, Switch, Storage-Knoten
Latenz bei Queue Depth 1Sehr niedrig (kein Netzwerk-Hop)Höher, Umlaufzeit nicht versteckbar
Latenz bei tiefer QueueSehr niedrigRound-Trips überlappen sich, sieht gut aus
fsync / Commit-PfadFlush über PCIe an den ControllerFlush über das Netz, ggf. über alle Repliken
Ausreißer unter Last (p99.9)Abhängig von den VMs auf dem HostAbhängig vom gesamten Cluster
DurchsatzVolle lokale BandbreiteDurch den Uplink begrenzt
Persistenz bei Host-AusfallAn den Host gebunden, Restore aus BackupUnabhängig, Volume woanders anhängen
Live-MigrationEingeschränktEinfach
Snapshots und KloneBytes kopierenCopy-on-Write, nahezu sofort
Kapazität erweiternBegrenzt durch den HostNahezu frei skalierbar
Ideal fürDatenbanken, Builds, CachesHochverfügbarkeit, große geteilte Volumes

Wo Netzwerk-Storage wirklich gewinnt

Hier gewinnt Netzwerk-Storage fair und ehrlich — und zwar nicht nur in einem Punkt. Wer die folgenden Fähigkeiten wirklich nutzt, kauft mit der höheren Latenz etwas Konkretes ein und nicht bloß einen Kompromiss:

  • Entkoppelte Persistenz: Fällt der Compute-Host aus, hängst du das Volume an eine andere Maschine. Bei lokalem NVMe steht stattdessen ein Restore aus dem Backup an — und das ist eine ganz andere Wiederanlaufzeit.
  • Live-Migration: Wartung an der Hardware ohne Downtime für dich. Mit lokalem NVMe bedeutet Hostwartung ein Reboot-Fenster oder eine echte Datenverschiebung.
  • Copy-on-Write- Snapshots und Klone: Zwanzig identische Testumgebungen aus einem Golden Volume sind fast kostenlos. Lokal kopierst du dafür Bytes.
  • Kapazität jenseits des Hosts: Ein Volume kann wachsen, ohne dass die Compute-Größe angefasst wird — und über das hinaus, was physisch in einen Server passt.
  • Eingebaute Redundanz: Replikation oder Erasure Coding liefern Haltbarkeit ohne eine Zeile Anwendungscode. Bei lokalem NVMe ist das dein Job — RAID schützt gegen ein Laufwerk, nicht gegen einen Host.
  • Betriebliche Einfachheit im großen Maßstab: Zustandsloses Compute lässt sich leichter automatisieren, autoskalieren und einfach neu aufbauen.
  • Und der ehrlichste Punkt: Für viele Workloads ist die Latenz schlicht egal. Ein Webshop, der aus dem Cache ausliefert, ein CMS mit wenigen Schreibvorgängen, Archive, Mail-Spools, Asset-Ablagen — hier eine Latenzsteuer zu zahlen, die niemand bemerkt, und dafür Hochverfügbarkeit zu bekommen, die man wirklich nutzt, ist die richtige Entscheidung.

Laute Nachbarn — und warum „lokal" allein nichts garantiert

Ein geteiltes Storage-Cluster hat dasselbe Grundproblem wie ein überbuchter Host: Wenn ein Nachbar das Storage mit IO flutet, spüren es alle anderen. Das ist die Storage-Variante der Überbuchung — nur eben für IOPS statt für CPU-Takte. Lokales NVMe teilst du dir höchstens mit den wenigen VMs auf demselben Host, nicht mit einem ganzen Cluster.

Trotzdem: „Lokal" ist kein Zauberwort. Ein Host, der vierzig VMs auf ein einziges Laufwerk stapelt, produziert genauso hässliche Ausreißer wie ein überlastetes SAN — der Engpass sitzt dann eben im Gerät statt im Netz. Das Wort „lokal" auf einem Datenblatt garantiert nur die Topologie, nicht die Performance. Was zählt, ist die Kombination aus kurzem Pfad und der Zusage, dass der Host nicht überbucht wird. Genau deshalb sind diese beiden Punkte bei uns zusammen formuliert und nicht einzeln.

Warum lokales NVMe bei Datenbanken und Builds gewinnt

  • Datenbanken (PostgreSQL, MySQL): Jeder Commit ist ein fsync bei Queue Depth 1 — genau die Disziplin, in der ein Netzwerk-Hop nicht zu verstecken ist. Die Commit-Latenz sinkt, und mit ihr die Obergrenze der Transaktionsrate pro Verbindung.
  • CI/CD- und Build-Server: Zehntausende kleine Dateizugriffe für node_modules, Compiler-Caches oder Container-Layer sind fast reine Metadaten-Last. Hier addieren sich Ausreißer über die gesamte Pipeline auf.
  • Caches und Suchindizes (Redis-Persistenz, Elasticsearch): Ein AOF-Rewrite oder ein Segment-Merge ist ein Schreibsturm, der bei geteiltem Storage genau dann zuschlägt, wenn die Antwortzeiten ruhig bleiben müssen.
  • Analyse-Workloads: hoher sequenzieller Durchsatz ohne Netzwerk-Flaschenhals — und ohne Konkurrenz um denselben Uplink.
  • Alles, was Zustand hält und dessen p99 sichtbar ist: Message Queues, Session-Stores, Event-Logs.

Entscheidungshilfe

  • Nimm lokales NVMe, wenn dein Workload synchron schreibt und die Antwortzeit sichtbar ist — und plane Backups von Anfang an mit ein, nicht später.
  • Nimm Netzwerk-Storage, wenn Wiederanlaufzeit, Live-Migration, Snapshots oder Kapazität jenseits eines Hosts deine eigentlichen Anforderungen sind.
  • Nimm beides, wenn du kannst: WAL und heiße Daten lokal, kalte Daten, Archive und Backups im entkoppelten Speicher. Das ist die Aufteilung, die die meisten reifen Setups am Ende haben.
  • Und entscheide nicht nach Datenblatt: Führe die fio-Läufe von oben auf beiden Kandidaten aus, mit deiner Blockgröße und deinem Zugriffsmuster.

Häufig gestellte Fragen