Precipice.tech Logo precipice.tech
Start / Blog / ZFS Deep Dive
Deep Dive · Storage-Architektur

ZFS von der Checksumme bis zum Dedup vdev

Warum ein RAIDZ-Pool ohne NVMe-Schichten an 150 IOPS erstickt, wie ARC, L2ARC, SLOG und Special vdevs zusammenspielen, und welche Commands du dafür brauchst. Eine technische Dokumentation aus dem Betrieb echter Cluster.

Autor Jan Uka Stand OpenZFS 2.3 Lesezeit ~18 min Level Admin / Architekt
01

Paradigma: Dateisystem und Volume-Manager in einer Schicht

Klassische Storage-Stacks trennen Verantwortung: Der RAID-Controller verwaltet Blöcke, LVM verwaltet Volumes, das Dateisystem verwaltet Dateien. Keine Schicht weiß, was die andere tut. Der Controller kann einen defekten Block zurückliefern, ohne dass das Dateisystem es merkt, und das Dateisystem kann Daten für konsistent halten, die es längst nicht mehr sind.

ZFS kollabiert diesen Stack. Pool, Redundanz, Volumes und Dateisystem sind eine Software-Schicht mit vollem Wissen über den gesamten Datenpfad. Daraus folgen die drei Eigenschaften, auf denen alles Weitere in dieser Doku aufbaut:

  • Copy-on-Write. Kein Block wird jemals in-place überschrieben. Jede Änderung landet in neuen Blöcken, erst danach schwenkt der Block-Pointer um. Ein halb geschriebener Zustand kann per Design nicht existieren, es gibt kein fsck und kein RAID Write Hole.
  • End-to-End-Checksummen. Die Checksumme eines Blocks liegt nicht im Block selbst, sondern im übergeordneten Block-Pointer. Der gesamte Pool ist ein Merkle-Baum. Beim Lesen wird jeder Block gegen die Checksumme im Parent verifiziert.
  • Self-Healing. Schlägt die Verifikation fehl und Redundanz existiert (Mirror, RAIDZ), rekonstruiert ZFS den Block aus Parität oder Kopie, liefert die korrekte Version aus und repariert den defekten Block auf der Platte. Im laufenden Betrieb, ohne Downtime.
Abb. 1 · Silent Data Corruption: Checksummen-Mismatch wird erkannt und aus Parität repariert

"Silent Data Corruption" ist keine Theorie. Bit-Rot auf Platter-Oberflächen, defekte Kabel, RAM-Fehler auf dem RAID-Controller und Firmware-Bugs erzeugen Fehler, die klassische Stacks kommentarlos durchreichen. ZFS macht sie sichtbar und, bei vorhandener Redundanz, reversibel. Genau deshalb hat Hardware-RAID in ZFS-Umgebungen nichts verloren: Der Controller gehört in den HBA/IT-Mode, damit ZFS jede Platte einzeln sieht und Checksummen bis auf das physische Medium wirken.

02

Pool-Anatomie: vdevs verstehen, bevor man sie baut

Ein Pool besteht aus einem oder mehreren vdevs (virtual devices), die wiederum aus physischen Datenträgern bestehen. ZFS striped Daten über alle Daten-vdevs. Redundanz existiert ausschließlich innerhalb eines vdevs, nie zwischen vdevs. Daraus folgt die wichtigste Regel der ZFS-Planung:

Kritisch Fällt ein einziges Daten-, Special- oder Dedup-vdev vollständig aus, ist der gesamte Pool verloren. Redundanz ist keine Eigenschaft des Pools, sondern jedes einzelnen vdevs.
zpool: tank

raidz3-0

12× 24 TB HDD · Rohkapazität, sequenzieller Durchsatz

special

3-way Mirror NVMe · Metadaten + Small Blocks

log

Mirror NVMe (PLP) · Sync-Write-Latenz

cache

1× NVMe · Read-Cache, keine Redundanz nötig

dedup

