Ein bisschen Vorgeschichte, denn die Frage „Warum hat diese x-beliebige Debian-Kiste plötzlich drei Laufwerke und eine feste Meinung zu Kompression?“ verdient eine Antwort. Bis vor Kurzem hat hier ein Windows Server sowohl Active Directory als auch die Fileserver-/NAS-Aufgaben übernommen – und Ehre, wem Ehre gebührt: Er hat das tadellos erledigt. Anmeldungen liefen, Freigaben liefen, niemand hat sich beschwert, nichts hat gebrannt. Aber jedes Mal, wenn ich von dieser Kiste in irgendetwas DevSecOps-Artiges gewechselt bin, habe ich die Reibung gespürt: anderes Denkmodell, andere Werkzeuge, anderes Muskelgedächtnis für „Okay, wie sehe ich hier eigentlich nach, was los ist?“. Der Windows-Server existierte nur noch für NAS und AD-Anmeldung, während eine separate Kiste still und leise die ganze eigentliche Linux-Arbeit erledigte – Jump-Host, die eine oder andere CI-nahe Aufgabe und neuerdings auch das Ding, das genau diesen Satz erzeugt. Zwei Server, zwei Betriebsphilosophien und das langsam wachsende Gefühl, dass ich eher ein kleines Museum für „So hat man das früher gemacht“ kuratiere, als ein stimmiges Setup zu betreiben. 🏛️ Klar, ich hätte zu Proxmox greifen und mich um die Entscheidung herumvirtualisieren können, statt sie tatsächlich zu treffen – Windows-Gast hochziehen, AD aus Gewohnheit behalten, das Ganze Konsolidierung nennen. Aber ich bin an dem Punkt, an dem ich wirklich lieber alles auf Linux habe, auf einer Kiste, und das dann konsequent: eine Toolchain, eine Sache zum Patchen, eine Sache zum Überwachen, statt zwei völlig verschiedene Welten zu pflegen, nur wegen einer Dateifreigabe und eines Login-Prompts. Und genau diese Konsolidierung hat mich zurück zu OpenZFS geführt – wenn ich die NAS-Rolle auf Linux sowieso von Grund auf neu baue, dann soll die Speicherschicht darunter auch wirklich gut sein und nicht bloß „gut genug, dass der Explorer aufhört zu meckern“. Also: Ich habe eine übrige Kiste frisch mit Debian 13 (trixie) aufgesetzt und beschlossen, dass es Zeit ist, Storage nicht mehr als Nebensache zu behandeln – du weißt schon, die klassische Lüge „Irgendwann sortiere ich meine Dateien mal ordentlich“, die wir uns alle erzählen. Drei 2-TB-SSDs vom Typ WD Red SA500 kamen rein, und das Ziel war auf dem Papier simpel: ein redundanter Pool, Kompression, die sich selbst bezahlt, und Freigaben pro Benutzer, die sich nicht gegenseitig auf die Füße treten. OpenZFS hat das richtig angenehm gemacht – nachdem ich an einer Paketierungs-Eigenheit vorbei war, an einem kosmetischen Upstream-Bug, der mich zehn Minuten lang an meinem Verstand zweifeln ließ, und an meinem eigenen, zutiefst peinlichen ersten Benchmark. Hier die komplette Anleitung, mit allen Macken. 🐛
🤔 Warum ZFS und nicht NTFS oder mdadm + ext4
Kurzfassung: Ich wollte Daten mit Prüfsummen, transparente Kompression, billige Snapshots und eine Speicherschicht, die „Dataset“ als vollwertiges Konzept versteht statt als „Verzeichnis, das ich versprochen habe, besonders zu behandeln, großes Pfadfinder-Ehrenwort“. Nach Jahren mit NTFS auf diesem Windows Server und mit ext4 überall sonst lohnt es sich, mal konkret auszuformulieren, was ZFS anders macht, statt einfach zu behaupten, es sei besser – denn auf dem Papier klingen NTFS und ext4 beide völlig in Ordnung. Sie lassen sich mounten, sie speichern Dateien, sie sind seit Jahrzehnten produktionstauglich. Die Unterschiede zeigen sich erst, wenn still und leise etwas schiefgeht – und genau für dieses Szenario ist keines von beiden gebaut. Prüfsummen – das eigentliche Hauptfeature. ZFS versieht jeden Datenblock und jeden Metadatenblock mit einer Prüfsumme und kontrolliert sie bei jedem einzelnen Lesezugriff, nicht nur bei einem gelegentlichen geplanten Scan. NTFS hat überhaupt keine Prüfsummen für Daten – das Integrity-Streams-Feature deckt immer nur Metadaten ab, ist Opt-in und wurde in späteren Windows-Server-Versionen bei ReFS für alles außer Metadaten stillschweigend gestrichen. ext4 sitzt im selben Boot: metadata_csum schützt die interne Buchhaltung des Dateisystems, aber den eigentlichen Inhalten deiner Dateien wird blind vertraut. Kippt irgendwo auf dem physischen Medium ein Bit – und bei jedem Laufwerk, das groß und alt genug ist, kippen irgendwann welche –, reichen dir NTFS und ext4 das kaputte Byte mit einem Lächeln und ohne den geringsten Hinweis, dass etwas passiert ist. Das ist stiller Bit-Rot, und „still“ ist das entscheidende Wort: kein Fehler, kein Logeintrag, kein Alarm, nur ein leicht falscher Pixel in einem Foto oder ein leicht falsches Byte in einem Backup-Archiv, das niemand bemerkt, bis es darauf ankommt. Selbstheilung, nicht nur Erkennung. Hier zieht ZFS sogar an Alternativen mit Prüfsummen vorbei: Weil es die Redundanz (RAIDZ, Mirrors) und die Prüfsummen in derselben Schicht verwaltet, kann es nicht nur sagen „dieser Block ist falsch“, sondern den korrekten Wert aus der Parität rekonstruieren und automatisch neu schreiben – bei normalen Lesezugriffen und flächendeckend bei einem zpool scrub. Vergleich das mit dem klassischen Stack aus mdadm + ext4: RAID-Schicht und Dateisystemschicht reden nicht miteinander. mdadm kann dir sagen, dass eine ganze Platte gestorben ist. Es kann dir nicht sagen, dass ein bestimmter Block still und leise kaputtgegangen ist, während jede Platte „alles gut“ meldet – es wird keine Prüfsumme verglichen, also gibt es auch nichts, worüber man uneins sein könnte. Die Beschädigung wandert einfach still durch jede Paritätsberechnung, als wären es korrekte Daten, denn aus Sicht des RAID sind sie das. Kein „initialer Resync“ beim Anlegen, weil Parität kein separates Konzept ist. Wer schon mal ein RAID5/6-Array mit mdadm gebaut hat, kennt das Ritual: anlegen und dann warten – manchmal stundenlang –, während es sich durch einen initialen Resync des gesamten Arrays mahlt und konsistente Parität über jeden einzelnen Block schreibt, egal ob dieser Block echte Daten enthält oder noch völlig leer ist. Es muss das tun: mdadm arbeitet rein auf Blockgeräte-Ebene, unterhalb des Dateisystems, ohne jeden Einblick, welche Blöcke „echt“ sind und welche ungenutzter Platz. Es kann nichts überspringen, also tut es das auch nicht. zpool create auf genau diesen drei Platten dauerte dagegen ein paar Sekunden. Kein Resync, kein Warten, nichts nachzuholen – denn bei ZFS wird die Parität als Teil desselben atomaren Schreibvorgangs berechnet und festgeschrieben, der auch die Daten selbst ablegt, und nicht belegter Platz hat schlicht keine Parität, die inkonsistent sein könnte. Es gibt keinen „Anfangszustand“ zu korrigieren, weil es nie einen Moment gibt, in dem Daten und Parität nicht übereinstimmen. Das nächstliegende ZFS-Gegenstück zu einem RAID-Rebuild ist ein Resilver – ausgelöst, wenn eine ausgefallene Platte ersetzt oder eine neue an einen Mirror gehängt wird –, und auch der erbt denselben Vorteil, dass das Dateisystem Bescheid weiß: ZFS resilvert nur die Blöcke, die tatsächlich belegt sind, nicht die volle Rohkapazität des Laufwerks. Ein Pool, der zu 40 % voll ist, muss nur 40 % an Daten resilvern; ein klassischer mdadm-Rebuild würde sich trotzdem durch 100 % des Arrays mahlen, weil er den Unterschied nicht kennt. Das proaktive Gegenstück zu einem check/repair-Durchlauf von mdadm ist zpool scrub – gleiche Idee, gleicher Effizienzgewinn: Es läuft jeden belegten Block ab, prüft ihn gegen seine Prüfsumme (und korrigiert ihn, anders als ein dummer RAID-Check, auch tatsächlich) und überspringt alles, was nie geschrieben wurde. Copy-on-Write vs. Journaling. ext4 und NTFS sind beide Journaling-Dateisysteme: Sie protokollieren Metadatenänderungen, bevor sie sie festschreiben, sodass ein Absturz mitten im Schreiben dir konsistente Metadaten hinterlässt – ein laufender Datenschreibvorgang kann dir aber trotzdem eine halb geschriebene Datei bescheren. ZFS überschreibt nie Live-Daten an Ort und Stelle – jeder Schreibvorgang landet in einem neuen Block, und das Dateisystem schaltet atomar um, sobald er vollständig festgeschrieben ist. Es gibt kein Zeitfenster, in dem ein Absturz eine Datei halb alt, halb neu hinterlassen kann; du siehst entweder den letzten vollständigen Zustand oder den neuen vollständigen Zustand, nie einen Mischmasch aus beiden. Snapshots, die wirklich billig sind. NTFS hat Volume Shadow Copy, das funktioniert, aber an einen separaten VSS-Dienst gebunden ist, praktische Grenzen dafür hat, wie viele man realistisch behält, und spürbar langsam werden kann, je mehr Änderungen sich ansammeln. ext4 hat überhaupt nichts Eingebautes – du greifst zu LVM-Snapshots darunter, die eine eigene Schicht mit eigenen Performance-Kosten sind, je weiter sie sich vom Original entfernen. ZFS-Snapshots sind nahezu sofort da (wieder Copy-on-Write – ein Snapshot heißt nur „diese Blöcke noch nicht freigeben“), kosten nichts, bis sich Daten tatsächlich ändern, und du kannst ohne weiteres Dutzende davon herumliegen lassen, ohne einen zweiten Gedanken daran zu verschwenden. Kompression als Standard, nicht als Kompromiss. NTFS-Kompression gibt es, aber sie ist alt, single-threaded und so langsam, dass sie für alles Performance-Kritische einzuschalten ein Fehler ist, den die meisten Admins genau einmal machen. ext4 hat überhaupt keine eingebaute Kompression. Die lz4-Kompression von ZFS ist, wie unten beschrieben, so schnell, dass sie eingeschaltet zu lassen schlicht der richtige Standard ist – kein Kompromiss, über den man nachdenken muss. Gepoolter Speicher als natives Modell. Ein NTFS-Volume oder ein ext4-Dateisystem zu vergrößern heißt, sich mit Storage Spaces bzw. LVM/mdadm herumzuschlagen – separate Werkzeuge, die darunter angeschraubt sind, jedes mit eigenem Denkmodell und eigenen Fehlerbildern. Pools und Datasets sind bei ZFS die native Abstraktion, kein Add-on: Kapazität hinzufügen, ein neues Dataset anlegen oder eine Quota pro Dataset setzen sind alles nur Unterbefehle von zpool/zfs, keine zweite Toolchain nötig.
Feature
NTFS
ext4
ZFS
Prüfsummen für Daten
Nein
Nein
Ja, jeder Block, bei jedem Lesen geprüft
Automatische Selbstheilung
Nein
Nein
Ja, durch Redundanz + Prüfsummen zusammen
Absturzkonsistenz
Nur Metadaten im Journal
Nur Metadaten im Journal
Vollständiges Copy-on-Write, kein Überschreiben an Ort und Stelle
Snapshots
VSS (begrenzt, wird mit der Zeit langsamer)
Nichts Eingebautes (braucht LVM)
Nativ, nahezu sofort, billig
Kompression
Langsam, selten genutzt
Nichts Eingebautes
Schnell (lz4), standardmäßig an
Gepoolter Speicher / Quotas
Storage Spaces (angeschraubt)
LVM (angeschraubt)
Teil des Dateisystems
Nichts davon macht NTFS oder ext4 zu schlechten Dateisystemen – sie sind ausgereift, gut verstanden, und gerade NTFS hat diesen Windows Server jahrelang fehlerfrei betrieben. Aber „fehlerfrei, soweit ich das beurteilen konnte“ ist genau die Formulierung, die dich bei stiller Datenkorruption nervös machen sollte: Der springende Punkt ist ja, dass keines der beiden Dateisysteme es mir gesagt hätte, wenn es passiert wäre. mdadm + LVM + ext4 können mit genug Klebeband und persönlicher Disziplin einen Teil der ZFS-Featureliste nachbilden. ZFS hat es einfach, Batterien inklusive, und verlangt nicht, dass du drei separate Werkzeuge mit purer Willenskraft zusammenhältst – oder darauf vertraust, dass auf der einen Achse, auf die niemand schaut, nie still etwas schiefgeht.
📦 Schritt 1: OpenZFS auf Debian 13 bringen
Debian liefert ZFS aus Lizenzgründen nicht in main aus (CDDL vs. GPL – die am längsten laufende Fehde der Softwarelizenzen, älter als die meisten Laufwerke in diesem Pool), also wohnt es in contrib. Diese Komponente ist standardmäßig nicht aktiviert:
# /etc/apt/sources.list — add contrib
deb http://deb.debian.org/debian trixie main contrib
deb http://deb.debian.org/debian trixie-updates main contrib
deb http://security.debian.org/debian-security trixie-security main contrib
Das zieht DKMS mit herein, baut zfs.ko und spl.ko gegen den laufenden Kernel (damals 6.12.107+deb13-amd64) und erzeugt – weil Modulsignierung für Secure Boot inzwischen ein Thema ist – im Vorbeigehen einen selbstsignierten MOK (Machine Owner Key), um die frisch gebauten Module zu signieren. Wenn deine Kiste Secure Boot aktiviert hat, musst du diesen MOK beim nächsten Neustart registrieren; diese hier hatte es nicht, also war es kein Thema, aber es ist gut zu wissen, dass DKMS im Hintergrund still seine eigenen Signatur-Zugangsdaten fälscht, wie ein sehr höflicher, absolut legaler Geldfälscher. 🔏
🏗️ Schritt 2: Den Pool bauen
Drei Platten, RAIDZ1 (eine Platte Parität – die richtige Wahl für drei Laufwerke; RAIDZ2 will mehr Spindeln, um nicht zu viel Kapazität zu verschwenden). Die eine Regel, die mir hier wirklich wichtig ist: Lass ZFS niemals auf /dev/sdX los. Gerätebuchstaben werden beim Booten vergeben und können nach einem Kernel-Update, einer BIOS-Änderung oder einfach einem schlechten Tag des Universums durcheinandergewürfelt werden. Nimm stattdessen die stabilen Pfade unter /dev/disk/by-id/, außer du stehst auf den ganz speziellen Horror, dass deine Paritätsplatte still und leise zur Datenplatte wird:
ashift=12 sagt ZFS, dass es sich um Geräte mit 4K-Sektoren handelt (SSDs sind das grundsätzlich, auch wenn sie in ihrer Firmware lügen und 512-Byte-Logiksektoren melden, als wäre noch 2009). Wenn du das beim Anlegen des Pools falsch setzt, kannst du es später nicht mehr korrigieren, ohne den Pool zu zerstören und neu zu bauen – das ist die eine Einstellung in diesem ganzen Aufbau, bei der es wirklich kein Zurück gibt, deshalb bekommt sie den gruseligen Fettdruck. autotrim=on ist speziell deshalb wichtig, weil es SSDs sind; ohne das verlässt du dich auf ein regelmäßiges manuelles zpool trim und lässt die Write Amplification still im Hintergrund ansteigen, wie Cholesterin in der Speicherschicht. Ergebnis: drei 2-TB-Laufwerke, ~5,45 TB roh, ~3,6 TB nutzbar nach RAIDZ1-Parität. Die fehlenden ~1,85 TB sind nicht verschwunden – das ist die Mautgebühr für „Ein Laufwerk darf sterben, und ich habe meine Daten trotzdem noch“.
🗜️ Kompression: lz4, und warum nicht zstd
ZFS-Kompression ist transparent, pro Dataset einstellbar und – mit lz4 – praktisch kostenlos, was in Storage-Nerd-Sprache das Nächste an einem Gratis-Mittagessen ist, das dir je angeboten wird. lz4 wurde gezielt so gebaut, dass es so schnell ist, dass das Einschalten nie den Durchsatz verschlechtert; außerdem hat es eine Early-Abort-Heuristik, die bei nicht komprimierbaren Daten (bereits komprimiertes Video, verschlüsselte Blobs) sofort aufgibt, statt es stur trotzdem zu versuchen und CPU für nichts zu verbrennen. zstd komprimiert stärker, kostet aber mehr CPU pro Byte. Für einen Allzweck-NAS-Workload – Dokumente, Fotos, das eine oder andere VM-Image, Code-Repos – war lz4 der offensichtliche Standard. Wäre das hier ein Backup-Ziel mit überwiegend Klartext-Dumps, hätte ich stattdessen zu zstd-3 gegriffen und die CPU-Steuer gern bezahlt. Jetzt, wo echte Daten drauf liegen statt synthetischer Benchmark-Dateien, hier, was es mir tatsächlich bringt – und ganz im Sinne des roten Fadens dieses Beitrags lautet die ehrliche Antwort „nicht viel“, und genau das würde man erwarten, sobald man weiß, was da eigentlich gespeichert ist:
$ zfs get compressratio storage storage/user3 storage/user1 storage/user2 storage/nextcloud
NAME PROPERTY VALUE
storage compressratio 1.01x
storage/user3 compressratio 1.00x
storage/user1 compressratio 1.05x
storage/user2 compressratio 1.05x
storage/nextcloud compressratio 1.03x
Dataset
Logisch (unkomprimiert)
Tatsächlich (auf der Platte)
Verhältnis
storage/user3
1.21T
1.20T
1.00x
storage/user1
15.0G
14.3G
1.05x
storage/user2
247G
236G
1.05x
storage/nextcloud
385G
375G
1.03x
Kaum messbare Verhältnisse, und der Grund ist keine Fehlkonfiguration – es sind die Daten selbst. Dieser Pool besteht überwiegend aus Fotos, Videos, Outlook-PST-Dateien und bereits komprimierten Archiven: genau die Inhalte, die die Early-Abort-Heuristik von lz4 erkennen soll, um keine CPU mehr daran zu verschwenden. Das ist kein Versagen der Kompression, sondern Kompression, die korrekt arbeitet und sich aus dem Weg nimmt. Am ehesten profitieren storage/user2 und storage/user1 mit 1,05x – immer noch bescheiden, aber echte, kostenlose Einsparungen auf dem Teil der Daten, der zufällig textähnlich ist (Dokumente, Konfigurationsdateien, der Metadaten-Overhead von Hunderttausenden kleiner Dateien). Würde dieser Pool einen Datenbank-Dump oder einen Haufen Logdateien beherbergen statt eines Familienfotoarchivs, sähe diese Tabelle deutlich beeindruckender aus – und das ist die eigentliche Lektion: Das Kompressionsverhältnis verrät dir genauso viel über deine Daten wie über dein Dateisystem.
👻 Der kosmetische Bug, der mich an mir zweifeln ließ
Ich habe beim Anlegen des Pools xattr=sa gesetzt (erweiterte Attribute direkt im Dnode statt als versteckte Verzeichniseinträge – schneller und nötig für ordentliche ACL-Performance). Der Befehl endete mit 0, keine Beschwerden, kein Drama. Dann meldete zfs get xattr storage stur on statt sa, jedes einzelne Mal, als hätte es noch nie von der Einstellung gehört, die ich ihm gerade gegeben hatte. Es stellte sich heraus, dass das ein bekannter, bestätigter Anzeigefehler in OpenZFS 2.3.2 ist (siehe openzfs/zfs discussion #16996, wenn du die blutigen Details willst) – die Eigenschaft ist tatsächlich korrekt gesetzt, zfs get zeigt nur das falsche Label an. Keine Fehlkonfiguration, nur kosmetisches Rauschen, das dich garantiert dazu bringt, denselben Befehl fünfmal auszuführen, bevor du auf die Idee kommst, es zu googeln. 🕵️
🧠 ARC-Tuning: RAM für alles andere übrig lassen
Der Adaptive Replacement Cache (ARC) von ZFS frisst fröhlich den Großteil deines RAMs, wenn du ihn lässt – was auf einer dedizierten Storage-Appliance super ist und deutlich weniger super auf einer Kiste, die auch noch andere Dienste betreibt und da gern ein Wörtchen mitreden würde. Unkontrolliert verhält sich der ARC genau wie ein Teenager, den man allein mit einem Kühlschrank lässt: Gib ihm Platz, er wird ihn füllen, und er wird von sich aus nichts wieder hergeben. Bei 31 GB RAM insgesamt habe ich ihn gedeckelt:
16 GiB maximal, 2 GiB Untergrenze. Live über /sys/module/zfs/parameters/zfs_arc_max angewendet, damit es sofort wirkt, und dann in die Initramfs eingebacken (update-initramfs -u -k all), damit es einen Neustart überlebt und nicht einfach wieder den Kühlschrank plündert.
🤥 Benchmarking, oder: Wie ich mich fünf Minuten lang selbst belogen habe
Beim ersten Durchgang habe ich das gemacht, was jeder ZFS-Neuling macht und sofort bereut:
8,7 GB/s Schreiben. 14,1 GB/s Lesen. Auf drei SATA-SSDs. In einem Heimnetz. Kurz habe ich überlegt, einen markigen LinkedIn-Beitrag darüber zu schreiben, wie unterschätzt Consumer-SSDs sind. Dann fiel mir ein, wie Kompression funktioniert: /dev/zero liefert einen endlosen Strom aus Nullen, lz4 komprimiert eine Folge von Nullen auf fast nichts, und ich hatte gerade meinen Kompressor gebenchmarkt, nicht meine Platten. Zutiefst albern, kurz aufregend, letztlich bedeutungslos – das Storage-Gegenstück dazu, zu messen, wie schnell man ein Buch liest, indem man nur die leeren Seiten liest. 📖💨 Also richtig wiederholt, mit nicht komprimierbaren Daten, denn sich selbst zu belügen macht nur einmal Spaß:
Ein zweites Mal sauber durchgeführt, Wochen später, als der Pool echte Daten hatte und – wichtig – als jeder andere Job auf der Kiste (eine 380-GB-Migration, ein 235-GB-Umzug zwischen Datasets, ein ACL-Neuaufbau) tatsächlich fertig war und die Load Average sich wieder nahe null eingependelt hatte. Einen Pool zu benchmarken, der noch damit beschäftigt ist, die Arbeit von gestern zu verdauen, misst nur Konkurrenz um Ressourcen, nicht die Platten.
Test
Ergebnis
Schreiben, gepuffert
138 MB/s
Schreiben, O_DIRECT
15,4 MB/s
Lesen, gepuffert
1,1 GB/s
Lesen, O_DIRECT
931 MB/s
Die Schreibzeile ist kein Tippfehler, und sie hat mich so überrascht, dass ich doppelt und dreifach geprüft habe, dass es keine Konkurrenz durch etwas anderes war, bevor ich ihr geglaubt habe: O_DIRECT-Schreibvorgänge waren auf diesem Pool ungefähr 9-mal langsamer als gepufferte, was das Gegenteil der Intuition ist, die die meisten von anderen Dateisystemen mitbringen („Direct I/O überspringt eine Schicht, also sollte es schneller sein“). Speziell bei ZFS kehrt sich diese Intuition um. Der gepufferte Pfad ist nicht einfach nur „gecacht“ – dort macht ZFS seine eigentliche Schreiboptimierung: Eingehende Schreibvorgänge werden im Speicher gesammelt und als Teil einer Transaktionsgruppe auf das RAIDZ-Vdev geschrieben, ausgerichtet und zu effizienten Full-Stripe-Writes zusammengefasst. O_DIRECT überspringt per Design genau diese Sammelschicht, also muss jeder Schreibvorgang einzeln direkt aufs Vdev – was auf einem paritätsbasierten Array kleinere, schlechter ausgerichtete I/O mit dem entsprechenden Overhead bedeuten kann. Den Cache zu umgehen umgeht hier keine echte Ineffizienz; es umgeht genau das, was die Schreibvorgänge überhaupt erst effizient gemacht hat. Update (6. Oktober 2026): Der Großteil dieser 9-fachen Lücke lag am Ende gar nicht an ZFS. Die Server-Firmware hatte den eigenen Schreibcache der SSDs abgeschaltet, sodass jeder nicht gesammelte Schreibvorgang auf den Flash selbst warten musste (und auch die gepufferten 138 MB/s waren niedrig). Die Erklärung mit dem Sammeln stimmt weiterhin, war aber der kleinere Teil. Die ganze Geschichte: Two Things My SSDs Were Missing: TRIM After BitLocker, and a Write Cache My HPE Server Quietly Switched Off (auf Englisch). Die Lesewerte bekommen dasselbe Sternchen wie zuvor: Das ist Single-Thread-Durchsatz mit einer einzelnen Datei, kein realistisches Bild der Gesamtleistung des Pools unter paralleler Last, und beide Lesewerte stammen womöglich teilweise aus dem ARC statt von der Platte – der ARC ist nicht dasselbe wie der Page Cache von Linux, also bewirkt echo 3 > /proc/sys/vm/drop_caches nichts, um ihn zu leeren, und der O_DIRECT-Lesepfad von OpenZFS reicht dir trotzdem fröhlich Blöcke aus dem ARC, statt einen echten Plattenzugriff zu erzwingen, wenn die Daten schon dort liegen. Nimm einzelne dd-Werte als Plausibilitätscheck für „Ist irgendwas offensichtlich kaputt?“, nicht als Datenblatt für eine Vertriebsfolie – und wenn du dir aus diesem Abschnitt nur eine Sache merkst, dann diese: „Geh nicht davon aus, dass O_DIRECT der schnelle Pfad ist, ohne es vorher auf deinem konkreten Dateisystem zu messen.“
👨👩👧 Warum jeder Benutzer sein eigenes Dataset bekommt
Der Pool bedient drei Personen, jede mit einer Samba-Freigabe. Die Wurzel jeder Freigabe ist ein eigenes ZFS-Dataset (storage/user3, storage/user1, storage/user2) statt drei schlichter Verzeichnisse in einem großen Dataset. Das war kein Standard, in den ich hineingerutscht bin – es ist eine bewusste strukturelle Entscheidung, und sie zahlt sich auf eine Weise aus, mit der ein gemeinsamer Verzeichnisbaum einfach nicht mithalten kann:
Quotas.zfs set quota=500G storage/user2 begrenzt einen Benutzer, ohne die anderen anzufassen. Eine Quota ist eine Eigenschaft des Datasets; ein Verzeichnis kennt so ein Konzept von sich aus nicht, egal wie streng du es darum bittest.
Unabhängige Snapshots.zfs snapshot storage/user2@before-cleanup lässt eine Person einen missglückten „Alles löschen“-Moment zurückrollen, ohne die anderen beiden Datasets mitzuschleifen.
Sofortige Belegungsübersicht.zfs list liest die Speicherbelegung direkt aus den Metadaten des Datasets. Kein Durchlaufen des Baums, kein du -sh, das sich anderthalb Minuten durch Hunderte Gigabyte kleiner Dateien mahlt, während du auf einen blinkenden Cursor starrst und deine Berufswahl hinterfragst.
Saubere Besitzgrenzen. Der Mountpoint jedes Datasets passt 1:1 zu seiner Samba-Freigabe, mit eigenem Unix-Besitzer. Dateisystem- und Freigabeberechtigungen bleiben synchron, statt sich allein auf Tricks in der Samba-Schicht zu verlassen.
Tuning pro Dataset. Kompression, atime, Recordsize – alles pro Dataset überschreibbar, falls der Workload eines Benutzers je etwas anderes als den Pool-Standard rechtfertigen sollte.
Granulare Replikation.zfs send/receive arbeitet pro Dataset. Du willst nur die Freigabe einer Person auf eine externe Platte sichern, ohne die anderen anzufassen? Ein Einzeiler, keine selektive rsync-Ausschlussliste, die von Reue zusammengehalten wird.
Der Preis für all das: Daten zwischen Datasets zu verschieben ist nicht mehr kostenlos. 😬 mv innerhalb eines Datasets ist ein reines Umbenennen – nur Metadaten, sofort, egal wie groß. mv über Datasets hinweg ist ein völlig anderes Tier, obwohl beide Datasets auf demselben Pool und denselben physischen Platten liegen: ZFS behandelt jedes Dataset als eigenes Dateisystem, also muss der Kernel tatsächlich jedes Byte an den neuen Ort kopieren und dann das Original löschen. Das habe ich auf die lustige Art neu gelernt, als ich ein Durcheinander verschachtelter Ordner entwirrt habe – das Zusammenlegen der Ordnerebenen innerhalb desselben Datasets war sofort fertig, und ein Umzug von ~235 GB zwischen Datasets saß den Großteil einer Stunde da und machte praktisch eine vollständige Kopie, während ich auf ein Terminal ohne Fortschrittsanzeige starrte und mich fragte, ob ich etwas kaputt gemacht hatte. Es liegt nicht an dir, ZFS, es liegt an den Dataset-Grenzen. Gut zu wissen, bevor du so einen Umzug anstößt und Drei-Sekunden-Magie wie beim letzten Mal erwartest.
🔑 Ein Admin, drei Freigaben: Warum schlichtes chmod das nicht konnte
Das Zugriffsmodell, das ich wollte, ist schnell ausgesprochen: Jeder bekommt vollen Zugriff auf seinen eigenen Ordner, und ich bekomme zusätzlich vollen Zugriff auf alle, sowohl über die Samba-Freigabe als auch auf dem rohen Linux-Dateisystem darunter. Schnell ausgesprochen – und dann habe ich tatsächlich versucht, es mit klassischem chmod zu bauen, und bin sofort gegen die Wand gelaufen, gegen die jeder Sysadmin irgendwann läuft: Eine Unix-Datei hat genau einen Besitzer und genau eine Gruppe. Das war’s. Das ist die gesamte Besetzung. Alle anderen landen in dem einen Topf mit der Aufschrift „other“. Dieses Modell kann „Besitzer bekommt Zugriff“ und „diese eine Gruppe bekommt Zugriff“ ausdrücken – es kann nicht „user1 bekommt Zugriff UND user3 auch, aber user2 nicht“ ausdrücken, weil es keinen zweiten Gruppenplatz gibt, in den man user3 stecken könnte, ohne gleich eine gemeinsame Gruppe zu erfinden, zu entscheiden, wer da später noch beitreten darf, und zu hoffen, dass in sechs Monaten niemand die falsche Person hinzufügt. Du kannst das umgehen, indem du user3 als sekundäres Mitglied in die privaten Gruppen von user1 und user2 aufnimmst und das Setgid-Bit setzt, damit neue Dateien die Gruppe erben – und für viele Setups ist das eine völlig ausreichende, langweilige, aufwandsarme Lösung. Mir gefiel sie hier aus zwei Gründen nicht: Sie gewährt Zugriff auf Gruppen-Ebene (ändert sich also still mit, falls sich die Gruppenmitgliedschaft je aus einem ganz anderen Grund ändert), und die Vererbung bei neuen Dateien hängt vom Setgid-Bit plus der Umask ab, die der erzeugende Prozess zufällig verwendet – was funktioniert, aber „funktioniert, weil drei separate Mechanismen zufällig korrekt zusammenpassen“ ist kein Satz, der um 2 Uhr nachts Vertrauen einflößt. POSIX-ACLs lösen das eigentliche Problem, statt es zu umschiffen: Sie erlauben einer Datei oder einem Verzeichnis, zusätzliche, explizit benannte Benutzer- und Gruppeneinträge über das klassische Trio Besitzer/Gruppe/other hinaus zu tragen. „user1 gehört das, und user3 bekommt ausdrücklich auch rwx, Punkt“ – keine gemeinsame Gruppe nötig, keine Unbeteiligten. Zuerst muss die ACL-Unterstützung pro Dataset eingeschaltet werden (standardmäßig aus):
zfs set acltype=posixacl storage/user1
zfs set acltype=posixacl storage/user2
Dann die eigentliche Berechtigung, in zwei Teilen – eine rekursive Freigabe für alles, was schon existiert, und eine Default-ACL, damit alles, was ab jetzt angelegt wird, automatisch dieselbe Regel erbt, ohne sich auf Umask-Roulette zu verlassen:
setfacl -R -m u:user3:rwx /storage/user1 # apply to everything that exists now
setfacl -R -d -m u:user3:rwx /storage/user1 # apply to everything created from now on
Dieselben zwei Zeilen für /storage/user2. Zusammen mit den eigenen Berechtigungen jedes Ordners, festgezurrt auf 750 (nur Besitzer und Gruppe, nichts für „other“ – sodass user2 und user1 grundsätzlich nicht in die Daten des jeweils anderen hineinspazieren können, nicht mal aus Versehen), ist der Endzustand genau das erklärte Ziel: Jede Person hat freie Hand in ihrem eigenen Ordner, ich habe freie Hand in allen dreien, und niemand bekommt eine versehentliche Hintertür. Es lohnt sich, die tatsächliche Wirkung zu überprüfen, statt darauf zu vertrauen, dass der Befehl nicht still ins Leere gelaufen ist:
Und auf Samba-Seite war diese ACL-Arbeit weniger wichtig als erwartet – die drei Freigaben verwenden bereits force user, wodurch jede Verbindung zu einer Freigabe als deren Besitzer arbeitet, egal wer sich tatsächlich angemeldet hat. Mein Zugriff auf die Freigaben von user1 und user2 über SMB war also schon durch diese Direktive abgedeckt, vorausgesetzt, der zugrunde liegende Ordner gehört tatsächlich der richtigen Person – was er, in einem netten Stück selbstverschuldeter Archäologie, kurzzeitig nicht tat (ein Überbleibsel einer früheren Umorganisation hatte ein Dataset mir statt seiner eigentlichen Benutzerin zugeordnet, was mir still SMB-Schreibzugriff gewährte, den ich nicht beabsichtigt hatte, und der rechtmäßigen Besitzerin ihre eigenen Standardrechte verweigerte, bis ich es bemerkt und behoben hatte). Die ACLs schließen speziell die andere Lücke: schlichten SSH-/lokalen Shell-Zugriff, der gar nicht über force user läuft und allein den rohen Unix-Berechtigungen gehorcht.
😵 „Moment, ist das überhaupt noch ein Pool?“ – die df-Verwirrung
Ein paar Wochen nach Inbetriebnahme sah es nach einem schnellen df -h so aus, als wäre etwas gewaltig schiefgegangen:
Vier verschiedene „Size“-Werte auf einem Pool, der eigentlich ein einheitlicher Speicherblock sein soll, der sich wie ein RAID5 verhält. Ist er über Nacht still in separate Pools zerfallen? Habe ich etwas falsch konfiguriert? 😱 Nein – das ist df, das auf die denkbar wenig hilfreiche Weise technisch korrekt ist. ZFS-Datasets haben keine feste Größe wie eine klassische RAID-Partition. Die „Size“-Spalte von df wird pro Mount als Used + Available berechnet, und – das ist der Teil, auf den es wirklich ankommt – Available ist bei jedem Dataset identisch, weil es derselbe gemeinsame freie Platz im selben Pool ist. Nur „Used“ unterscheidet sich, weil das pro Person tatsächlich verschieden ist. Nimm stattdessen zfs list, und das Rätsel löst sich auf:
Dasselbe AVAIL – 1,85T – in jeder einzelnen Zeile, weil es ein gemeinsamer Topf ist. Die df-„Größe“ von user3 mit 3,1T sind einfach 1,20T belegt + 1,85T frei; die 1,9T von user1 sind 14,3G + 1,85T. Nichts ist zerfallen, nichts ging verloren – sobald irgendwer ein Gigabyte schreibt, sinkt dieses gemeinsame AVAIL für alle gleichzeitig, und das ist genau das RAID5-artige Verhalten, das von Anfang an erwartet war. zpool list klärt es endgültig:
$ zpool list storage
NAME SIZE ALLOC FREE CKPOINT EXPANDSZ FRAG CAP DEDUP HEALTH
storage 5.45T 2.50T 2.95T - - 0% 45% 1.00x ONLINE
Ein Pool, 5,45T roh, ein RAIDZ1-Vdev, alle drei Platten darin. df wurde einfach nicht für „mehrere Dateisysteme, die sich dynamisch einen Kapazitätspool teilen“ gebaut – es berichtet ehrlich, beantwortet aber eine Frage („Wie groß ist dieser Mount?“), die hier nicht auf dieselbe Weise gilt. zfs list / zpool list sind die Werkzeuge, die tatsächlich „Wie viel Platz habe ich?“ beantworten, nicht df.
📋 Cheatsheet: die Befehle, die ich auf diesem Pool wirklich benutze
Die Hälfte des ZFS-Lernens besteht in der Erkenntnis, dass der Unterbefehl, den du willst, mit ziemlicher Sicherheit schon existiert – versteckt unter einem Namen, der im Nachhinein völlig logisch ist. Hier das Arbeitsset für genau dieses Setup – Pool storage, Datasets storage/user3, storage/user1, storage/user2, storage/nextcloud. Zustand des Pools
# Is everything online? Any read/write/checksum errors?
zpool status storage
# One-line health summary, good for a cron/monitoring check
zpool status -x
# I/O activity per vdev, refreshed every 2s
zpool iostat -v storage 2
# Manually kick a trim outside the autotrim schedule
zpool trim storage
# Integrity scrub — reads and verifies every block against its checksum
zpool scrub storage
zpool status storage # shows progress while it's running
Datasets: Auflistung und Speicherbelegung
# All datasets under the pool, with usage — instant, no tree walk
zfs list -r storage
# Include snapshots in the listing
zfs list -t all -r storage
# Just one dataset
zfs list storage/user2
Kompression: Wie viel bringt sie mir tatsächlich?
# The headline number: ratio of logical (pre-compression) to physical size
zfs get compressratio storage
zfs get compressratio storage/user3 storage/user1 storage/user2 storage/nextcloud
# Same thing, the manual way — logicalused is what it WOULD take uncompressed,
# used is what it actually takes on disk
zfs get logicalused,used storage
# Which compression algorithm is actually active per dataset
zfs get compression storage/user3 storage/nextcloud
Quotas und Reservierungen
# Cap a user's dataset so they can't eat the whole pool
zfs set quota=500G storage/user2
# Guarantee a dataset a minimum amount of space, even if others fill up
zfs set reservation=100G storage/nextcloud
# See what's currently set
zfs get quota,reservation -r storage
Snapshots
# Take one
zfs snapshot storage/user2@2026-09-01-before-cleanup
# List them
zfs list -t snapshot -r storage
# Roll back to one (destroys anything written after it — says so loudly for a reason)
zfs rollback storage/user2@2026-09-01-before-cleanup
# Recover a single file without a full rollback — snapshots are browsable here
ls /storage/user2/.zfs/snapshot/2026-09-01-before-cleanup/
# Done with it
zfs destroy storage/user2@2026-09-01-before-cleanup
Eigenschaften: get/set, die allgemeine Form
# Everything set on a dataset, and where each value comes from
# (default / inherited / local override)
zfs get all storage/user3 | grep -v default
# Set anything per-dataset — overrides pool default for that subtree only
zfs set atime=off storage/nextcloud
zfs set recordsize=1M storage/nextcloud # bigger records suit large sequential files
ARC (der RAM-Cache)
# Live stats: hit rate, size, current bounds
cat /proc/spl/kstat/zfs/arcstats | grep -E '^(size|c_max|c_min|hits|misses) '
# Or the friendlier summary, if arcstat is installed
arcstat 2
Replikation
# Full send of a dataset to another pool/host, piped over SSH
zfs snapshot storage/user2@backup-2026-09-01
zfs send storage/user2@backup-2026-09-01 | ssh backup-host zfs receive backuppool/user2
# Incremental send — only what changed since the last snapshot
zfs send -i storage/user2@backup-2026-08-01 storage/user2@backup-2026-09-01 | ssh backup-host zfs receive backuppool/user2
🚑 Troubleshooting: was ich bisher tatsächlich gebraucht habe
Den größten Teil seines Lebens war dieser Pool langweilig, und genau darum geht es bei ZFS – langweilig ist ein Feature. Aber beim Aufbau kamen ein paar Dinge auf, die es wert sind, fürs nächste Mal griffbereit zu sein, hier abgelegt, damit mein zukünftiges Ich nichts davon zweimal auf die harte Tour neu lernen muss. Pool ist DEGRADED oder eine Platte zeigt FAULTED
# First stop: what exactly is wrong, and with which vdev/disk
zpool status -v storage
# Confirm the disk is still physically present and matches the pool's idea of it
ls -la /dev/disk/by-id/ | grep WD_Red
# Once the disk is physically replaced, tell ZFS to resilver the new one in
zpool replace storage ata-WD_Red_SA500_2.5_2TB_OLDSERIAL ata-WD_Red_SA500_2.5_2TB_NEWSERIAL
# Watch the resilver progress
zpool status storage
# If a disk had a transient error but is actually fine (loose cable, one-off glitch),
# clear the error counters instead of replacing anything
zpool clear storage
Resilver oder Scrub scheint zu hängen / dauert ewig
# Check it's actually making progress, not hung
zpool status storage # look at the % done and time-remaining estimate, run twice a few minutes apart
# See if something else is hammering the pool's I/O concurrently
zpool iostat -v storage 2
# Scrub/resilver speed is throttled by design so it doesn't starve normal I/O;
# these bump the priority if you genuinely need it to finish faster
cat /sys/module/zfs/parameters/zfs_scan_vdev_limit
echo 33554432 > /sys/module/zfs/parameters/zfs_scan_vdev_limit # example: raise the per-vdev scan limit
Dataset lässt sich nicht mounten / „filesystem already mounted“ / „dataset is busy“
# What does ZFS think is mounted where
zfs mount
# Force ZFS to (re)mount everything it thinks should be mounted
zfs mount -a
# "Device or resource busy" on unmount usually means an open file handle —
# find who's holding it
lsof +D /storage/user2
fuser -vm /storage/user2
# Last resort: force-unmount (drops any open handles, use with care)
umount -l /storage/user2
„Permission denied“ beim Verschieben/Schreiben in das Dataset eines anderen Benutzers Genau darauf bin ich gestoßen, als ich die Dateien von user2 als Unix-Benutzer user3 in ihr Dataset verschoben habe – Besitzer und Modus des Datasets gewähren absichtlich keinen Schreibzugriff, und ZFS interessierte sich nicht für meine Ausreden. Entweder die Aktion als root ausführen oder die Besitzrechte hinterher korrigieren:
# Check who actually owns the mountpoint and what the mode is
ls -ld /storage/user2
# Do the privileged operation, then hand ownership back if root touched it
chown -R user2:user2 /storage/user2
ZFS-Kernelmodul fehlt nach einem Kernel-Update Da das per DKMS gebaut wird (out-of-tree), bedeutet ein neuer Kernel, dass DKMS zfs.ko/spl.ko dagegen neu bauen muss, bevor das Modul lädt – das passiert normalerweise automatisch im Postinst des Kernel-Pakets, ist aber das Erste, was du prüfen solltest, wenn sich ein Pool nach einem Neustart weigert, importiert zu werden, und du kurz überzeugt bist, 3,6 TB Daten ans Nichts verloren zu haben:
# Is the module actually loaded for the running kernel?
lsmod | grep zfs
# Does DKMS have a built module for this exact kernel version?
dkms status
# If not, force a rebuild against the current kernel
dkms install zfs/$(dkms status zfs | head -1 | cut -d, -f2 | tr -d ' ') -k $(uname -r)
# Then try importing again
zpool import storage
Ein abhängiger Dienst (GitLab, ein Container, egal was) startet, bevor der Pool gemountet ist Symptom: Der Dienst kommt sauber hoch, schreibt aber in ein leeres Verzeichnis auf dem Root-Dateisystem statt in die echten Daten auf ZFS, weil sein Mountpoint beim Booten noch nicht bereit war – still, ohne Fehler, und das ist die schlimmste Art von Bug. Prüf die tatsächliche systemd-Reihenfolge, statt anzunehmen, dass sie passt:
# Does local-fs.target really wait for this dataset's mount unit?
systemctl show local-fs.target -p After | tr ' ' '\n' | grep storage
# Is the mount unit itself enabled and correctly ordered before local-fs.target?
systemctl show storage-user2.mount -p Before,After,WantedBy
# And does the dependent service start late enough (after multi-user.target,
# which itself sits after local-fs.target)?
systemctl show gitlab-runsvdir.service -p After
Freier Platz ist „verschwunden“, und du erklärt es nicht
# Snapshots hold on to space for blocks that changed since they were taken —
# a forgotten snapshot is the single most common cause of "where did my space go"
zfs list -t snapshot -r storage -o name,used,creation
# See exactly how much of a dataset's usage is snapshots vs. live data
zfs get usedbydataset,usedbysnapshots storage/user2
Der ARC frisst den ganzen RAM
# Confirm it's actually ARC and not something else
free -h
cat /proc/spl/kstat/zfs/arcstats | grep '^size '
# Confirm the cap is actually applied (compare against zfs_arc_max in modprobe.d)
cat /sys/module/zfs/parameters/zfs_arc_max
# If the modprobe.d change hasn't taken effect yet, push it live without a reboot
echo 17179869184 > /sys/module/zfs/parameters/zfs_arc_max
🏁 Endgültiges Layout
$ zpool status storage
pool: storage
state: ONLINE
config:
NAME STATE
storage ONLINE
raidz1-0 ONLINE
ata-WD_Red_SA500_2.5_2TB_25273ZD00458 ONLINE
ata-WD_Red_SA500_2.5_2TB_25273ZD01387 ONLINE
ata-WD_Red_SA500_2.5_2TB_25273ZD02050 ONLINE
errors: No known data errors
$ zfs list -r storage
NAME USED AVAIL REFER MOUNTPOINT
storage ... 3.5T ... /storage
storage/user3 ... 3.5T ... /storage/user3
storage/user1 ... 3.5T ... /storage/user1
storage/user2 ... 3.5T ... /storage/user2
Drei SSDs, eine Platte Redundanz, transparente Kompression, die nichts kostet, und Datasets pro Benutzer, die Quotas, Snapshots und Backups zum Einzeiler statt zum Projekt machen. Nicht schlecht für einen Nachmittag – selbst wenn man die zehn Minuten mitzählt, die ich gegen einen Bug gekämpft habe, der nicht echt war, und die fünf Minuten, in denen mich ein Benchmark beeindruckt hat, der in Wahrheit komplett erfunden war. 🎉
📚 Mehr aus dieser Reihe: das RAIDZ1-NAS (auf Englisch)