Seguridad de zfs destroy: las comprobaciones que evitan «Ups, dataset equivocado»

¿Te fue útil?

Eventualmente alguien ejecuta zfs destroy con prisa. No porque sea temerario, sino porque producción suena, los discos se llenan a las 2 a.m. y los nombres de dataset parecen creados por comité. ZFS es potente, pero no es tu niñera. El sistema borrará exactamente lo que pediste, no lo que quisiste decir.

Esta es la guía práctica para evitar el cráter del “dataset equivocado”. Repasaremos las salvaguardas reales que ofrece ZFS, las trampas que no cubre y las comprobaciones repetibles que mantienen las eliminaciones aburridas.

Qué hace realmente zfs destroy (y qué se niega a hacer)

zfs destroy elimina un dataset ZFS: un filesystem, un volumen (zvol), un snapshot o un bookmark. Esa eliminación está impulsada por metadatos; ZFS no “borra bloques” al estilo antiguo. Simplemente elimina referencias, y el espacio queda disponible una vez que nada más referencia esos bloques.

Eso suena reversible. En la práctica, casi no lo es. ZFS no incluye una “papelera” para datasets. Si no hiciste snapshot o replicaste en otro sitio, “deshacer” es una máquina del tiempo que olvidaste construir.

Objetivos de destrucción y flags que debes respetar

  • Filesystem: tank/app — tiene un mountpoint y puede contener hijos.
  • Snapshot: tank/app@2025-12-26 — registro inmutable; eliminarlo libera bloques referenciados que ningún otro snapshot/clone retenga.
  • Volume (zvol): tank/vm01 — dispositivo de bloques bajo /dev/zvol; usado frecuentemente por hipervisores.
  • Bookmark: tank/app#bk1 — puntero para send/receive; pequeño, pero con significado de dependencia.

Los flags son donde la seguridad existe o muere:

  • -r destruye hijos (filesystems, snapshots bajo un dataset) de forma recursiva. Genial cuando es intencional. Catastrófico cuando te equivocaste.
  • -R destruye dependientes, incluidos clones. Este es el botón de “realmente lo digo en serio” —y también el botón de “no sabía que existían clones”.
  • -f fuerza el unmount al destruir un filesystem (donde esté soportado). Si usas -f a la ligera, ya estás fuera del camino feliz.
  • -n (simulación) existe en algunas plataformas/versiones para algunas operaciones, pero no dependas universalmente de él para el comportamiento de destroy. Tu flujo seguro no debería apoyarse en que un flag esté presente en todas partes.

Una verdad incómoda: zfs destroy no es “peligroso” porque sea impredecible. Es peligroso porque es perfectamente predecible. Si tu selección del dataset objetivo es descuidada, ZFS ejecutará fielmente tu descuido a velocidad de red.

Un chiste corto, porque todos necesitamos uno: ZFS no tiene “papelera”, tiene “remordimiento de reciclaje”.

Por qué ocurre “dataset equivocado” en la vida real

La mayoría de incidentes por destrucción equivocada no son un solo error tipográfico. Son una cadena de pequeñas suposiciones:

  • Asumes que mountpoint equivale a identidad del dataset. No siempre (los mountpoints pueden heredarse, cambiarse o ponerse en legacy).
  • Asumes que “este host” es el propietario del dataset. Quizá esté importado en otro lado, replicado o gestionado por HA.
  • Asumes que “nadie lo usa” porque nadie lo mencionó. Mientras tanto, un contenedor, hipervisor o agente de backup lo está usando en silencio.
  • Asumes que los hijos son inofensivos. Entonces -r borra diez datasets que olvidaste que existían.
  • Asumes que los snapshots son backups. No lo son, a menos que tengas una segunda copia.

Salvaguardas integradas: clones, holds, montajes ocupados y permisos

ZFS sí proporciona salvaguardas reales. No siempre están presentes cuando las necesitas, y no cubren la categoría de “apunté a lo incorrecto”. Pero entenderlas cambia de inmediato tu postura de seguridad.

1) Las dependencias de clones bloquean la eliminación de snapshots

Si un snapshot tiene un clone, ZFS no te dejará destruir el snapshot a menos que resuelvas la dependencia del clone (o uses una operación de destrucción dependiente). Esta es una de las pocas veces que ZFS te salva de ti mismo.