Mirror NVMe · Deduplication Table
Daten-vdev · RAIDZ3: 12× 24 TB HDD, 3 Platten Parität. Sequenziell stark (~9 Datenplatten Streaming), aber Random-4K-IOPS ≈ eine einzelne HDD: 120–200 IOPS für den gesamten vdev.
Abb. 2 · Referenz-Topologie eines Hybrid-Pools (vdevs antippen für Details)

Warum RAIDZ bei Random-I/O einbricht

Jeder logische Block wird in einem RAIDZ-vdev als Stripe über die Member-Platten geschrieben. Ein zufälliger 4K-Read muss den Stripe koordiniert von mehreren Spindeln lesen, der vdev agiert als Verbund. Effektiv liefert ein RAIDZ-vdev die Random-IOPS einer einzelnen Member-Platte, bei HDDs also 120–200 IOPS, egal ob 6 oder 12 Platten im vdev stecken. Sequenzieller Durchsatz skaliert dagegen mit der Anzahl der Datenplatten.

Wer Random-IOPS aus Spindeln braucht, nimmt viele Mirror-vdevs statt weniger breiter RAIDZ-vdevs. Wer Kapazität braucht und den IOPS-Engpass mit NVMe-Schichten kompensiert, landet bei der Architektur aus Abb. 2. Der Rest dieser Doku beschreibt genau diese Kompensation.

03

Basis-Setup: Entscheidungen, die du nur einmal triffst

Drei Parameter sind nach der Pool-Erstellung nicht mehr oder nur mit Schmerzen änderbar und gehören deshalb vor das erste zpool create: ashift, die vdev-Geometrie und die Frage nach Special-/Dedup-vdevs.

pool-erstellung
# Immer /dev/disk/by-id/ verwenden: /dev/sdX ist nach Reboots nicht stabil
zpool create -f \
  -o ashift=12 \
  -O compression=lz4 \
  -O atime=off \
  -O xattr=sa \
  -O acltype=posixacl \
  tank raidz3 /dev/disk/by-id/wwn-0x5000c500a{1..12}

# Kontrolle: ashift pro vdev anzeigen
zdb -C tank | grep ashift
  • ashift=12 setzt die Sektorgröße auf 2¹² = 4096 Byte. Moderne HDDs und praktisch alle NVMe sind 4K-native, melden aber teils 512e. Ein zu kleiner ashift erzeugt Read-Modify-Write auf jeder Platte und ist pro vdev unveränderlich. Im Zweifel 12, für manche NVMe (8K-Pages) 13.
  • compression=lz4 ist praktisch gratis (mehrere GB/s pro Core) und reduziert I/O auf die Spindeln. Für kalte Daten lohnt zstd mit deutlich besserer Ratio bei moderatem CPU-Aufwand. Kompression ist per Dataset umstellbar und wirkt auf neue Writes.
  • atime=off verhindert, dass jeder Lesevorgang einen Metadaten-Write auslöst.
  • recordsize ist die maximale logische Blockgröße pro Dataset. Default 128K. Für Media/Backups 1M, für Datenbanken die Page-Size der DB (z. B. 16K für InnoDB), für zvols analog volblocksize, die nach Erstellung des zvols fix ist.
dataset-tuning
zfs create -o recordsize=1M  -o compression=zstd tank/backups
zfs create -o recordsize=16K -o logbias=latency tank/databases
zfs create -V 500G -o volblocksize=16K tank/vm-disk-101
04

Special vdevs: Metadaten runter von den Spindeln

Jeder Verzeichnis-Scan, jedes stat(), jeder rsync-Vergleich und jedes Öffnen kleiner Dateien ist Random-I/O auf Metadaten. Auf einem reinen RAIDZ-Pool konkurriert all das mit den Nutzdaten um die ~150 IOPS des vdevs. Special Allocation Classes lösen das strukturell: Ein Special vdev aus NVMe übernimmt sämtliche Pool-Metadaten und optional alle Blöcke unterhalb einer konfigurierbaren Schwelle.

