Un poco de historia, porque la pregunta «¿por qué esta máquina Debian cualquiera tiene de repente tres discos y opiniones firmes sobre la compresión?» merece una respuesta. Hasta hace poco, un Windows Server se encargaba aquí tanto de Active Directory como de las tareas de servidor de archivos/NAS, y hay que reconocerlo: hacía su trabajo perfectamente. Los inicios de sesión funcionaban, las carpetas compartidas funcionaban, nadie se quejaba, nada estaba en llamas. Pero cada vez que cambiaba de contexto desde esa máquina a cualquier cosa con pinta de DevSecOps, notaba la fricción: otro modelo mental, otras herramientas, otra memoria muscular para «vale, ¿cómo compruebo realmente qué está pasando aquí?». El servidor Windows existía únicamente para el NAS y los inicios de sesión de AD, mientras una máquina aparte seguía haciendo en silencio todo el trabajo de Linux de verdad: host de salto, alguna que otra tarea cercana a CI y, últimamente, también la cosa que genera esta misma frase. Dos servidores, dos filosofías de operación y la sensación creciente de que estaba conservando un pequeño museo de «así se hacía antes» en lugar de gestionar una instalación coherente. 🏛️ Claro, podría haber recurrido a Proxmox y esquivar la decisión a base de virtualización en vez de tomarla de verdad: levantar un invitado Windows, mantener AD por costumbre y llamarlo consolidación. Pero he llegado al punto en que prefiero sinceramente tenerlo todo en Linux, en una sola máquina, y apostar por ello: una sola cadena de herramientas, una sola cosa que parchear, una sola cosa que monitorizar, en lugar de mantener dos mundos totalmente distintos por una carpeta compartida y una pantalla de inicio de sesión. Y perseguir esa consolidación es exactamente lo que me devolvió a OpenZFS: si de todas formas voy a reconstruir el rol de NAS desde cero en Linux, quiero que la capa de almacenamiento que hay debajo sea buena de verdad, no solo «lo bastante buena para que el Explorador deje de quejarse». Así que reinstalé una máquina que tenía de sobra con un Debian 13 (trixie) limpio y decidí que era hora de dejar de tratar el almacenamiento como algo secundario; ya sabes, la clásica mentira de «algún día organizaré bien mis archivos» que todos nos contamos. Metí tres SSD WD Red SA500 de 2 TB, y el objetivo era sencillo sobre el papel: un pool redundante, una compresión que se pague sola y carpetas compartidas por usuario que no se pisen entre sí. OpenZFS hizo que fuera realmente agradable, una vez superados una peculiaridad del empaquetado, un bug cosmético de upstream que me hizo dudar de mi cordura durante diez minutos y mi propio primer benchmark, profundamente vergonzoso. Aquí va la guía completa, con todos sus defectos. 🐛
🤔 Por qué ZFS y no NTFS o mdadm + ext4
Versión corta: quería datos con sumas de verificación, compresión transparente, snapshots baratos y una capa de almacenamiento que entienda el «dataset» como un concepto de primera clase y no como un «directorio que prometo tratar de forma especial, palabrita del Niño Jesús». Después de años de NTFS en ese Windows Server y de ext4 en todo lo demás, merece la pena explicar de verdad qué hace ZFS de forma distinta en lugar de limitarse a afirmar que es mejor, porque sobre el papel NTFS y ext4 suenan perfectamente bien. Se montan, guardan archivos, llevan décadas listos para producción. Las diferencias solo aparecen cuando algo sale mal en silencio, que es justo el escenario para el que ninguno de los dos está diseñado. Sumas de verificación: la auténtica función estrella. ZFS calcula una suma de verificación para cada bloque de datos y cada bloque de metadatos, y la comprueba en cada lectura, no solo durante algún análisis programado ocasional. NTFS no tiene ninguna suma de verificación para datos: su función integrity streams solo ha cubierto siempre los metadatos, es opcional y se eliminó discretamente de ReFS en versiones posteriores de Windows Server para todo lo que no sean metadatos. ext4 está en el mismo barco: metadata_csum protege la contabilidad interna del sistema de archivos, pero en el contenido real de tus archivos se confía a ciegas. Si un bit cambia en algún lugar del soporte físico (y en cualquier disco lo bastante grande y viejo, algunos acabarán cambiando), NTFS y ext4 te entregarán el byte corrupto con una sonrisa y sin la menor señal de que haya pasado algo. Es la corrupción silenciosa de datos (bit rot), y «silenciosa» es la palabra clave: ningún error, ninguna entrada de log, ninguna alerta, solo un píxel ligeramente incorrecto en una foto o un byte ligeramente incorrecto en un archivo de copia de seguridad que nadie nota hasta que importa. Autorreparación, no solo detección. Aquí es donde ZFS supera incluso a las alternativas con sumas de verificación: como gestiona la redundancia (RAIDZ, espejos) y las sumas de verificación en la misma capa, no solo puede decir «este bloque está mal», sino reconstruir el valor correcto a partir de la paridad y reescribirlo automáticamente, durante las lecturas normales y de forma exhaustiva durante un zpool scrub. Compáralo con la pila clásica mdadm + ext4: la capa RAID y la capa del sistema de archivos no se hablan. mdadm puede decirte que se ha muerto un disco entero. No puede decirte que un bloque concreto se ha estropeado en silencio mientras todos los discos dicen «estoy bien»: no se compara ninguna suma de verificación, así que no hay nada en lo que discrepar. La corrupción simplemente se propaga en silencio por cada cálculo de paridad como si fueran datos correctos, porque para el RAID lo son. Sin «resincronización inicial» al crear el pool, porque la paridad no es un concepto aparte. Cualquiera que haya montado un array RAID5/6 con mdadm conoce el ritual: crearlo y luego esperar (a veces horas) mientras tritura una resincronización inicial del array entero, escribiendo paridad coherente en cada bloque, contenga datos reales o esté todavía completamente vacío. No le queda otra: mdadm trabaja puramente a nivel de dispositivo de bloques, por debajo del sistema de archivos, sin ninguna visibilidad sobre qué bloques son «reales» y cuáles son espacio sin usar. No puede saltarse nada, así que no se salta nada. zpool create sobre estos mismos tres discos, en cambio, tardó unos segundos. Sin resincronización, sin espera, nada que recuperar, porque en ZFS la paridad se calcula y se confirma como parte de la misma escritura atómica que deposita los propios datos, y el espacio no asignado simplemente no tiene paridad que pueda ser incoherente. No hay ningún «estado inicial» que corregir, porque nunca hay un momento en que datos y paridad no coincidan. Lo más parecido en ZFS a una reconstrucción RAID es un resilver (se dispara al sustituir un disco averiado o al añadir uno nuevo a un espejo), y también hereda la misma ventaja de un sistema de archivos que sabe lo que contiene: ZFS solo hace resilver de los bloques realmente asignados, no de toda la capacidad bruta del disco. Un pool lleno al 40 % solo tiene que hacer resilver del 40 % de datos; una reconstrucción mdadm tradicional trituraría el 100 % del array de todos modos, porque no distingue. El equivalente proactivo de una pasada check/repair de mdadm es zpool scrub: misma idea, misma ganancia de eficiencia. Recorre y verifica (y, a diferencia de una comprobación RAID tonta, realmente corrige) cada bloque asignado frente a su suma de verificación, y se salta todo lo que nunca se llegó a escribir. Copy-on-write frente a journaling. ext4 y NTFS son ambos sistemas de archivos con journaling: registran los cambios de metadatos antes de confirmarlos, de modo que un fallo a mitad de una escritura te deja unos metadatos coherentes, pero una escritura de datos en curso aún puede dejarte un archivo a medio escribir. ZFS nunca sobrescribe datos vivos en su sitio: cada escritura va a un bloque nuevo y el sistema de archivos cambia de forma atómica en cuanto está completamente confirmada. No existe ninguna ventana en la que un fallo pueda dejar un archivo medio viejo, medio nuevo; ves o bien el último estado completo o bien el nuevo estado completo, nunca un batiburrillo de ambos. Snapshots que son baratos de verdad. NTFS tiene Volume Shadow Copy, que funciona, pero depende de un servicio VSS aparte, tiene límites prácticos sobre cuántas copias vas a conservar de forma realista y puede volverse notablemente lento a medida que se acumulan cambios. ext4 no tiene nada nativo: acabas recurriendo a snapshots de LVM por debajo, que son su propia capa con su propio coste de rendimiento a medida que divergen del origen. Los snapshots de ZFS son casi instantáneos (otra vez copy-on-write: un snapshot solo significa «no liberes todavía estos bloques»), no cuestan nada hasta que los datos cambian de verdad y puedes tener docenas tranquilamente sin pensarlo dos veces. Compresión por defecto, no como concesión. La compresión de NTFS existe, pero es antigua, de un solo hilo y lo bastante lenta como para que activarla en algo sensible al rendimiento sea un error que la mayoría de los administradores cometen una sola vez. ext4 no tiene compresión nativa en absoluto. La compresión lz4 de ZFS, como se explica más abajo, es lo bastante rápida como para que dejarla activada sea simplemente lo correcto por defecto, no una concesión en la que tengas que pensar. Almacenamiento en pool como modelo nativo. Ampliar un volumen NTFS o un sistema de archivos ext4 implica pelearse con Storage Spaces o con LVM/mdadm, respectivamente: herramientas aparte atornilladas por debajo, cada una con su propio modelo mental y sus propios modos de fallo. Los pools y datasets de ZFS son la abstracción nativa, no un añadido: añadir capacidad, crear un dataset nuevo o fijar una cuota por dataset son solo subcomandos de zpool/zfs, sin necesidad de una segunda cadena de herramientas.
Característica
NTFS
ext4
ZFS
Sumas de verificación de datos
No
No
Sí, cada bloque, verificado en cada lectura
Autorreparación automática
No
No
Sí, mediante redundancia + sumas de verificación juntas
Coherencia tras un fallo
Solo metadatos con journaling
Solo metadatos con journaling
Copy-on-write completo, sin sobrescrituras en el sitio
Snapshots
VSS (limitado, se ralentiza con el tiempo)
Nada nativo (necesita LVM)
Nativos, casi instantáneos, baratos
Compresión
Lenta, poco usada
Nada nativo
Rápida (lz4), activada por defecto
Almacenamiento en pool / cuotas
Storage Spaces (atornillado encima)
LVM (atornillado encima)
Integrado en el sistema de archivos
Nada de esto convierte a NTFS o ext4 en malos sistemas de archivos: son maduros, se conocen bien y NTFS en particular hizo funcionar ese Windows Server sin fallos durante años. Pero «sin fallos, que yo sepa» es exactamente la expresión que debería ponerte nervioso ante la corrupción silenciosa: la clave es que ninguno de los dos me habría avisado si hubiera ocurrido. mdadm + LVM + ext4 pueden aproximarse a parte de la lista de funciones de ZFS con suficiente cinta americana y disciplina personal. ZFS simplemente las tiene, pilas incluidas, y no te pide que mantengas unidas tres herramientas distintas a base de fuerza de voluntad, ni que confíes en que nada salga mal en silencio justo en el único eje que nadie vigila.
📦 Paso 1: instalar OpenZFS en Debian 13
Debian no incluye ZFS en main por motivos de licencia (CDDL contra GPL, la disputa más larga de la historia de las licencias de software, más antigua que la mayoría de los discos de este pool), así que vive en contrib. Ese componente no está activado por defecto:
# /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
Esto instala DKMS, compila zfs.ko y spl.ko para el kernel en ejecución (6.12.107+deb13-amd64 en ese momento) y, como la firma de módulos para Secure Boot ya es algo habitual, genera sobre la marcha una MOK (Machine Owner Key) autofirmada para firmar los módulos recién compilados. Si tu máquina tiene Secure Boot activado, tendrás que registrar esa MOK en el siguiente reinicio; esta no lo tenía, así que no fue problema, pero conviene saber que DKMS está falsificando en silencio sus propias credenciales de firma en segundo plano, como un falsificador muy educado y completamente legal. 🔏
🏗️ Paso 2: construir el pool
Tres discos, RAIDZ1 (un disco de paridad, la decisión correcta para tres unidades; RAIDZ2 quiere más discos para no desperdiciar demasiada capacidad). La única regla que realmente me importa aquí: nunca apuntes ZFS a /dev/sdX. Las letras de dispositivo se asignan en el arranque y pueden barajarse tras una actualización del kernel, un cambio en la BIOS o simplemente un mal día del universo. Usa en su lugar las rutas estables de /dev/disk/by-id/, a menos que disfrutes del horror muy particular de que tu disco de paridad se convierta en silencio en tu disco de datos:
ashift=12 le dice a ZFS que son dispositivos con sectores de 4K (los SSD lo son siempre, incluso cuando mienten en su firmware e informan de sectores lógicos de 512 bytes como si siguiéramos en 2009). Si te equivocas al crear el pool, no podrás corregirlo después sin destruir y reconstruir el pool: es el único ajuste de todo este montaje que de verdad no tiene vuelta atrás, por eso se lleva la negrita que da miedo. autotrim=on importa precisamente porque son SSD; sin él dependes de un zpool trim manual periódico y dejas que la amplificación de escritura suba en silencio en segundo plano, como el colesterol de la capa de almacenamiento. Resultado final: tres unidades de 2 TB, ~5,45 TB en bruto, ~3,6 TB utilizables tras la paridad RAIDZ1. Los ~1,85 TB que faltan no se han esfumado: son el peaje de «puede morir un disco y sigo teniendo mis datos».
🗜️ Compresión: lz4, y por qué no zstd
La compresión de ZFS es transparente, configurable por dataset y, con lz4, prácticamente gratuita, que en jerga de friki del almacenamiento es lo más parecido a una comida gratis que te van a ofrecer nunca. lz4 se diseñó expresamente para ser tan rápido que activarlo nunca empeora el rendimiento; además tiene una heurística de abandono temprano que se rinde de inmediato ante datos incompresibles (vídeo ya comprimido, blobs cifrados) en lugar de empeñarse en intentarlo y quemar CPU para nada. zstd comprime más, pero cuesta más CPU por byte. Para una carga de NAS de uso general (documentos, fotos, alguna imagen de VM, repositorios de código), lz4 era la opción obvia por defecto. Si esto fuera un destino de copias de seguridad con sobre todo volcados en texto plano, habría optado por zstd-3 y pagado el impuesto de CPU con gusto. Ahora que tiene datos reales en lugar de archivos de benchmark sintéticos, esto es lo que me aporta en realidad y, fiel al hilo conductor de este artículo, la respuesta honesta es «no mucho», que es exactamente lo que cabría esperar una vez que sabes qué hay almacenado:
$ 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
Lógico (sin comprimir)
Real (en disco)
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
Ratios casi inexistentes, y la razón no es una mala configuración: son los propios datos. Este pool contiene sobre todo fotos, vídeos, archivos PST de Outlook y archivos ya comprimidos: justo el contenido que la heurística de abandono temprano de lz4 está diseñada para reconocer y dejar de malgastar CPU en él. No es un fallo de la compresión, es la compresión funcionando correctamente y apartándose. Los datasets que más se benefician son storage/user2 y storage/user1, con 1,05x: modesto, pero un ahorro real y gratuito en la parte de los datos que resulta ser texto o algo parecido (documentos, archivos de configuración, la sobrecarga de metadatos de cientos de miles de archivos pequeños). Si este pool alojara un volcado de base de datos o un montón de archivos de log en lugar de un archivo de fotos familiar, esta tabla sería mucho más impresionante, y esa es la verdadera lección: el ratio de compresión te dice tanto sobre tus datos como sobre tu sistema de archivos.
👻 El bug cosmético que me hizo dudar de mí mismo
Fijé xattr=sa al crear el pool (atributos extendidos guardados directamente en el dnode en lugar de como entradas de directorio ocultas: más rápido y necesario para un rendimiento decente de las ACL). El comando terminó con 0, sin quejas, sin drama. Luego zfs get xattr storage se empeñó en mostrar on en lugar de sa, todas y cada una de las veces, como si nunca hubiera oído hablar del ajuste que le acababa de dar. Resulta que es un bug de visualización conocido y confirmado en OpenZFS 2.3.2 (consulta openzfs/zfs discussion #16996 si quieres los detalles escabrosos): la propiedad está realmente bien configurada, zfs get solo muestra la etiqueta equivocada. No es una mala configuración, solo ruido cosmético que sin duda te hará ejecutar el mismo comando cinco veces antes de que se te ocurra buscarlo en Google. 🕵️
🧠 Ajuste del ARC: dejar RAM para todo lo demás
El Adaptive Replacement Cache (ARC) de ZFS se comerá alegremente la mayor parte de tu RAM si le dejas, lo cual es estupendo en un appliance de almacenamiento dedicado y bastante menos en una máquina que también ejecuta otros servicios y querría opinar al respecto. Sin control, el ARC se comporta exactamente como un adolescente al que dejas solo con la nevera: dale espacio, lo llenará, y no devolverá nada por iniciativa propia. Con 31 GB de RAM en total, le puse un tope:
16 GiB como máximo, 2 GiB como mínimo. Aplicado en caliente mediante /sys/module/zfs/parameters/zfs_arc_max para que surta efecto de inmediato, y después integrado en el initramfs (update-initramfs -u -k all) para que sobreviva a un reinicio y no vuelva a asaltar la nevera.
🤥 Benchmarks, o cómo me engañé a mí mismo durante cinco minutos
En la primera pasada hice lo que hace todo novato en ZFS y me arrepentí al instante:
8,7 GB/s de escritura. 14,1 GB/s de lectura. En tres SSD SATA. En una red doméstica. Por un momento pensé en escribir una publicación contundente en LinkedIn sobre lo infravalorados que están los SSD de consumo. Luego recordé cómo funciona la compresión: /dev/zero produce un flujo infinito de ceros, lz4 comprime una ristra de ceros a casi nada, y yo acababa de medir mi compresor, no mis discos. Profundamente tonto, brevemente emocionante, en definitiva sin sentido: el equivalente en almacenamiento a cronometrar lo rápido que lees un libro leyendo solo las páginas en blanco. 📖💨 Lo repetí como es debido con datos incompresibles, porque engañarse a uno mismo solo tiene gracia una vez:
Lo ejecuté bien por segunda vez, semanas después, cuando el pool ya tenía datos reales y (esto es importante) cuando todas las demás tareas de la máquina (una migración de 380 GB, un traslado de 235 GB entre datasets, una reconstrucción de ACL) habían terminado de verdad y la carga media había vuelto a estar cerca de cero. Hacer un benchmark contra un pool que sigue digiriendo el trabajo de ayer solo mide la contención, no los discos.
Prueba
Resultado
Escritura, con búfer
138 MB/s
Escritura, O_DIRECT
15,4 MB/s
Lectura, con búfer
1,1 GB/s
Lectura, O_DIRECT
931 MB/s
Esa fila de escritura no es una errata, y me sorprendió lo suficiente como para comprobar dos y tres veces que no se debía a contención por otra cosa antes de creérmela: las escrituras O_DIRECT fueron aproximadamente 9 veces más lentas que las escrituras con búfer en este pool, justo lo contrario de la intuición que la mayoría trae de otros sistemas de archivos («la E/S directa se salta una capa, así que debería ser más rápida»). En ZFS concretamente, esa intuición se invierte. La ruta con búfer no es simplemente «cacheada»: es donde ZFS hace su verdadera optimización de escritura. Las escrituras entrantes se agrupan en memoria y se confirman en el vdev RAIDZ como parte de un grupo de transacciones, alineadas y fusionadas en escrituras eficientes de franja completa. O_DIRECT, por diseño, se salta exactamente esa capa de agrupación, así que cada escritura tiene que ir directamente al vdev por su cuenta, lo que en un array con paridad puede significar E/S más pequeñas y peor alineadas, con la sobrecarga que eso conlleva. Saltarse la caché no se salta aquí ninguna ineficiencia real; se salta precisamente lo que hacía eficientes las escrituras. Actualización (6 de octubre de 2026): la mayor parte de esa diferencia de 9x resultó no tener nada que ver con ZFS. El firmware del servidor había desactivado la caché de escritura propia de los SSD, así que cada escritura no agrupada tenía que esperar a la propia memoria flash (y los 138 MB/s con búfer también eran bajos). La explicación de la agrupación sigue siendo cierta, pero era la parte menor. La historia completa: Two Things My SSDs Were Missing: TRIM After BitLocker, and a Write Cache My HPE Server Quietly Switched Off (en inglés). Las cifras de lectura llevan el mismo asterisco que antes: es rendimiento de un solo hilo y un solo archivo, no una imagen realista del rendimiento agregado del pool bajo carga concurrente, y es posible que ambas cifras de lectura salgan en parte del ARC en lugar del disco. El ARC no es lo mismo que la caché de páginas de Linux, así que echo 3 > /proc/sys/vm/drop_caches no sirve para vaciarlo, y la ruta de lectura O_DIRECT de OpenZFS te seguirá entregando tan tranquila bloques residentes en el ARC en lugar de forzar una lectura real del disco si los datos ya están ahí. Toma las cifras de un único dd como una comprobación de cordura para «¿hay algo obviamente roto?», no como una ficha técnica para una diapositiva comercial; y si solo te quedas con una cosa de esta sección, que sea: «no des por hecho que O_DIRECT es la ruta rápida sin medirlo antes en tu sistema de archivos concreto».
👨👩👧 Por qué cada usuario tiene su propio dataset
El pool da servicio a tres personas, cada una con una carpeta compartida de Samba. La raíz de cada carpeta compartida es su propio dataset de ZFS (storage/user3, storage/user1, storage/user2) en lugar de tres simples directorios dentro de un único dataset grande. No es un valor por defecto en el que caí sin más: es una decisión estructural deliberada, y compensa de formas con las que un árbol de directorios compartido sencillamente no puede competir:
Cuotas.zfs set quota=500G storage/user2 limita a un usuario sin tocar a los demás. Una cuota es una propiedad del dataset; un directorio no tiene ese concepto por sí mismo, por muy seriamente que se lo pidas.
Snapshots independientes.zfs snapshot storage/user2@before-cleanup permite a una persona deshacer un mal momento de «borrarlo todo» sin arrastrar consigo a los otros dos datasets.
Contabilidad de uso instantánea.zfs list lee el espacio usado directamente de los metadatos del dataset. Sin recorrer el árbol, sin un du -sh triturando cientos de gigabytes de archivos pequeños durante minuto y medio mientras miras un cursor parpadeante y te replanteas tu carrera profesional.
Límites de propiedad limpios. El punto de montaje de cada dataset se corresponde 1:1 con su carpeta compartida de Samba, con su propio propietario Unix. Los permisos del sistema de archivos y los de la carpeta compartida se mantienen sincronizados en lugar de depender solo de trucos en la capa de Samba.
Ajustes por dataset. Compresión, atime, tamaño de registro: todo se puede sobrescribir por dataset si la carga de algún usuario llegara a justificar algo distinto del valor por defecto del pool.
Replicación granular.zfs send/receive trabaja por dataset. ¿Quieres hacer una copia de seguridad solo de la carpeta de una persona en un disco externo sin tocar las demás? Una sola línea, no una lista selectiva de exclusiones de rsync sujeta con remordimientos.
El precio de todo esto: mover datos entre datasets ya no sale gratis. 😬 Un mv dentro de un mismo dataset es un simple cambio de nombre: solo metadatos, instantáneo sea cual sea el tamaño. Un mv entre datasets es un animal completamente distinto, aunque ambos datasets vivan en el mismo pool y en los mismos discos físicos: ZFS trata cada dataset como su propio sistema de archivos, así que el kernel tiene que copiar de verdad cada byte a la nueva ubicación y después borrar el original. Lo reaprendí por las malas mientras desenredaba un lío de carpetas anidadas: un aplanado dentro del mismo dataset terminó al instante, y un traslado de ~235 GB entre datasets se quedó ahí casi una hora haciendo, a efectos prácticos, una copia completa, mientras yo miraba un terminal sin barra de progreso preguntándome si había roto algo. No eres tú, ZFS, son los límites entre datasets. Conviene saberlo antes de lanzar uno esperando la misma magia de tres segundos que la vez anterior.
🔑 Un administrador, tres carpetas compartidas: por qué un simple chmod no bastaba
El modelo de acceso que quería es fácil de enunciar: cada uno tiene acceso completo a su propia carpeta, y yo además tengo acceso completo a las de todos, tanto en la carpeta compartida de Samba como en el sistema de archivos Linux en bruto que hay debajo. Fácil de enunciar; luego intenté de verdad construirlo con el chmod clásico y me di de bruces contra el muro con el que todo administrador de sistemas acaba chocando: un archivo Unix tiene exactamente un propietario y exactamente un grupo. Y ya está. Ese es todo el reparto. Todos los demás caen en el único cajón etiquetado como «other». Ese modelo puede expresar «el propietario tiene acceso» y «este grupo concreto tiene acceso», pero no puede expresar «user1 tiene acceso Y user3 también, pero user2 no», porque no hay un segundo hueco de grupo donde meter a user3 sin inventarse además un grupo compartido, decidir quién más podría unirse después y esperar que dentro de seis meses nadie añada a la persona equivocada. Puedes sortearlo añadiendo a user3 como miembro secundario de los grupos privados de user1 y user2 y activando el bit setgid para que los archivos nuevos hereden el grupo, y para muchas instalaciones es una solución perfectamente válida, aburrida y de poco esfuerzo. Aquí no me convencía por dos motivos: concede el acceso a nivel de grupo (así que cambia en silencio si alguna vez cambia la pertenencia al grupo por un motivo que no tiene nada que ver), y la herencia en archivos nuevos depende del bit setgid más el umask que esté usando el proceso que los crea; funciona, pero «funciona porque tres mecanismos distintos se han alineado correctamente» no es una frase que inspire confianza a las 2 de la madrugada. Las ACL POSIX resuelven el problema real en lugar de esquivarlo: permiten que un archivo o directorio lleve entradas de usuario y de grupo adicionales y nombradas explícitamente más allá del clásico trío propietario/grupo/other. «user1 es el propietario, y user3 también tiene rwx explícitamente, punto»: sin grupo compartido, sin espectadores. Primero hay que activar el soporte de ACL por dataset (desactivado por defecto):
zfs set acltype=posixacl storage/user1
zfs set acltype=posixacl storage/user2
Después, la concesión en sí, en dos partes: una concesión recursiva para todo lo que ya existe y una ACL por defecto para que todo lo que se cree a partir de ahora herede automáticamente la misma regla, sin depender de la ruleta del 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
Las mismas dos líneas para /storage/user2. Junto con los permisos propios de cada carpeta fijados en 750 (solo propietario y grupo, nada para «other», de modo que user2 y user1 no pueden meterse en los datos del otro ni siquiera por accidente), el estado final es exactamente el objetivo marcado: cada persona tiene vía libre en su propia carpeta, yo la tengo en las tres y nadie obtiene una puerta trasera accidental. Merece la pena comprobar el efecto real en lugar de confiar en que el comando no se haya quedado en nada sin avisar:
Y en el lado de Samba, este trabajo con las ACL resultó importar menos de lo esperado: las tres carpetas compartidas ya usan force user, que hace que todas las conexiones a una carpeta compartida operen como el propietario de esa carpeta, independientemente de quién se haya autenticado realmente. Así que mi acceso a las carpetas de user1 y user2 por SMB ya estaba cubierto por esa directiva, siempre que la carpeta subyacente pertenezca de verdad a la persona correcta, cosa que, en un divertido episodio de arqueología autoinfligida, durante un tiempo no ocurría (un resto de una reorganización anterior había dejado un dataset a mi nombre en lugar del de su usuaria real, concediéndome en silencio un acceso de escritura por SMB que yo no pretendía y negando a su legítima propietaria sus propios derechos por defecto hasta que lo detecté y lo arreglé). Las ACL cierran específicamente el otro hueco: el acceso simple por SSH o shell local, que no pasa en absoluto por force user y solo obedece a los permisos Unix en bruto.
😵 «Un momento, ¿esto sigue siendo un solo pool?»: la confusión de df
Unas semanas después de ponerlo en marcha, un rápido df -h hacía parecer que algo había salido muy mal:
Cuatro valores distintos de «Size» en un pool que se supone que es un único bloque de almacenamiento unificado que se comporta como un RAID5. ¿Se fragmentó en silencio en pools separados durante la noche? ¿Configuré algo mal? 😱 No: es df teniendo técnicamente razón de la forma menos útil posible. Los datasets de ZFS no tienen un tamaño fijo como una partición RAID clásica. La columna «Size» de df se calcula por punto de montaje como Used + Available y (esta es la parte que de verdad importa) Available es idéntico en todos los datasets, porque es el mismo espacio libre compartido del mismo pool. Solo «Used» cambia, porque eso sí es distinto para cada persona. Ejecuta zfs list en su lugar y deja de ser un misterio:
El mismo AVAIL (1,85T) en todas y cada una de las líneas, porque es un bote común. El «tamaño» de df de 3,1T para user3 es solo 1,20T usados + 1,85T disponibles; los 1,9T de user1 son 14,3G + 1,85T. Nada fragmentado, nada perdido: en cuanto alguien escribe un gigabyte, ese AVAIL compartido baja para todos a la vez, que es exactamente el comportamiento tipo RAID5 que se esperaba desde el principio. zpool list lo zanja definitivamente:
$ 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 en bruto, un vdev RAIDZ1, los tres discos dentro. df simplemente no se diseñó pensando en «varios sistemas de archivos que comparten dinámicamente un pool de capacidad»: informa con honestidad, solo que responde a una pregunta («¿cuánto mide este punto de montaje?») que aquí no se aplica de la misma manera. zfs list / zpool list son las herramientas que realmente responden a «¿cuánto espacio me queda?», no df.
📋 Chuleta: los comandos que de verdad uso en este pool
La mitad de aprender ZFS consiste en darse cuenta de que el subcomando que buscas casi seguro que ya existe, escondido bajo un nombre que tiene todo el sentido a posteriori. Este es el conjunto de trabajo para esta instalación concreta: pool storage, datasets storage/user3, storage/user1, storage/user2, storage/nextcloud. Estado del 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: listado y espacio usado
# 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
Compresión: cuánto me aporta realmente
# 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
Cuotas y reservas
# 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
Propiedades: get/set, la forma general
# 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 (la caché 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
Replicación
# 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
🚑 Resolución de problemas: lo que de verdad he necesitado hasta ahora
La mayor parte de la vida de este pool ha sido aburrida, que es justo de lo que se trata con ZFS: lo aburrido es una característica. Pero durante la construcción surgieron unas cuantas cosas que merece la pena tener a mano para la próxima vez, archivadas aquí para que mi yo del futuro no tenga que volver a aprender nada de esto por las malas. El pool está DEGRADED o un disco aparece como 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
El resilver o el scrub parece atascado / tarda una eternidad
# 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
El dataset no se monta / «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» al mover/escribir en el dataset de otro usuario Me topé con esto directamente al mover los archivos de user2 a su dataset como el usuario Unix user3: el propietario y el modo del dataset no conceden acceso de escritura, por diseño, y a ZFS no le interesaban mis excusas. O haces la operación como root o corriges la propiedad después:
# 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
Falta el módulo del kernel de ZFS tras una actualización del kernel Como se compila con DKMS (fuera del árbol), un kernel nuevo implica que DKMS tiene que recompilar zfs.ko/spl.ko para él antes de que el módulo cargue. Normalmente esto ocurre automáticamente en el postinst del paquete del kernel, pero es lo primero que hay que comprobar si un pool se niega a importarse tras un reinicio y por un momento estás convencido de haber perdido 3,6 TB de datos en el vacío:
# 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 servicio dependiente (GitLab, un contenedor, lo que sea) arranca antes de que el pool esté montado Síntoma: el servicio arranca sin problemas, pero escribe en un directorio vacío del sistema de archivos raíz en lugar de en los datos reales sobre ZFS, porque su punto de montaje aún no estaba listo en el arranque; en silencio, sin errores, que es el peor tipo de bug. Comprueba el orden real de systemd en lugar de dar por hecho que está 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
El espacio libre ha «desaparecido» y du no lo explica
# 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
El ARC se está comiendo toda 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
🏁 Configuración final
$ 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
Tres SSD, un disco de redundancia, compresión transparente que no cuesta nada y datasets por usuario que convierten cuotas, snapshots y copias de seguridad en una sola línea en lugar de en un proyecto. Nada mal para una tarde, incluso contando los diez minutos que pasé peleando contra un bug que no era real y los cinco minutos que pasé impresionado por un benchmark que, de hecho, era completamente ficticio. 🎉