Pero también es una trampa: podrías pensar que estás borrando “un snapshot antiguo” para liberar espacio. ZFS se niega, añades -R para “hacer que funcione” y de repente también destruiste un clone que alguien usaba para pruebas, analítica o —mi favorito personal— datos “temporales” que se volvieron permanentes hace tres trimestres.

2) Holds: lo más cercano a un seguro

Un hold en un snapshot evita su eliminación hasta que se libere explícitamente. Los holds son simples, baratos y extremadamente efectivos para la seguridad. También están poco usados porque se sienten “extra”. En producción, “extra” es el punto.

3) Datasets y montajes ocupados pueden resistir la destrucción

Un filesystem montado y en uso puede fallar al desmontarse limpiamente. Dependiendo de la plataforma y opciones, ZFS puede negarse a destruirlo, o puedes “arreglarlo” con -f y desmontar algo que una aplicación aún necesita. Cualquiera de los dos resultados es informativo.

Si destroy falla porque está ocupado, no es ZFS siendo molesto. Es ZFS diciéndote: “Oye, un proceso está adjunto; quizá verifica que no vas a borrar una carga de trabajo en vivo.” Escucha.

4) La delegación de permisos puede salvarte a ti (y también frenarte)

ZFS soporta delegación: puedes permitir que un rol haga snapshots pero no destrucciones; o destruir solo dentro de un subárbol; o requerir privilegios elevados para las “herramientas afiladas”. Las organizaciones que tratan zfs destroy como rm -rf / —es decir, no todo el mundo puede hacerlo— tienden a tener menos eventos que limitan carreras.

5) Nombres, ascendencia y la “sorpresa de los hijos”

Los datasets ZFS son jerárquicos. Los humanos no lo son. Bajo presión, la gente ve tank/app y no nota tank/app/cache, tank/app/log, tank/app/old y tank/app/jenkins. Entonces -r hace una limpieza total. ZFS está haciendo exactamente lo que pediste.

Hechos e historia que explican el comportamiento actual

Algunos puntos de contexto que hacen el comportamiento de zfs destroy menos misterioso y más predecible:

  1. ZFS se originó en Sun Microsystems a mediados de los 2000, diseñado para hacer la gestión de almacenamiento más como software que como ritual.
  2. Copy-on-write es el mecánico central: ZFS nunca sobrescribe bloques en vivo en su lugar. Esto hace que los snapshots sean baratos y que las eliminaciones se basen principalmente en contadores de referencias.
  3. Los snapshots no son un almacén separado. Son una vista puntual; el espacio solo se libera cuando los bloques dejan de estar referenciados por todos los snapshots/clones.
  4. Los clones se diseñaron para aprovisionamiento rápido (piensa en dev/test, plantillas de VM). La consecuencia de seguridad: los snapshots pueden volverse “indelebles” hasta que resuelvas los dependientes.
  5. Los primeros ZFS enfatizaban la claridad administrativa mediante propiedades e herencia, lo cual es genial—hasta que las propiedades heredadas hacen que dos datasets parezcan idénticos a simple vista.
  6. zfs destroy es rápido porque trabaja con metadatos. La eliminación rápida es una característica. El coste de seguridad es que debes frenarte a ti mismo.
  7. OpenZFS creció en múltiples sistemas operativos. Algunos flags/comportamientos difieren sutilmente según plataforma y versión; los runbooks portables deben evitar asumir una capacidad CLI exacta.
  8. La separación “zpool/zfs” (operaciones de pool vs dataset) refleja la arquitectura: las operaciones de pool son más físicas, las de dataset más lógicas. Los incidentes por dataset equivocado suelen ocurrir en la capa lógica.

La ingeniería de confiabilidad tiene una cosmovisión para esto. Aquí va una idea parafraseada frecuentemente atribuida a James Hamilton (AWS): Opera con la suposición de que la falla es normal; construye sistemas y procesos que la toleren. Eso incluye el fallo del operador.

Guion de diagnóstico rápido (cuando necesitas espacio ya)

Este es el “estoy de guardia, el pool está al 95% y alguien está a punto de sugerir borrar algo” secuencia. El objetivo: encontrar el verdadero consumidor de espacio y confirmar la identidad del dataset antes de cualquier acción destructiva.

Primero: confirma la salud del pool y la presión real

  1. Revisa el estado del pool (errores, degradado, resilvering pueden distorsionar expectativas).
  2. Revisa uso lógico vs físico (compresión, snapshots, special vdevs pueden cambiar la historia).
  3. Identifica los datasets principales por espacio usado y separa “datos en vivo” de “datos retenidos por snapshots”.