special-vdev
# 3-way Mirror: Redundanzniveau an das Daten-vdev (RAIDZ3) angleichen
zpool add tank special mirror \
  /dev/disk/by-id/nvme-...01 \
  /dev/disk/by-id/nvme-...02 \
  /dev/disk/by-id/nvme-...03

# Blöcke ≤ 128K zusätzlich zu den Metadaten aufs Special vdev lenken
zfs set special_small_blocks=128K tank

# Auslastung der Allocation Classes prüfen
zpool list -v tank

Sizing und Fallstricke

  • Metadaten-Anteil abschätzen: Als Faustwert 0,3–1 % der belegten Poolkapazität, stark abhängig von recordsize und Dateigrößenverteilung. Für Bestandspools liefert zdb -bbb tank ein Block-Histogramm, aus dem sich Metadaten- und Small-Block-Volumen exakt ablesen lassen.
  • special_small_blocks vs. recordsize: Setzt du die Schwelle gleich der recordsize eines Datasets, landen alle Blöcke dieses Datasets auf dem Special vdev. Das kann gewollt sein (dediziertes Hot-Dataset auf Flash), läuft aber unbemerkt voll. Läuft das Special vdev voll, spillen neue Metadaten zurück auf die HDDs, und der Effekt ist weg.
  • Redundanz: Das Special vdev enthält die Block-Pointer des Pools. Sein Ausfall zerstört den Pool genauso sicher wie der Verlust des Daten-vdevs. Redundanzklasse angleichen: RAIDZ2/3-Pool → mindestens 3-way Mirror.
Hinweis Special vdevs wirken nur auf Blöcke, die nach dem Hinzufügen geschrieben werden. Bestandsdaten wandern erst durch Rewrite (z. B. zfs send | zfs recv in ein neues Dataset) auf die neue Allocation Class.
05

ARC: warum der ZFS-Cache kein LRU ist

Der Adaptive Replacement Cache ist der primäre Lese-Cache und lebt im RAM. Ein klassischer LRU-Cache hat eine bekannte Schwäche: Ein einziger sequenzieller Scan (Backup-Job, Virenscanner, find /) spült den kompletten Cache und verdrängt die Blöcke, die tatsächlich heiß sind. Der ARC verhindert genau das durch zwei konkurrierende Listen:

  • MRU (Most Recently Used): Blöcke, die einmal gelesen wurden.
  • MFU (Most Frequently Used): Blöcke, die wiederholt gelesen wurden.

Zwei "Ghost Lists" merken sich zusätzlich Checksummen kürzlich verdrängter Blöcke. Trifft ein Read einen Ghost-Eintrag, weiß ZFS, dass die Balance falsch stand, und verschiebt die Grenze zwischen MRU und MFU dynamisch. Ein Scan bläht nur die MRU-Seite auf; die MFU-Seite mit den heißen Blöcken bleibt geschützt.

Abb. 3 · Dynamische MRU/MFU-Balance unter wechselnder Last

Dimensionierung

Unter Linux nimmt der ARC per Default bis zu 50 % des RAM (zfs_arc_max=0). Auf einem Hypervisor mit VMs ist das ein sinnvoller Kompromiss. Auf einem dedizierten Storage-Knoten ist es Verschwendung. Dort gehören 80–90 % des RAM dem ARC, der Rest dem OS und den Diensten (NFS, Samba, iSCSI-Target).

arc-tuning
# Persistent: /etc/modprobe.d/zfs.conf, hier 128 GiB (Wert in Bytes)
options zfs zfs_arc_max=137438953472

# Zur Laufzeit ohne Reboot setzen
echo 137438953472 > /sys/module/zfs/parameters/zfs_arc_max

# Live-Monitoring: Hit-Rate, MRU/MFU-Verteilung, ARC-Größe
arcstat 1
arc_summary | less

Zielgröße im Betrieb: eine ARC-Hit-Rate dauerhaft über 90 % bei typischer Last. Liegt sie darunter und der RAM ist ausgereizt, ist das der Moment, über L2ARC nachzudenken, nicht früher.

