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
fsckund 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.
"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.
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:
raidz3-0
12× 24 TB HDD · Rohkapazität, sequenzieller Durchsatzspecial
3-way Mirror NVMe · Metadaten + Small Blockslog
Mirror NVMe (PLP) · Sync-Write-Latenzcache
1× NVMe · Read-Cache, keine Redundanz nötigdedup
Mirror NVMe · Deduplication TableWarum 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.
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.
# 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=12setzt 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=lz4ist praktisch gratis (mehrere GB/s pro Core) und reduziert I/O auf die Spindeln. Für kalte Daten lohntzstdmit deutlich besserer Ratio bei moderatem CPU-Aufwand. Kompression ist per Dataset umstellbar und wirkt auf neue Writes.atime=offverhindert, dass jeder Lesevorgang einen Metadaten-Write auslöst.recordsizeist die maximale logische Blockgröße pro Dataset. Default 128K. Für Media/Backups1M, für Datenbanken die Page-Size der DB (z. B. 16K für InnoDB), für zvols analogvolblocksize, die nach Erstellung des zvols fix ist.
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
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.
# 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 tankein Block-Histogramm, aus dem sich Metadaten- und Small-Block-Volumen exakt ablesen lassen. special_small_blocksvs.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.
zfs send | zfs recv in ein neues Dataset) auf die neue Allocation Class.
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.
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).
# 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.
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:
# 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.
# 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
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.
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 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
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.
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.
# 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
dedup=off setzt. Die Tabelle schrumpft erst, wenn die Blöcke selbst gelöscht oder neu geschrieben werden.
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.
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.
# 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 inzpool statusist 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 -vist 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.
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 | Übernimmt | Medium | Redundanz | Bei Ausfall |
|---|---|---|---|---|
| raidz3 | Rohkapazität, sequenzieller Durchsatz | HDD | 3 Paritäten im vdev | Pool tot (jenseits Parität) |
| special | Metadaten + Small Blocks (Random-IOPS) | NVMe | Pflicht · 3-way Mirror | Pool tot |
| log | Sync-Write-Latenz (ZIL) | NVMe + PLP | Mirror empfohlen | Fallback auf ZIL-on-Pool, Latenz bricht ein |
| cache | Read-Cache hinter dem ARC | NVMe | keine nötig | Reads gehen auf HDD, transparent |
| dedup | Deduplication Table | NVMe | Pflicht · Mirror | Pool tot |
| ARC | Primärer Lese-Cache, MRU/MFU | RAM | entfällt | entfä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.
precipice.tech