Segundo: localiza “espacio retenido por snapshots” frente a “espacio del head actual”

  1. Usa usedbysnapshots a nivel de dataset.
  2. Lista snapshots ordenados por usado si está disponible.
  3. Revisa clones/holds que bloquearán la eliminación o la volverán arriesgada.

Tercero: valida que el dataset sea el que pretendes

  1. Confirma el mountpoint y dónde está montado (incluyendo montajes legacy).
  2. Confirma que no es un zvol que respalde una VM (busca consumidores).
  3. Confirma que no está recibiendo replicación (destruir un destino de receive a mitad de una operación es una forma fácil de hacer una noche larga).

Si sigues solo una regla: nunca decidas “qué destruir” basándote exclusivamente en una ruta como /srv/app. Decide según el nombre del dataset y las propiedades, luego mapea a rutas.

Tareas prácticas: comandos, salidas y decisiones

Estas son tareas reales que puedes ejecutar en producción. Cada una incluye una salida de ejemplo y la decisión que deberías tomar a partir de ella. Ajusta nombres de pool/dataset a tu entorno.

Task 1: Verifica que el pool esté sano antes de tocar nada

cr0x@server:~$ zpool status -x
all pools are healthy

Qué significa: No hay fallos conocidos a nivel de pool en este momento.

Decisión: Procede con el diagnóstico normalmente. Si esto muestra degraded/faulted, detente y atiende hardware/resilvering primero; borrar datos durante un evento de fallo de pool convierte “apretado” en “irrecuperable”.

Task 2: Obtén una vista rápida de “qué está usando espacio” entre datasets

cr0x@server:~$ zfs list -o name,used,available,refer,mountpoint -S used tank
NAME                USED  AVAIL  REFER  MOUNTPOINT
tank/backup         7.12T  1.80T  7.12T  /tank/backup
tank/app            1.04T  1.80T   120G  /srv/app
tank/app/log        320G  1.80T   320G  /srv/app/log
tank/app/cache      210G  1.80T   210G  /srv/app/cache
tank/home            96G  1.80T    96G  /home

Qué significa: USED incluye snapshots/hijos; REFER es el head actual del dataset (excluyendo hijos).

Decisión: Si USED es enorme pero REFER es pequeño, probablemente la historia sean snapshots/hijos. No destruyas “el dataset” para arreglar “bloat de snapshots”.

Task 3: Separa espacio retenido por snapshots del dato en vivo

cr0x@server:~$ zfs get -o name,property,value -H used,usedbysnapshots,usedbychildren tank/app
tank/app	used	1.04T
tank/app	usedbysnapshots	880G
tank/app	usedbychildren	40G

Qué significa: La mayor parte del espacio está retenida por snapshots (usedbysnapshots), no por el filesystem en vivo.

Decisión: Revisa la política de snapshots y elimina snapshots específicos (con cuidado) en lugar de destruir el dataset. Si destruyes el dataset podrías borrar datos en vivo más sus descendientes. El exceso sigue siendo mortal.

Task 4: Lista snapshots y ve qué vas a tocar realmente

cr0x@server:~$ zfs list -t snapshot -o name,used,refer,creation -S used tank/app
NAME                         USED  REFER  CREATION
tank/app@hourly-2025-12-26    22G   120G   Fri Dec 26 02:00 2025
tank/app@hourly-2025-12-25    21G   118G   Thu Dec 25 23:00 2025
tank/app@daily-2025-12-20     19G   110G   Sat Dec 20 01:00 2025

Qué significa: USED del snapshot es el espacio único retenido por ese snapshot.

Decisión: Elimina snapshots conforme a una política de retención conocida. Si no puedes explicar por qué existe un snapshot, asume que existe por una razón y encuentra esa razón antes de borrar.

Task 5: Comprueba holds en snapshots (tu etiqueta de “no borrar”)

cr0x@server:~$ zfs holds tank/app@daily-2025-12-20
NAME                        TAG     TIMESTAMP
tank/app@daily-2025-12-20    legal   Wed Dec 24 10:13 2025

Qué significa: Un hold llamado legal evita la destrucción de este snapshot.

Decisión: Alto. Esto es gobernanza o intención operativa explícita. Encuentra al propietario y obtén aprobación antes de liberar holds.

Task 6: Comprueba clones que hagan riesgosa la eliminación de snapshots

cr0x@server:~$ zfs get -H -o value clones tank/app@daily-2025-12-20
tank/devclone