06

L2ARC: der Cache, der RAM kostet

Der L2ARC ist ein Flash-Lesecache hinter dem ARC. Blöcke, die aus dem ARC verdrängt werden, wandern (gedrosselt) in den L2ARC statt komplett zu verfallen. Ein späterer Read trifft dann NVMe-Latenz statt HDD-Seek. Zwei Dinge, die der L2ARC ausdrücklich nicht tut: Er beschleunigt keine Writes, und er ersetzt keinen RAM.

Die Header-Falle

Jeder Block im L2ARC braucht einen Verwaltungs-Header im RAM-ARC, rund 70–96 Byte pro Block. Die Rechnung entscheidet, ob sich das Device lohnt:

header-overhead · rechenbeispiel
# 2 TB L2ARC, durchschnittliche Blockgröße 16K (VM-/DB-Workload):
#   2 TB / 16K            = ~125 Mio. Blöcke
#   125 Mio. × ~88 Byte   = ~11 GB RAM nur für L2ARC-Header
# Dieselben 2 TB bei 1M-Records (Media): ~2 Mio. Blöcke ≈ 176 MB RAM, unkritisch.

Ein überdimensionierter L2ARC bei kleinen Blöcken frisst den ARC, aus dem er gefüttert wird, und senkt die Gesamtperformance. Deshalb: erst RAM physisch maximieren, dann L2ARC, und die Devicegröße an die reale Blockgrößenverteilung koppeln.

l2arc
# Cache-Device hinzufügen: keine Redundanz nötig, bei Ausfall wird von HDD gelesen
zpool add tank cache /dev/disk/by-id/nvme-...

# Persistenter L2ARC (Standard seit OpenZFS 2.0): Cache überlebt Reboots
cat /sys/module/zfs/parameters/l2arc_rebuild_enabled   # 1 = aktiv

# Füllrate erhöhen (Default ist konservativ: 8 MB/s)
echo 268435456 > /sys/module/zfs/parameters/l2arc_write_max

# Auch sequenzielle/prefetched Blöcke cachen (Default: nur Random-Reads)
echo 0 > /sys/module/zfs/parameters/l2arc_noprefetch

# Wirkung prüfen
arc_summary -s l2arc
07

SLOG & ZIL: Sync-Writes ohne Latenz-Einbruch

ZFS sammelt Schreibvorgänge im RAM zu Transaction Groups (TXGs) und flusht sie standardmäßig alle 5 Sekunden als großen, sequenziellen Write auf die vdevs. Für asynchrone Writes ist das ideal. Synchrone Writes (NFS-Exporte, Datenbanken mit fsync(), VM-Images unter Proxmox) verlangen aber eine Persistenz-Bestätigung, bevor der Client weiterschreiben darf.

Dafür existiert das ZIL (ZFS Intent Log): ein Journal, das Sync-Writes sofort persistiert, damit die Bestätigung rausgehen kann, während die Daten regulär mit der nächsten TXG auf die Daten-vdevs wandern. Ohne dediziertes Device liegt das ZIL auf den Daten-vdevs selbst. Jeder Sync-Write erzwingt dann einen Seek auf den HDDs, und die Latenz explodiert. Ein SLOG (Separate Log Device) lagert das ZIL auf ein latenzoptimiertes Flash-Device aus.

Abb. 4 · Schreibpfade im Vergleich (Modus oben umschalten)

