🪟➡️🐧 Contexte : la machine Windows part à la retraite
Un peu d’histoire, parce que la question « pourquoi cette machine Debian quelconque a-t-elle soudain trois disques et des opinions bien arrêtées sur la compression ? » mérite une réponse. Jusqu’à récemment, un Windows Server assurait ici à la fois Active Directory et le rôle de serveur de fichiers/NAS – et rendons à César ce qui est à César : il faisait parfaitement le travail. Les connexions marchaient, les partages marchaient, personne ne se plaignait, rien ne prenait feu. Mais chaque fois que je passais de cette machine à quelque chose qui ressemblait à du DevSecOps, je sentais la friction : autre modèle mental, autres outils, autres réflexes pour « bon, comment je vois ce qui se passe ici, concrètement ? ». Le serveur Windows n’existait plus que pour le NAS et les connexions AD, pendant qu’une machine séparée faisait discrètement tout le vrai travail Linux – serveur de rebond, l’une ou l’autre tâche proche de la CI et, depuis peu, la chose qui génère cette phrase même. Deux serveurs, deux philosophies d’exploitation, et le sentiment grandissant que je conservais un petit musée de « la façon dont on faisait avant » plutôt que de gérer une installation cohérente. 🏛️ Bien sûr, j’aurais pu sortir Proxmox et contourner la décision à coups de virtualisation au lieu de la prendre vraiment – lancer un invité Windows, garder AD par habitude, appeler ça de la consolidation. Mais j’en suis arrivé au point où je préfère sincèrement tout mettre sous Linux, sur une seule machine, et l’assumer : une seule chaîne d’outils, une seule chose à patcher, une seule chose à surveiller, au lieu d’entretenir deux mondes totalement différents pour un partage de fichiers et une invite de connexion. Et c’est précisément cette consolidation qui m’a ramené à OpenZFS – si je reconstruis de toute façon le rôle de NAS de zéro sous Linux, je veux que la couche de stockage en dessous soit vraiment bonne, pas juste « assez bonne pour que l’Explorateur arrête de râler ». Donc : j’ai réinstallé une machine inutilisée avec un Debian 13 (trixie) tout frais et décidé qu’il était temps d’arrêter de traiter le stockage comme un détail – vous savez, le classique mensonge « un jour, je rangerai mes fichiers correctement » qu’on se raconte tous. Trois SSD WD Red SA500 de 2 To ont été installés, et l’objectif était simple sur le papier : un pool redondant, une compression qui se rentabilise toute seule et des partages par utilisateur qui ne se marchent pas sur les pieds. OpenZFS a rendu tout cela vraiment agréable – une fois dépassés une bizarrerie d’empaquetage, un bug cosmétique en amont qui m’a fait douter de ma santé mentale pendant dix minutes, et mon propre premier benchmark, profondément embarrassant. Voici le guide complet, défauts compris. 🐛
🤔 Pourquoi ZFS et pas NTFS ou mdadm + ext4
Version courte : je voulais des données avec sommes de contrôle, une compression transparente, des snapshots bon marché et une couche de stockage qui comprend le « dataset » comme un concept à part entière plutôt que comme un « répertoire que je promets de traiter à part, croix de bois, croix de fer ». Après des années de NTFS sur ce Windows Server et d’ext4 partout ailleurs, il vaut la peine d’expliquer concrètement ce que ZFS fait différemment au lieu d’affirmer simplement qu’il est meilleur – car sur le papier, NTFS et ext4 ont l’air très bien tous les deux. Ils se montent, ils stockent des fichiers, ils sont prêts pour la production depuis des décennies. Les différences n’apparaissent que lorsque quelque chose tourne mal en silence, ce qui est exactement le scénario qu’aucun des deux n’est conçu pour détecter. Les sommes de contrôle – la vraie fonctionnalité phare. ZFS calcule une somme de contrôle pour chaque bloc de données et chaque bloc de métadonnées, et la vérifie à chaque lecture, pas seulement lors d’une analyse planifiée occasionnelle. NTFS n’a aucune somme de contrôle pour les données – sa fonction integrity streams ne couvre que les métadonnées, est optionnelle, et a été discrètement abandonnée dans ReFS sur les versions ultérieures de Windows Server pour tout ce qui n’est pas métadonnées. ext4 est dans le même bateau : metadata_csum protège la comptabilité interne du système de fichiers, mais le contenu réel de vos fichiers est accepté les yeux fermés. Si un bit bascule quelque part sur le support physique – et sur tout disque assez grand et assez vieux, certains finiront par basculer –, NTFS et ext4 vous rendront l’octet corrompu avec le sourire et sans la moindre indication que quelque chose s’est passé. C’est la corruption silencieuse des données (bit rot), et « silencieuse » est le mot clé : pas d’erreur, pas d’entrée de journal, pas d’alerte, juste un pixel légèrement faux dans une photo ou un octet légèrement faux dans une archive de sauvegarde que personne ne remarque avant que ça compte. Autoréparation, pas seulement détection. C’est là que ZFS devance même les alternatives avec sommes de contrôle : comme il gère la redondance (RAIDZ, miroirs) et les sommes de contrôle dans la même couche, il peut non seulement dire « ce bloc est faux », mais aussi reconstruire la valeur correcte à partir de la parité et la réécrire automatiquement – lors des lectures normales, et de manière exhaustive lors d’un zpool scrub. Comparez avec la pile classique mdadm + ext4 : la couche RAID et la couche système de fichiers ne se parlent pas. mdadm peut vous dire qu’un disque entier est mort. Il ne peut pas vous dire qu’un bloc précis s’est discrètement dégradé alors que chaque disque affirme « tout va bien » – aucune somme de contrôle n’est comparée, donc il n’y a rien sur quoi être en désaccord. La corruption se propage simplement en silence dans chaque calcul de parité comme s’il s’agissait de données correctes, car du point de vue du RAID, c’en sont. Pas de « resynchronisation initiale » à la création, parce que la parité n’est pas un concept séparé. Quiconque a construit une grappe RAID5/6 avec mdadm connaît le rituel : la créer, puis attendre – parfois des heures – pendant qu’elle mouline une resynchronisation initiale de la grappe entière, en écrivant une parité cohérente sur chaque bloc, que ce bloc contienne de vraies données ou soit encore complètement vide. Il n’a pas le choix : mdadm opère purement au niveau des périphériques bloc, sous le système de fichiers, sans aucune visibilité sur les blocs « réels » et l’espace inutilisé. Il ne peut rien sauter, donc il ne saute rien. zpool create sur ces trois mêmes disques a pris, à l’inverse, quelques secondes. Pas de resynchronisation, pas d’attente, rien à rattraper – parce que dans ZFS, la parité est calculée et validée dans la même écriture atomique qui dépose les données elles-mêmes, et l’espace non alloué n’a tout simplement pas de parité qui pourrait être incohérente. Il n’y a pas d’« état initial » à corriger, car il n’existe jamais de moment où données et parité sont en désaccord. L’équivalent ZFS le plus proche d’une reconstruction RAID est un resilver – déclenché quand un disque défaillant est remplacé ou qu’un nouveau est rattaché à un miroir –, et lui aussi hérite du même avantage d’un système de fichiers qui sait ce qu’il contient : ZFS ne resilvère que les blocs effectivement alloués, pas la capacité brute totale du disque. Un pool rempli à 40 % n’a que 40 % de données à resilverer ; une reconstruction mdadm classique moulinerait quand même 100 % de la grappe, parce qu’elle ne fait pas la différence. L’équivalent proactif d’une passe check/repair de mdadm est zpool scrub – même idée, même gain d’efficacité : il parcourt et vérifie (et, contrairement à une vérification RAID bête, corrige réellement) chaque bloc alloué par rapport à sa somme de contrôle, et ignore tout ce qui n’a jamais été écrit. Copy-on-write contre journalisation. ext4 et NTFS sont tous deux des systèmes de fichiers journalisés : ils consignent les modifications de métadonnées avant de les valider, si bien qu’un plantage en pleine écriture vous laisse des métadonnées cohérentes, mais une écriture de données en cours peut quand même vous laisser un fichier à moitié écrit. ZFS n’écrase jamais les données vivantes sur place – chaque écriture va dans un nouveau bloc, et le système de fichiers bascule de manière atomique une fois qu’elle est entièrement validée. Il n’existe aucune fenêtre où un plantage pourrait laisser un fichier à moitié ancien, à moitié nouveau ; vous voyez soit le dernier état complet, soit le nouvel état complet, jamais un mélange des deux. Des snapshots vraiment bon marché. NTFS a Volume Shadow Copy, qui fonctionne mais dépend d’un service VSS séparé, a des limites pratiques sur le nombre qu’on garde raisonnablement et peut devenir sensiblement lent à mesure que les modifications s’accumulent. ext4 n’a rien de natif – vous vous rabattez sur des snapshots LVM en dessous, qui forment leur propre couche avec leur propre coût en performances à mesure qu’ils divergent de l’original. Les snapshots ZFS sont quasi instantanés (copy-on-write, encore – un snapshot signifie juste « ne libère pas encore ces blocs »), ne coûtent rien tant que les données ne changent pas, et vous pouvez sans problème en garder des dizaines sans y penser. La compression par défaut, pas par compromis. La compression NTFS existe, mais elle est ancienne, monothread et assez lente pour que l’activer sur quoi que ce soit de sensible aux performances soit une erreur que la plupart des admins ne font qu’une fois. ext4 n’a aucune compression native. La compression lz4 de ZFS, comme expliqué plus bas, est assez rapide pour que la laisser activée soit tout simplement le bon choix par défaut – pas un compromis auquel il faut réfléchir. Le stockage en pool comme modèle natif. Agrandir un volume NTFS ou un système de fichiers ext4, c’est se battre avec Storage Spaces ou LVM/mdadm respectivement – des outils séparés boulonnés en dessous, chacun avec son propre modèle mental et ses propres modes de défaillance. Les pools et datasets ZFS sont l’abstraction native, pas un module complémentaire : ajouter de la capacité, créer un nouveau dataset ou définir un quota par dataset ne sont que des sous-commandes de zpool/zfs, sans deuxième chaîne d’outils.
Fonctionnalité
NTFS
ext4
ZFS
Sommes de contrôle des données
Non
Non
Oui, chaque bloc, vérifié à chaque lecture
Autoréparation automatique
Non
Non
Oui, via redondance + sommes de contrôle combinées
Cohérence après plantage
Métadonnées journalisées uniquement
Métadonnées journalisées uniquement
Copy-on-write intégral, aucun écrasement sur place
Snapshots
VSS (limité, ralentit avec le temps)
Rien de natif (nécessite LVM)
Natifs, quasi instantanés, bon marché
Compression
Lente, rarement utilisée
Rien de natif
Rapide (lz4), activée par défaut
Stockage en pool / quotas
Storage Spaces (rajouté par-dessus)
LVM (rajouté par-dessus)
Intégré au système de fichiers
Rien de tout cela ne fait de NTFS ou d’ext4 de mauvais systèmes de fichiers – ils sont matures, bien compris, et NTFS en particulier a fait tourner ce Windows Server sans faille pendant des années. Mais « sans faille, pour autant que je sache » est exactement la formule qui devrait vous rendre nerveux face à la corruption silencieuse : tout l’intérêt, c’est qu’aucun des deux ne m’aurait prévenu si c’était arrivé. mdadm + LVM + ext4 peuvent approcher une partie des fonctionnalités de ZFS avec assez de ruban adhésif et de discipline personnelle. ZFS les a, tout simplement, piles incluses, et ne vous demande pas de faire tenir trois outils séparés par la seule force de la volonté – ni de croire que rien ne tourne jamais mal en silence sur le seul axe que personne ne surveille.
📦 Étape 1 : installer OpenZFS sur Debian 13
Debian ne livre pas ZFS dans main pour des raisons de licence (CDDL contre GPL – la plus longue querelle de l’histoire des licences logicielles, plus vieille que la plupart des disques de ce pool), il vit donc dans contrib. Ce composant n’est pas activé par défaut :
# /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
Cela installe DKMS, compile zfs.ko et spl.ko pour le noyau en cours d’exécution (6.12.107+deb13-amd64 à l’époque) et – parce que la signature des modules pour Secure Boot est désormais un sujet – génère à la volée une MOK (Machine Owner Key) autosignée pour signer les modules fraîchement compilés. Si votre machine a Secure Boot activé, vous devrez enrôler cette MOK au prochain redémarrage ; celle-ci ne l’avait pas, donc aucun souci, mais il est bon de savoir que DKMS forge discrètement ses propres identifiants de signature en arrière-plan, comme un faussaire très poli et parfaitement légal. 🔏
🏗️ Étape 2 : construire le pool
Trois disques, RAIDZ1 (un disque de parité – le bon choix pour trois disques ; RAIDZ2 demande plus de disques pour ne pas gaspiller trop de capacité). La seule règle qui compte vraiment pour moi ici : ne jamais pointer ZFS vers /dev/sdX. Les lettres de périphérique sont attribuées au démarrage et peuvent être mélangées après une mise à jour du noyau, un changement dans le BIOS ou simplement un mauvais jour de l’univers. Utilisez plutôt les chemins stables de /dev/disk/by-id/, à moins d’apprécier l’horreur très particulière de voir votre disque de parité devenir discrètement votre disque de données :
ashift=12 indique à ZFS qu’il s’agit de périphériques à secteurs de 4K (les SSD le sont universellement, même quand ils mentent dans leur firmware et annoncent des secteurs logiques de 512 octets comme si on était encore en 2009). Si vous vous trompez à la création du pool, impossible de corriger plus tard sans détruire et reconstruire le pool – c’est le seul réglage de toute cette installation sans aucun retour en arrière possible, d’où le gras qui fait peur. autotrim=on compte justement parce que ce sont des SSD ; sans lui, vous dépendez d’un zpool trim manuel périodique et laissez l’amplification d’écriture grimper discrètement en arrière-plan, comme du cholestérol dans la couche de stockage. Résultat : trois disques de 2 To, environ 5,45 To bruts, environ 3,6 To utilisables après la parité RAIDZ1. Les quelque 1,85 To manquants n’ont pas disparu – c’est le péage pour « un disque peut mourir et j’ai toujours mes données ».
🗜️ Compression : lz4, et pourquoi pas zstd
La compression ZFS est transparente, réglable par dataset et – avec lz4 – pratiquement gratuite, ce qui, en langage de geek du stockage, est ce qui se rapproche le plus d’un repas gratuit. lz4 a été conçu pour être si rapide que l’activer ne dégrade jamais le débit ; il possède aussi une heuristique d’abandon précoce qui renonce immédiatement face aux données incompressibles (vidéo déjà compressée, blobs chiffrés) au lieu de s’obstiner et de brûler du CPU pour rien. zstd compresse davantage mais coûte plus de CPU par octet. Pour une charge de NAS généraliste – documents, photos, l’image de VM occasionnelle, dépôts de code –, lz4 était le choix évident par défaut. Si c’était une cible de sauvegarde contenant surtout des dumps en texte brut, j’aurais pris zstd-3 et payé volontiers la taxe CPU. Maintenant qu’il y a de vraies données dessus au lieu de fichiers de benchmark synthétiques, voici ce que ça m’apporte réellement – et dans l’esprit du fil rouge de cet article, la réponse honnête est « pas grand-chose », ce qui est exactement ce qu’on attend une fois qu’on sait ce qui est stocké :
$ 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
Logique (non compressé)
Réel (sur disque)
Ratio
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
Des ratios à peine perceptibles, et la raison n’est pas une mauvaise configuration – ce sont les données elles-mêmes. Ce pool contient surtout des photos, des vidéos, des fichiers PST d’Outlook et des archives déjà compressées : exactement le contenu que l’heuristique d’abandon précoce de lz4 est censée reconnaître pour ne plus y gaspiller de CPU. Ce n’est pas un échec de la compression, c’est la compression qui fonctionne correctement et s’efface. Les datasets qui en profitent le plus sont storage/user2 et storage/user1, à 1,05x – toujours modeste, mais des économies réelles et gratuites sur la part de données qui se trouve être du texte ou presque (documents, fichiers de configuration, surcoût de métadonnées de centaines de milliers de petits fichiers). Si ce pool hébergeait un dump de base de données ou une pile de fichiers journaux au lieu d’archives photo familiales, ce tableau serait bien plus impressionnant – et c’est la vraie leçon : le ratio de compression en dit autant sur vos données que sur votre système de fichiers.
👻 Le bug cosmétique qui m’a fait douter de moi
J’ai défini xattr=sa à la création du pool (attributs étendus stockés directement dans le dnode au lieu d’entrées de répertoire cachées – plus rapide, et nécessaire pour des performances ACL correctes). La commande a retourné 0, aucune plainte, aucun drame. Puis zfs get xattr storage a obstinément affiché on au lieu de sa, à chaque fois, comme s’il n’avait jamais entendu parler du réglage que je venais de lui donner. Il s’avère qu’il s’agit d’un bug d’affichage connu et confirmé dans OpenZFS 2.3.2 (voir openzfs/zfs discussion #16996 si vous voulez les détails sordides) – la propriété est bel et bien correctement définie, zfs get affiche juste la mauvaise étiquette. Pas une erreur de configuration, juste du bruit cosmétique qui vous poussera immanquablement à relancer cinq fois la même commande avant de penser à chercher sur Google. 🕵️
🧠 Réglage de l’ARC : laisser de la RAM pour tout le reste
L’Adaptive Replacement Cache (ARC) de ZFS dévorera joyeusement l’essentiel de votre RAM si vous le laissez faire – ce qui est formidable sur une appliance de stockage dédiée et nettement moins sur une machine qui fait aussi tourner d’autres services et aimerait avoir son mot à dire. Sans contrôle, l’ARC se comporte exactement comme un adolescent laissé seul avec un frigo : donnez-lui de la place, il la remplira, et il n’en rendra rien de lui-même. Avec 31 Go de RAM au total, je l’ai plafonné :
16 Gio maximum, 2 Gio minimum. Appliqué à chaud via /sys/module/zfs/parameters/zfs_arc_max pour un effet immédiat, puis intégré à l’initramfs (update-initramfs -u -k all) pour survivre à un redémarrage et ne pas recommencer à piller le frigo.
🤥 Benchmarks, ou comment je me suis menti pendant cinq minutes
Au premier essai, j’ai fait ce que fait tout débutant ZFS et l’ai aussitôt regretté :
8,7 Go/s en écriture. 14,1 Go/s en lecture. Sur trois SSD SATA. Sur un réseau domestique. J’ai brièvement envisagé d’écrire un post LinkedIn bien senti sur le fait que les SSD grand public sont sous-estimés. Puis je me suis souvenu du fonctionnement de la compression : /dev/zero produit un flux infini de zéros, lz4 compresse une suite de zéros en presque rien, et je venais de mesurer mon compresseur, pas mes disques. Profondément idiot, brièvement excitant, au final dénué de sens – l’équivalent stockage de chronométrer sa vitesse de lecture d’un livre en ne lisant que les pages blanches. 📖💨 Je l’ai refait correctement avec des données incompressibles, parce que se mentir à soi-même n’est amusant qu’une fois :
Je l’ai relancé proprement une seconde fois, des semaines plus tard, une fois que le pool contenait de vraies données et – surtout – une fois que toutes les autres tâches de la machine (une migration de 380 Go, un déplacement de 235 Go entre datasets, une reconstruction des ACL) étaient vraiment terminées et que la charge moyenne était retombée près de zéro. Mesurer un pool encore occupé à digérer le travail de la veille, c’est mesurer la contention, pas les disques.
Test
Résultat
Écriture, avec tampon
138 MB/s
Écriture, O_DIRECT
15,4 MB/s
Lecture, avec tampon
1,1 GB/s
Lecture, O_DIRECT
931 MB/s
Cette ligne d’écriture n’est pas une faute de frappe, et elle m’a assez surpris pour que je vérifie deux et trois fois qu’il ne s’agissait pas de contention due à autre chose avant d’y croire : les écritures O_DIRECT étaient environ 9 fois plus lentes que les écritures avec tampon sur ce pool, soit l’inverse de l’intuition que la plupart des gens ramènent d’autres systèmes de fichiers (« l’I/O directe saute une couche, donc elle devrait être plus rapide »). Sur ZFS en particulier, cette intuition s’inverse. Le chemin avec tampon n’est pas simplement « mis en cache » – c’est là que ZFS fait sa véritable optimisation d’écriture : les écritures entrantes sont regroupées en mémoire et validées sur le vdev RAIDZ au sein d’un groupe de transactions, alignées et fusionnées en écritures efficaces sur des bandes complètes. O_DIRECT, par conception, saute exactement cette couche de regroupement, donc chaque écriture doit aller directement au vdev de son côté – ce qui, sur une grappe à parité, peut signifier des I/O plus petites, moins bien alignées, avec le surcoût qui va avec. Contourner le cache ne contourne ici aucune inefficacité réelle ; cela contourne justement ce qui rendait les écritures efficaces. Mise à jour (6 octobre 2026) : l’essentiel de cet écart de 9x ne venait finalement pas du tout de ZFS. Le firmware du serveur avait désactivé le cache d’écriture propre aux SSD, si bien que chaque écriture non regroupée devait attendre la mémoire flash elle-même (et les 138 Mo/s avec tampon étaient faibles, eux aussi). L’explication du regroupement reste vraie, mais elle était la plus petite part. L’histoire complète : Two Things My SSDs Were Missing: TRIM After BitLocker, and a Write Cache My HPE Server Quietly Switched Off (en anglais). Les chiffres de lecture ont droit au même astérisque qu’avant : il s’agit de débit monothread sur un seul fichier, pas d’une image réaliste des performances globales du pool sous charge concurrente, et les deux valeurs de lecture proviennent peut-être en partie de l’ARC plutôt que du disque – l’ARC n’est pas la même chose que le cache de pages de Linux, donc echo 3 > /proc/sys/vm/drop_caches ne fait rien pour le vider, et le chemin de lecture O_DIRECT d’OpenZFS vous servira quand même volontiers des blocs résidant dans l’ARC au lieu de forcer une vraie lecture disque si les données y sont déjà. Prenez les chiffres d’un dd isolé comme un contrôle de cohérence pour « y a-t-il quelque chose de manifestement cassé ? », pas comme une fiche technique à mettre sur une diapo commerciale – et si vous ne retenez qu’une chose de cette section, que ce soit : « ne supposez pas qu’O_DIRECT est le chemin rapide sans l’avoir mesuré d’abord sur votre système de fichiers précis ».
👨👩👧 Pourquoi chaque utilisateur a son propre dataset
Le pool sert trois personnes, chacune avec un partage Samba. La racine de chaque partage est son propre dataset ZFS (storage/user3, storage/user1, storage/user2) plutôt que trois simples répertoires dans un grand dataset. Ce n’est pas un réglage par défaut dans lequel je suis tombé – c’est un choix structurel délibéré, et il porte ses fruits d’une manière qu’une arborescence partagée ne peut tout simplement pas égaler :
Quotas.zfs set quota=500G storage/user2 plafonne un utilisateur sans toucher aux autres. Un quota est une propriété du dataset ; un répertoire n’a pas ce concept en lui-même, aussi sévèrement que vous le lui demandiez.
Snapshots indépendants.zfs snapshot storage/user2@before-cleanup permet à une personne d’annuler un malheureux moment « tout supprimer » sans embarquer les deux autres datasets dans l’aventure.
Comptabilité d’utilisation instantanée.zfs list lit l’espace utilisé directement dans les métadonnées du dataset. Pas de parcours de l’arborescence, pas de du -sh qui mouline pendant une minute et demie sur des centaines de gigaoctets de petits fichiers pendant que vous fixez un curseur clignotant en remettant en question vos choix de carrière.
Frontières de propriété nettes. Le point de montage de chaque dataset correspond 1:1 à son partage Samba, avec son propre propriétaire Unix. Les permissions du système de fichiers et celles du partage restent synchronisées au lieu de reposer uniquement sur des astuces de la couche Samba.
Réglages par dataset. Compression, atime, taille d’enregistrement – tout est modifiable par dataset si la charge d’un utilisateur justifiait un jour autre chose que la valeur par défaut du pool.
Réplication granulaire.zfs send/receive fonctionne par dataset. Vous voulez sauvegarder le partage d’une seule personne sur un disque externe sans toucher aux autres ? Une ligne, pas une liste d’exclusions rsync sélective tenue ensemble par les regrets.
Le prix de tout cela : déplacer des données entre datasets n’est plus gratuit. 😬 Un mv au sein d’un même dataset est un simple renommage – métadonnées uniquement, instantané quelle que soit la taille. Un mv entre datasets est une tout autre bête, même si les deux datasets vivent sur le même pool et les mêmes disques physiques : ZFS traite chaque dataset comme son propre système de fichiers, donc le noyau doit réellement copier chaque octet vers le nouvel emplacement puis supprimer l’original. Je l’ai réappris de la manière amusante en démêlant un fouillis de dossiers imbriqués – un aplatissement dans le même dataset s’est terminé instantanément, et un déplacement d’environ 235 Go entre datasets est resté planté là pendant presque une heure à faire, en pratique, une copie complète, pendant que je fixais un terminal sans barre de progression en me demandant si j’avais cassé quelque chose. Ce n’est pas toi, ZFS, ce sont les frontières de datasets. Bon à savoir avant d’en lancer un en attendant la même magie de trois secondes que la fois précédente.
🔑 Un admin, trois partages : pourquoi un simple chmod ne suffisait pas
Le modèle d’accès que je voulais est simple à énoncer : chacun a un accès complet à son propre dossier, et j’ai en plus un accès complet à ceux de tout le monde, à la fois sur le partage Samba et sur le système de fichiers Linux brut en dessous. Simple à énoncer – puis j’ai vraiment essayé de le construire avec le chmod classique et me suis immédiatement heurté au mur que tout sysadmin finit par heurter : un fichier Unix a exactement un propriétaire et exactement un groupe. C’est tout. C’est toute la distribution. Tous les autres tombent dans l’unique case étiquetée « other ». Ce modèle peut exprimer « le propriétaire a accès » et « ce groupe-là a accès » – il ne peut pas exprimer « user1 a accès ET user3 aussi, mais pas user2 », parce qu’il n’y a pas de deuxième emplacement de groupe où mettre user3 sans inventer aussi un groupe partagé, décider qui d’autre pourrait le rejoindre plus tard et espérer que personne n’y ajoute la mauvaise personne dans six mois. Vous pouvez contourner cela en ajoutant user3 comme membre secondaire des groupes privés de user1 et user2 et en positionnant le bit setgid pour que les nouveaux fichiers héritent du groupe – et pour beaucoup d’installations, c’est une réponse parfaitement correcte, ennuyeuse et peu coûteuse. Elle ne me plaisait pas ici pour deux raisons : elle accorde l’accès au niveau du groupe (donc elle change en silence si l’appartenance au groupe change un jour pour une raison sans rapport), et l’héritage pour les nouveaux fichiers dépend du bit setgid plus du umask qu’utilise le processus créateur – ce qui fonctionne, mais « ça marche parce que trois mécanismes distincts se sont correctement alignés » n’est pas une phrase qui inspire confiance à 2 h du matin. Les ACL POSIX résolvent le vrai problème au lieu de le contourner : elles permettent à un fichier ou un répertoire de porter des entrées utilisateur et groupe supplémentaires, nommées explicitement, au-delà du trio classique propriétaire/groupe/other. « user1 possède ceci, et user3 a aussi explicitement rwx, point » – pas de groupe partagé nécessaire, pas de spectateurs. D’abord, la prise en charge des ACL doit être activée par dataset (désactivée par défaut) :
zfs set acltype=posixacl storage/user1
zfs set acltype=posixacl storage/user2
Puis l’attribution elle-même, en deux parties – une attribution récursive pour tout ce qui existe déjà, et une ACL par défaut pour que tout ce qui sera créé à partir de maintenant hérite automatiquement de la même règle, sans dépendre de la roulette du umask :
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
Les deux mêmes lignes pour /storage/user2. Combiné aux permissions propres de chaque dossier verrouillées à 750 (propriétaire et groupe uniquement, rien pour « other » – user2 et user1 ne peuvent donc absolument pas s’aventurer dans les données l’un de l’autre, même par accident), l’état final correspond exactement à l’objectif annoncé : chaque personne a les coudées franches dans son propre dossier, je les ai dans les trois, et personne n’obtient de porte dérobée accidentelle. Il vaut la peine de vérifier l’effet réel plutôt que de croire que la commande n’a pas échoué en silence :
Et côté Samba, ce travail sur les ACL s’est révélé moins important que prévu – les trois partages utilisent déjà force user, qui fait que chaque connexion à un partage agit en tant que propriétaire de ce partage, quelle que soit la personne réellement authentifiée. Mon accès aux partages de user1 et user2 via SMB était donc déjà couvert par cette directive, à condition que le dossier sous-jacent appartienne bien à la bonne personne – ce qui, dans un joli moment d’archéologie auto-infligée, n’était brièvement pas le cas (un vestige d’une réorganisation précédente avait laissé un dataset m’appartenir au lieu de son utilisatrice réelle, m’accordant en silence un accès SMB en écriture que je n’avais pas voulu et privant la propriétaire légitime de ses propres droits par défaut, jusqu’à ce que je le remarque et corrige). Les ACL comblent spécifiquement l’autre lacune : l’accès SSH / shell local simple, qui ne passe pas du tout par force user et n’obéit qu’aux permissions Unix brutes.
😵 « Attendez, c’est encore un seul pool ? » – la confusion de df
Quelques semaines après la mise en service, un rapide df -h donnait l’impression que quelque chose avait très mal tourné :
Quatre valeurs de « Size » différentes, sur un pool censé être un bloc de stockage unifié se comportant comme un RAID5. S’est-il fragmenté en silence en pools séparés pendant la nuit ? Ai-je mal configuré quelque chose ? 😱 Non – c’est df qui a techniquement raison de la manière la moins utile possible. Les datasets ZFS n’ont pas de taille fixe comme une partition RAID classique. La colonne « Size » de df est calculée par point de montage comme Used + Available, et – c’est la partie qui compte vraiment – Available est identique pour chaque dataset, parce que c’est le même espace libre partagé dans le même pool. Seul « Used » diffère, parce que c’est réellement différent pour chaque personne. Lancez plutôt zfs list et le mystère disparaît :
Le même AVAIL – 1,85T – sur chaque ligne, parce que c’est une cagnotte commune. La « taille » df de 3,1T pour user3, c’est juste 1,20T utilisés + 1,85T disponibles ; les 1,9T de user1, c’est 14,3G + 1,85T. Rien de fragmenté, rien de perdu – dès que quelqu’un écrit un gigaoctet, ce AVAIL partagé baisse pour tout le monde en même temps, ce qui est exactement le comportement de type RAID5 attendu dès le départ. zpool list règle la question pour de bon :
$ 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
Un pool, 5,45T bruts, un vdev RAIDZ1, les trois disques dedans. df n’a simplement pas été conçu pour « plusieurs systèmes de fichiers partageant dynamiquement un pool de capacité » – il rapporte honnêtement, il répond juste à une question (« quelle est la taille de ce point de montage ? ») qui ne s’applique pas vraiment de la même façon ici. zfs list / zpool list sont les outils qui répondent réellement à « combien de place me reste-t-il ? », pas df.
📋 Aide-mémoire : les commandes que j’utilise vraiment sur ce pool
La moitié de l’apprentissage de ZFS consiste à réaliser que la sous-commande que vous cherchez existe presque certainement déjà, cachée sous un nom qui paraît parfaitement logique a posteriori. Voici la boîte à outils pour cette installation précise – pool storage, datasets storage/user3, storage/user1, storage/user2, storage/nextcloud. Santé du pool
# 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 : liste et espace utilisé
# 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
Compression : combien elle m’apporte vraiment
# 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 et réservations
# 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
Propriétés : get/set, la forme générale
# 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 (le cache en RAM)
# 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
Réplication
# 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
🚑 Dépannage : ce dont j’ai réellement eu besoin jusqu’ici
La majeure partie de la vie de ce pool a été ennuyeuse, ce qui est tout l’intérêt de ZFS – l’ennui est une fonctionnalité. Mais quelques points sont apparus pendant la construction et méritent d’être gardés sous la main pour la prochaine fois, classés ici pour que mon moi futur n’ait pas à réapprendre tout ça à la dure une seconde fois. Le pool est DEGRADED ou un disque affiche 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
Le resilver ou le scrub semble bloqué / dure une éternité
# 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
Le dataset ne se monte pas / « 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 » en déplaçant/écrivant dans le dataset d’un autre utilisateur Je suis tombé directement dessus en déplaçant les fichiers de user2 dans son dataset en tant qu’utilisateur Unix user3 – le propriétaire et le mode du dataset n’accordent pas d’accès en écriture, par conception, et ZFS n’était pas intéressé par mes excuses. Soit faire l’opération en root, soit corriger la propriété après coup :
# 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
Module noyau ZFS manquant après une mise à jour du noyau Comme il est compilé par DKMS (hors arbre), un nouveau noyau signifie que DKMS doit recompiler zfs.ko/spl.ko pour lui avant que le module puisse se charger – cela se fait normalement automatiquement dans le postinst du paquet du noyau, mais c’est la première chose à vérifier si un pool refuse de s’importer après un redémarrage et que vous êtes brièvement convaincu d’avoir perdu 3,6 To de données dans le néant :
# 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
Un service dépendant (GitLab, un conteneur, peu importe) démarre avant que le pool soit monté Symptôme : le service démarre sans problème mais écrit dans un répertoire vide du système de fichiers racine au lieu des vraies données sur ZFS, parce que son point de montage n’était pas encore prêt au démarrage – en silence, sans erreur, ce qui est le pire type de bug. Vérifiez l’ordre systemd réel au lieu de supposer que tout va bien :
# 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
De l’espace libre a « disparu » et du ne l’explique pas
# 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
L’ARC dévore toute la 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
🏁 Disposition finale
$ 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
Trois SSD, un disque de redondance, une compression transparente qui ne coûte rien et des datasets par utilisateur qui font des quotas, snapshots et sauvegardes une affaire d’une ligne plutôt qu’un projet. Pas mal pour un après-midi – même en comptant les dix minutes passées à combattre un bug qui n’existait pas et les cinq minutes passées à être impressionné par un benchmark qui était, en fait, entièrement fictif. 🎉