Qué significa: El snapshot tiene un clone dependiente tank/devclone.

Decisión: No uses -R como reflejo. Confirma si tank/devclone está en uso, y si puedes promoverlo o hacerle snapshot antes de cualquier cadena de eliminación.

Task 7: Confirma identidad y ascendencia del dataset antes de cualquier destroy

cr0x@server:~$ zfs list -r -o name,mountpoint,canmount,readonly tank/app
NAME            MOUNTPOINT     CANMOUNT  RDONLY
tank/app        /srv/app       on        off
tank/app/cache  /srv/app/cache on        off
tank/app/log    /srv/app/log   on        off

Qué significa: Este es el subtree exacto que afectarás con -r. Sin adivinanzas.

Decisión: Si pretendes borrar solo logs, apunta a tank/app/log, no a tank/app. Si tu intención era borrar toda la app, asegúrate de que cada hijo sea esperado.

Task 8: Mapea un mountpoint de vuelta a un dataset (deja de borrar por ruta)

cr0x@server:~$ findmnt -T /srv/app -o TARGET,SOURCE,FSTYPE,OPTIONS
TARGET   SOURCE         FSTYPE  OPTIONS
/srv/app tank/app       zfs     rw,xattr,noacl

Qué significa: El filesystem montado en /srv/app es tank/app.

Decisión: Usa el nombre del dataset en todos los comandos y aprobaciones. Si SOURCE no es lo que esperabas, detente e investiga. Las rutas mienten; los nombres ZFS no.

Task 9: Comprueba si un dataset está en uso activo (antes de forzar unmount)

cr0x@server:~$ lsof +f -- /srv/app | head
COMMAND   PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
nginx    1421 root  cwd    DIR  0,105     4096    2 /srv/app
java     1880 app   txt    REG  0,105 12345678    7 /srv/app/bin/app.jar

Qué significa: Procesos están usando archivos bajo este mount.

Decisión: No destruyas ni fuerces desmontaje. Coordina el paro de la aplicación, o apunta a snapshots en lugar del filesystem en vivo.

Task 10: Confirma si tratas con un zvol (riesgo VM)

cr0x@server:~$ zfs list -t volume -o name,used,volsize,readonly tank | head
NAME         USED  VOLSIZE  RDONLY
tank/vm01    220G   256G    off
tank/vm02    180G   256G    off

Qué significa: Estos datasets son dispositivos de bloque. Destruirlos es una amputación instantánea de VM.

Decisión: Requiere una verificación explícita de propiedad de la VM antes de cualquier destroy. Trata los volúmenes como de mayor riesgo que los filesystems porque el consumidor suele ser un hipervisor, no un humano.

Task 11: Revisa propiedades del dataset que cambian lo “obvio”

cr0x@server:~$ zfs get -o name,property,value -H mountpoint,canmount,origin,receive_resume_token tank/app
tank/app	mountpoint	/srv/app
tank/app	canmount	on
tank/app	origin	-
tank/app	receive_resume_token	-

Qué significa: No es un clone (origin es -), y no está en reanudación de un receive.

Decisión: Si origin está establecido, entiende la línea de clones antes de eliminar. Si receive_resume_token está presente, la replicación está/estuvo en curso — destruir puede complicar la recuperación o futuros sends incrementales.

Task 12: Usa zfs destroy en un snapshot (de forma concreta), no en un dataset (de forma amplia)

cr0x@server:~$ zfs destroy tank/app@hourly-2025-12-25

Qué significa: Snapshot eliminado (si no lo bloquean holds/clones). La falta de salida en éxito es típica.

Decisión: Prefiere eliminar snapshots específicos para liberar espacio en lugar de borrar datasets enteros. Estás recortando, no decapitando.

Task 13: Verifica que la eliminación realmente devolvió espacio (y si podía hacerlo)

cr0x@server:~$ zfs get -o name,property,value -H usedbysnapshots tank/app
tank/app	usedbysnapshots	840G

Qué significa: El espacio retenido por snapshots bajó de 880G a 840G en nuestro ejemplo anterior, por lo que el espacio se está liberando como se esperaba.

Decisión: Si el espacio no baja, probablemente borraste snapshots que no retenían datos únicos, o clones/otros snapshots todavía referencian los bloques. Sigue diagnosticando en lugar de “intentar más destroys”.

Task 14: Comprueba qué snapshots realmente están reteniendo espacio (vista a nivel dataset)