Hardware-Anforderungen

  • PLP ist Pflicht. Power Loss Protection garantiert, dass bestätigte ZIL-Einträge einen Stromausfall überleben. Consumer-NVMe ohne PLP lügen bei Flush-Kommandos oder brechen auf ehrliche, katastrophale Latenzen ein. Enterprise-NVMe oder Optane.
  • Endurance vor Kapazität. Das SLOG hält nur die Daten der letzten TXGs. Genutzt werden selten mehr als 10–20 GB, geschrieben wird permanent, deshalb zählt hohe DWPD.
  • Sizing-Formel: maximaler Sync-Durchsatz × zfs_txg_timeout (Default 5 s) × Sicherheitsfaktor 2. Bei 10 GbE also: ~1,2 GB/s × 5 s × 2 ≈ 12 GB.
  • Immer als Mirror. Fällt ein einzelnes SLOG im Betrieb aus, fängt der Pool das ab. Fällt es aber gleichzeitig mit einem Stromausfall aus, sind die letzten Sekunden bestätigter Sync-Writes verloren.
slog
# SLOG als Mirror hinzufügen
zpool add tank log mirror /dev/disk/by-id/nvme-slog1 /dev/disk/by-id/nvme-slog2

# Sync-Verhalten pro Dataset steuern
zfs set sync=always   tank/databases   # jeden Write wie fsync behandeln
zfs set sync=standard tank/vms         # Default: Applikation entscheidet

# ZIL-Aktivität live beobachten
zilstat 1
Nicht tun sync=disabled beantwortet fsync() mit einer Lüge. Auf Benchmarks sieht das spektakulär aus, bei einem Stromausfall verlierst du bestätigte Transaktionen. Bei Datenbanken und fremden VM-Daten ist das ein No-Go.
08

Dedup vdevs: Deduplikation ohne RAM-Kollaps

ZFS dedupliziert inline auf Blockebene: Jeder eingehende Block wird gehasht, der Hash gegen die DDT (Deduplication Table) geprüft. Bekannter Hash → es wird nur ein Referenzzähler erhöht statt Daten geschrieben. Bei redundanten Daten wie identischen VM-Templates oder geklonten Build-Umgebungen spart das massiv Kapazität. Der Preis ist die DDT selbst.

Faustregel: 1–5 GB DDT pro TB deduplizierter Daten, abhängig von der Blockgröße. Solange die DDT in den ARC passt, ist Dedup schnell. Passt sie nicht mehr, muss ZFS für jeden Write (und jedes Löschen!) DDT-Einträge von den Daten-vdevs nachladen. Auf HDDs bedeutet das den faktischen Stillstand des Pools. Genau hier setzen Dedup vdevs an: Die DDT wandert komplett auf dediziertes Flash. Der ARC wird entlastet, DDT-Lookups treffen NVMe-Latenz.

dedup
# VORHER simulieren: lohnt sich Dedup für diesen Datenbestand überhaupt?
zdb -S tank        # simulierte DDT + erwartete Dedup-Ratio, ohne etwas zu ändern

# Dedup vdev als Mirror hinzufügen
zpool add tank dedup mirror /dev/disk/by-id/nvme-dedup1 /dev/disk/by-id/nvme-dedup2

# Dedup gezielt pro Dataset aktivieren, nie pauschal auf dem Pool
zfs set dedup=on tank/vm-templates

# DDT-Statistik im Betrieb
zpool status -D tank
Irreversibel Dedup vdevs sind poolkritisch wie Special vdevs: Ausfall = Poolverlust, deshalb zwingend Mirror. Und einmal deduplizierte Blöcke bleiben DDT-referenziert, auch wenn du dedup=off setzt. Die Tabelle schrumpft erst, wenn die Blöcke selbst gelöscht oder neu geschrieben werden.
OpenZFS 2.3 "Fast Dedup" überarbeitet die DDT grundlegend (Log-basierte Updates, prunbare Einträge, kleinere Tabellen). Die Ökonomie von Dedup verschiebt sich damit spürbar. Die Architektur-Regeln oben (Simulation vorab, Flash für die DDT, Mirror-Pflicht) bleiben aber unverändert gültig.

Ehrliche Einordnung aus der Praxis: Bei Dedup-Ratios unter ~1,5:1 ist Kompression mit zstd fast immer die bessere Antwort: ohne DDT, ohne Poolrisiko, ohne Spezial-Hardware. Dedup lohnt dort, wo zdb -S Ratios von 3:1 und mehr nachweist.

09

Betrieb: Scrubs, Monitoring, Frühwarnung

Checksummen nützen nur, wenn sie regelmäßig geprüft werden. Ein Scrub liest jeden belegten Block, verifiziert ihn gegen den Merkle-Baum und repariert Fehler aus Redundanz, im laufenden Betrieb und mit drosselbarer Priorität. Kalte Daten, die nie gelesen werden, verrotten sonst unbemerkt, bis beim Resilver nach einem Plattenausfall plötzlich die zweite Fehlerquelle auffliegt.

betrieb
# Monatlicher Scrub (Debian/Ubuntu bringen systemd-Timer mit: zfs-scrub-monthly@)
systemctl enable --now zfs-scrub-monthly@tank.timer

# Pool-Gesundheit inkl. Fehlerzählern pro Device
zpool status -v tank

# I/O-Verteilung pro vdev live: zeigt sofort, ob Special/SLOG greifen
zpool iostat -v tank 1

# Latenz-Histogramme pro vdev (wo klemmt es wirklich?)
zpool iostat -w tank

# Ereignis-Log: Checksum-Fehler, Device-Faults, Resilver
zpool events -v

# ZED konfigurieren: Mail bei Fehlern (/etc/zfs/zed.d/zed.rc)
ZED_EMAIL_ADDR="kontakt@it-uka.de"
  • CKSUM-Zähler > 0 in zpool status ist nie normal. Einzelne Fehler → Kabel/Backplane prüfen, wiederkehrende Fehler auf einer Platte → tauschen, Fehler über viele Platten → RAM/HBA/Netzteil verdächtigen.
  • zpool iostat -v ist das schnellste Werkzeug, um die Architektur zu validieren: Metadaten-Reads müssen auf dem Special vdev landen, Sync-Writes auf dem log-vdev. Tun sie das nicht, stimmt die Konfiguration nicht.
  • Kapazitätsdisziplin: Als CoW-System braucht ZFS freie, zusammenhängende Bereiche. Ab ~85–90 % Füllstand steigt die Fragmentierung, die Allokation wird teurer, die Performance sinkt spürbar. Quotas setzen, bevor es der Pool tut.
10

Blueprint: alle Schichten auf einen Blick

Ein Hybrid-Pool ist kein Haufen Tuning-Tricks, sondern eine Arbeitsteilung: HDDs liefern Kapazität und sequenziellen Durchsatz, jede NVMe-Schicht übernimmt genau eine Klasse von I/O, an der Spindeln strukturell scheitern.

SchichtÜbernimmtMediumRedundanzBei Ausfall
raidz3Rohkapazität, sequenzieller DurchsatzHDD3 Paritäten im vdevPool tot (jenseits Parität)
specialMetadaten + Small Blocks (Random-IOPS)NVMePflicht · 3-way MirrorPool tot
logSync-Write-Latenz (ZIL)NVMe + PLPMirror empfohlenFallback auf ZIL-on-Pool, Latenz bricht ein
cacheRead-Cache hinter dem ARCNVMekeine nötigReads gehen auf HDD, transparent
dedupDeduplication TableNVMePflicht · MirrorPool tot
ARCPrimärer Lese-Cache, MRU/MFURAMentfälltentfällt

Die Reihenfolge der Investition ist dabei fast immer gleich: erst RAM (ARC), dann Special vdev, dann SLOG, wenn Sync-Workloads existieren. L2ARC und Dedup kommen nur mit nachgewiesenem Bedarf aus arc_summary bzw. zdb -S dazu. Wer die Kette umdreht, kauft Flash gegen Probleme, die er mit RAM und Metadaten-Placement für einen Bruchteil gelöst hätte.

Nächster Schritt

Storage, der diese Architektur wirklich fährt

Wir planen, bauen und betreiben ZFS- und Ceph-Backends für Virtualisierung, HPC und KI-Workloads, vom Sizing über die vdev-Topologie bis zum Monitoring. Sprechen wir über dein System.