cr0x@server:~$ zfs list -t snapshot -o name,used -S used -r tank/app | head
NAME                         USED
tank/app@hourly-2025-12-26    22G
tank/app@hourly-2025-12-24    22G
tank/app@hourly-2025-12-23    21G
tank/app@daily-2025-12-20     19G

Qué significa: Los mayores deltas únicos de snapshot están arriba.

Decisión: Elimina en orden descendente por used solo si coincide con la política y las necesidades del negocio. Si un snapshot es grande, podría ser grande porque capturó un despliegue mayor.

Task 15: Encuentra datasets “ocultos” vía montajes legacy

cr0x@server:~$ zfs get -o name,property,value -H mountpoint tank | grep legacy
tank/oldapps	mountpoint	legacy

Qué significa: El mountpoint de este dataset no lo gestiona ZFS; probablemente se monta via /etc/fstab o una unidad de servicio.

Decisión: No asumas que está sin uso porque no está montado donde esperas. Revisa montajes del sistema y consumidores antes de cualquier eliminación.

Task 16: Confirma la cadena exacta del dataset que vas a destruir (prueba humana)

cr0x@server:~$ zfs list -H -o name tank/app
tank/app

Qué significa: El dataset existe y tienes el nombre canónico sin ruido de formato.

Decisión: Copia y pega esta salida exacta en tu registro de cambio y en la línea de comandos. Escribir a mano es cómo se crean realidades alternativas.

Segundo chiste corto (y luego volvemos al trabajo): Lo único más rápido que zfs destroy es la reunión que te programan después.

Tres microhistorias corporativas desde las trincheras de eliminación

Microhistoria 1: El incidente causado por una suposición errónea

El equipo tenía un patrón estándar: los archivos de la aplicación vivían bajo /srv/app, logs bajo /srv/app/log, cache bajo /srv/app/cache. Alguien tomó la guardia y heredó el modelo mental: “Si la ruta existe, debe ser el dataset.” Esa suposición funcionó durante años—hasta una migración.

Durante la migración, un ingeniero de almacenamiento movió tank/app/log a un pool separado para aislamiento de I/O y usó un bind mount para preservar rutas. El nombre del dataset cambió; el mountpoint no. Al ingeniero de guardia le llegó una página por poco espacio y siguió un runbook antiguo: “destroy old logs dataset.” Ejecutó zfs destroy -r tank/app, creyendo que solo afectaría el subtree de logs que le importaba.

Hizo exactamente lo que decía el comando. tank/app era el dataset de la aplicación en vivo, y -r barrió los hijos. El dataset de logs con bind mount sobrevivió (pool distinto), pero los binarios de la aplicación, configuraciones y activos subidos por clientes no. El monitoreo mostró la app cayendo; el load balancer la sacó; incidente declarado.

El postmortem no fue sobre “error del operador”. Fue sobre una organización que dejó que un runbook basado en rutas se desviara de la realidad de los datasets. La solución fue aburrida: cada paso destructivo del runbook que mencionaba una ruta debía incluir la verificación del nombre del dataset vía findmnt y zfs list. También impusieron “no -r sin listar hijos en el ticket.” Al siguiente de guardia no le gustó—hasta el siguiente casi accidente.

Microhistoria 2: La optimización que salió mal

Un equipo de plataforma quería entornos CI más rápidos. Los clones eran la jugada obvia: tomar un snapshot dorado de un dataset de cache de build, clonar por job, destruir al final. Era elegante. También fue una fuga lenta de almacenamiento disfrazada de éxito.

El diseño inicial clonaba desde tank/ci/cache@golden y fijaba un TTL corto. Pero el job de limpieza estaba atado al hook “job finished” del scheduler CI. Cuando el scheduler tuvo un mal día (partición de red, reinicio del plano de control), la limpieza no se ejecutó. Los clones se acumularon. Todos seguían teniendo CI rápido, lo que hizo que nadie mirara de cerca el crecimiento del almacenamiento.

Entonces el pool alcanzó alta utilización y el rendimiento se volvió extraño. La asignación empezó a fragmentarse; las escrituras síncronas se ralentizaron; picos de latencia aparecieron en cargas no relacionadas. El de guardia vio el mayor consumidor: tank/ci/cache. Intentaron borrar snapshots antiguos para liberar espacio. ZFS se negó. El snapshot tenía clones. “Arreglarlo” se volvió “usar -R”.

-R sí liberó espacio. También eliminó clones activos pertenecientes a jobs en vuelo, provocando fallos de build y una reacción en cadena de reintentos. El pool se recuperó; CI se convirtió en tormenta; el plano de control se cayó otra vez. La optimización se transformó en un bucle de retroalimentación.

La solución a largo plazo no fue “no usar clones”. Fue “tratar el ciclo de vida de clones como datos de producción.” Añadieron holds para el snapshot golden, un reconciliador periódico que destruye clones expirados basándose en una propiedad ZFS, y una barrera: nadie podía ejecutar zfs destroy -R en namespaces de CI sin una segunda aprobación.

Microhistoria 3: La práctica aburrida pero correcta que salvó el día

Un servicio financiero almacenaba facturas en ZFS. El equipo hizo una cosa poco sexy bien: usaron holds en snapshots de cumplimiento y los replicaron fuera del host. Cada snapshot de fin de mes recibía un hold y una referencia de ticket. Era burocrático. También funcionó.

Un trimestre, un despliegue escribió accidentalmente una avalancha de datos malos—duplicados, PDFs corruptos, de todo. La respuesta inicial se centró en rollback de código, pero los datos ya estaban contaminados. Alguien sugirió: “Destruyamos el dataset y restauremos desde backup.” Esa idea suena decisiva hasta que notas que “backup” es un proceso, no un sustantivo.

En cambio, usaron el snapshot mensual con hold como ancla de confianza. Crearon un clone del snapshot para análisis forense manteniendo el original inmutable, y luego usaron replicación para restaurar un dataset limpio en un nuevo namespace. El dataset viejo y contaminado se mantuvo el tiempo suficiente para extraer lo que aún servía, y luego se destruyó deliberadamente con firmas.

Sin heroísmos. Sin pánico con -R. La clave fue que el mecanismo “aburrido” de seguridad—holds + replicación off-host—permitió al equipo moverse despacio incluso cuando el incidente fue rápido.

Errores comunes: síntoma → causa raíz → solución

1) “Eliminé snapshots pero el pool no liberó espacio”

Síntoma: Eliminas varios snapshots; zpool list apenas cambia.

Causa raíz: Los bloques siguen referenciados por otros snapshots, clones o el head del dataset; los snapshots eliminados no tenían datos únicos.

Solución: Inspecciona el USED de snapshots y dependencias de clones. Usa zfs list -t snapshot -o name,used -S used y revisa zfs get clones en snapshots grandes/antiguos. Elimina snapshots con alta unicidad que coincidan con la política de retención, no snapshots aleatorios.

2) “Destroy falló: el dataset está ocupado”

Síntoma: Errores cannot unmount / dataset is busy.

Causa raíz: Procesos mantienen archivos abiertos; exportaciones NFS; runtimes de contenedores; dependencias de systemd mount.

Solución: Identifica consumidores con lsof/fuser, detén servicios limpiamente y luego destruye. Si te tienta -f, trátalo como una solicitud de cambio, no como un impulso de shell.

3) “Destruí un dataset pero el directorio sigue existiendo”

Síntoma: La ruta sigue ahí; asumes que la destrucción no funcionó.

Causa raíz: El directorio del mountpoint es solo un directorio; el dataset montado estaba encima. Después del unmount/destruir, el directorio subyacente permanece.

Solución: Verifica con findmnt -T y zfs list. Elimina o reutiliza el directorio intencionalmente; no lo tomes como evidencia de fallo.

4) “Destruí un snapshot y ahora los incrementos de replicación fallan”

Síntoma: El siguiente send incremental falla porque falta el snapshot base esperado.

Causa raíz: Las herramientas de replicación dependían de una cadena de nombres de snapshot específica; la eliminación rompió la cadena.

Solución: Alinea la retención de snapshots con los requisitos de replicación. Conserva snapshots ancla de replicación mediante holds o clases de retención distintas. No borres snapshots antiguos al azar en datasets replicados.

5) “Ejecuté destroy -r y borré más de lo que quería”

Síntoma: Datasets inesperados desaparecieron; mountpoints se esfumaron; servicios fallaron.

Causa raíz: No inventariaste hijos, o asumiste que eran “solo directorios”. Eran datasets.

Solución: Siempre ejecuta zfs list -r y pega la salida en el registro de cambio antes de usar -r. Si el subtree te sorprende, detente y reencuadra.

6) “Destroy tuvo éxito, pero los usuarios todavía ven datos”

Síntoma: Tras destruir un dataset, la ruta sigue sirviendo contenido.

Causa raíz: Destruiste un dataset con el mismo mountpoint que otro dataset, o tienes un automounter/remontaje, o otro host lo está sirviendo (NFS/SMB/cluster).

Solución: Confirma las fuentes reales de montaje (findmnt), confirma exportaciones y verifica que estés en el host que sirve la ruta. El nombrado ZFS es por host; la experiencia del usuario puede provenir de otro lado.

Listas de verificación / plan paso a paso para destrucción segura

Checklist A: Antes de destruir cualquier cosa (comprobaciones de identidad)

  1. Obtén el nombre del dataset desde el sistema, no de tu memoria. Usa zfs list -H -o name o el mapeo de findmnt.
  2. Confirma el subtree. Ejecuta zfs list -r y revisa hijos.
  3. Confirma el comportamiento de montaje. Revisa mountpoint, canmount, readonly y status legacy.
  4. Confirma consumidores. Usa lsof en mountpoints; para zvols verifica propiedad VM/hipervisor.
  5. Revisa holds y clones de snapshots. Holds significan “alto”. Clones significan “entiende los dependientes”.
  6. Confirma estado de replicación/receive. Busca receive resume tokens o cronogramas de replicación conocidos; no rompas cadenas incrementales a la ligera.

Checklist B: Si el objetivo es “liberar espacio”, prefiere estas acciones en orden

  1. Elimina basura obvia dentro del filesystem (limpieza a nivel de aplicación) si es seguro.
  2. Elimina snapshots que coincidan con una política documentada de retención, comenzando por los que tienen mayor USED único.
  3. Mueve datos (replica, archiva) y luego destruye un dataset ahora vacío.
  4. Destruye un dataset completo solo cuando realmente esté fuera de servicio, no solo “grande”.

Checklist C: Si debes destruir un dataset en producción

  1. Toma un snapshot final (y replícalo fuera del host si puedes). Si el dataset ya es conocido como malo (corrupción a nivel de app), haz snapshot de todos modos para análisis forense.
  2. Pon un hold de corta duración en ese snapshot final para que nadie lo borre “útilmente” durante la ventana de cambio.
  3. Deshabilita automounters/servicios que puedan remontar o recrear la ruta del dataset.
  4. Destruye el objetivo más estrecho. No uses -r a menos que pretendas todo el subtree. No uses -R a menos que pretendas matar clones.
  5. Verifica la eliminación con zfs list, verifica montajes y el comportamiento de la aplicación.
  6. Vigila espacio del pool y errores al menos durante un intervalo de monitoreo después del cambio.

Salvaguardas de automatización que no te hacen más lento

Las prácticas humanas de seguridad son necesarias. No son suficientes. Los borrados en producción eventualmente ocurren vía automatización: pipelines de desprovisionamiento, limpieza de tenants, tear-down de CI, reinicios de entornos. Necesitas rieles que asuman que el operador está cansado y el script es literal.

1) Requiere un patrón explícito de allowlist de datasets

La automatización debería destruir solo datasets en un namespace conocido, como tank/ci/* o tank/ephemeral/*. Si un nombre de dataset cae fuera de ese prefijo, el script debe negarse.

2) Aplicar “listar subtree antes de destroy recursivo”

Antes de cualquier -r, los scripts deberían imprimir la salida de zfs list -r y requerir confirmación humana en modo interactivo, o almacenarla como artefacto en modo no interactivo. Esto te da evidencia forense después y evita borrados silenciosos.

3) Usa holds como límite de política

La automatización debe negarse a destruir cualquier snapshot con holds, y ser cautelosa con datasets que contengan snapshots retenidos. Los holds son un “alto” barato y nativo. Trátalos como tal.

4) Etiqueta datasets con propiedades y toma decisiones desde propiedades

Configura una propiedad personalizada como com.example:purpose=ci o com.example:ttl=2025-12-27T00:00Z (la convención varía por organización). Entonces tu job de limpieza solo destruye datasets cuyas propiedades indiquen que son desechables. Los nombres ayudan. Las propiedades se pueden aplicar.

5) Limita quién puede destruir en primer lugar

Usa delegación para que la mayoría de roles puedan snapshotear/clonar pero no destruir fuera de su subárbol. No se trata de confianza; se trata de reducir el radio de impacto. En práctica, esto también fuerza que las operaciones de destroy entren en flujos revisables.

Preguntas frecuentes

1) ¿ZFS tiene un “undelete” integrado para datasets destruidos?

No. Si destruiste un dataset y no tienes snapshots ni copia replicada, la recuperación no es algo que debas planear. Trata la destrucción como permanente.

2) ¿Es “más seguro” borrar un snapshot que un dataset?

Más seguro, sí, porque es más estrecho. Aún así no es automáticamente seguro: los snapshots pueden ser anclas de replicación, artefactos de cumplimiento o requeridos para rollback. Revisa holds, dependencias de clones y tu flujo de backups/replicación.

3) ¿Cuál es la diferencia entre -r y -R en zfs destroy?

-r destruye datasets hijos y snapshots bajo el dataset objetivo. -R destruye dependientes, incluidos clones, e implica recursión de forma mucho más destructiva. Usa -R solo cuando hayas enumerado explícitamente lo que llevará consigo.

4) ¿Por qué ZFS a veces se niega a destruir un snapshot?

Las razones comunes son: el snapshot tiene un hold, el snapshot tiene clones dependientes o no tienes permiso. ZFS se niega porque la eliminación violaría reglas de dependencia o política.

5) ¿Puedo confiar en los mountpoints para identificar datasets?

No de forma fiable. Los mountpoints pueden heredarse, cambiarse, ponerse en legacy o quedar enmascarados por otros montajes. Siempre mapea rutas a datasets con herramientas de inspección de montajes y luego opera sobre nombres de dataset.

6) Si destruyo un dataset, ¿los snapshots de ese dataset también desaparecen?

Sí, si destruyes el dataset (y especialmente con recursión), normalmente estás eliminando el dataset y sus snapshots en ese ámbito. Si quieres conservar un punto de recuperación, haz snapshot y replica en otro sitio primero —luego destruye.

7) ¿Por qué al borrar un snapshot grande no se liberó tanto espacio como su tamaño?

El “tamaño” de un snapshot no es un número simple. El valor USED del snapshot es el espacio único retenido por ese snapshot, no la vista total de datos. Si otros snapshots o clones referencian los mismos bloques, el espacio no se liberará hasta que todas las referencias desaparezcan.

8) ¿Está bien usar zfs destroy -f para forzarlo?

Sólo cuando hayas confirmado que el dataset debe morir y hayas identificado por qué está ocupado. Forzar unmount puede romper aplicaciones en ejecución y ocultar un error de objetivo equivocado. Trata -f como un paso de escalado con verificación explícita.

9) ¿Cuál es el patrón operativo más seguro para la descomisión de datasets?

Snapshot → replica (o asegura copia off-host) → pon hold temporal en el snapshot final → deshabilita servicios/montajes → destruye de forma estrecha → verifica. Si no puedes replicar, al menos haz snapshot y pon hold por una ventana definida.

10) ¿Cómo evito que un script destruya el dataset equivocado?

Usa allowlist estricta de namespace, valida existencia y subtree del dataset, rechaza objetivos con holds y requiere propiedades que marquen el dataset como desechable. Haz que el script sea molesto en las formas correctas.

Siguientes pasos que puedes implementar esta semana

Si ejecutas ZFS en producción, puedes hacer que los borrados por dataset equivocado sean menos frecuentes sin frenar al equipo.

  1. Actualiza tus runbooks: cada paso destructivo debe incluir “mapear ruta a dataset” y comprobaciones de “listar subtree”.
  2. Adopta holds de snapshot para snapshots “debe conservarse”: cumplimiento, fin de mes, pre-migración, pre-upgrade. Los holds son un seguro barato.
  3. Establece una convención de nombres + namespaces: los datasets desechables viven bajo un prefijo que la automatización puede forzar.
  4. Restringe privilegios de destroy: la mayoría de las personas no los necesita. Delega snapshot/clone ampliamente; restringe destroy estrechamente.
  5. Practica el playbook de espacio: enseña a la guardia a distinguir refer vs usedbysnapshots para que dejen de borrar “datasets grandes” para resolver “snapshots grandes”.
  6. Haz que -R sea socialmente costoso: exige listar dependencias de clones y una segunda revisión. Esto por sí solo previene una cantidad deprimente de outages evitables.

ZFS es un bisturí. También puede ser una motosierra, pero solo si insistes en sostenerla por el extremo equivocado. Las salvaguardas existen; tu trabajo es usarlas a propósito.

← Anterior
MariaDB vs PostgreSQL: limitación de tasas en la base de datos — por qué es una trampa y qué hacer en su lugar
Siguiente →
Evolución de Zen: cambios entre generaciones que realmente notas

Deja un comentario