ZFS dnodes y metadatos: por qué los metadatos pueden ser tu verdadero cuello de botella

¿Te fue útil?

Cuando ZFS se siente “lento”, la gente culpa primero a los discos, luego a la red y luego a “ese dataset vecino ruidoso”. Mientras tanto, el verdadero culpable suele ser los metadatos: IOs diminutas, seguimiento de punteros y dnodes que ya no caben bien en la caché. Tu pool puede mover gigabytes por segundo en lecturas secuenciales grandes y aun así colapsar cuando se le pide hacer git status en un repositorio grande, descomprimir un árbol del kernel o escanear millones de objetos de 4–16 KB.

Este es el tipo de problema de rendimiento que arruina fines de semana de guardia porque parece todo y nada a la vez. La CPU parece estar bien. El ancho de banda parece correcto. Y, sin embargo, la latencia se dispara, las operaciones de directorio se arrastran y la aplicación jura que “la base de datos está caída” mientras tus paneles muestran que los discos están sólo a la mitad de uso.

Qué significa realmente “cuello de botella de metadatos” en ZFS

En ZFS, “metadatos” no es un concepto vago. Son bloques concretos y estructuras en disco que describen todo lo demás: conjuntos de objetos, punteros de bloque, bloques indirectos, dnodes, entradas de directorio (ZAP), spacemaps, clases de asignación, intent logs y más. Metadatos también es el patrón de carga: muchas lecturas/escrituras pequeñas, muchas IO aleatorias, muchas dependencias y semánticas síncronas frecuentes que fuerzan orden.

Los cuellos de botella de metadatos ocurren cuando tu sistema pasa más tiempo averiguando dónde está y qué es el dato que en mover el dato en sí. Por eso un pool que transmite backups a velocidad de línea puede colapsar ante:

  • Jobs de CI que crean/eliminan millones de archivos por día
  • capas de contenedores y extracción de imágenes
  • patrones tipo maildir (muchos archivos pequeños, muchas renombradas)
  • flotas de VM con snapshots, clones y alta rotación
  • cualquier cosa que recorra directorios y haga stats a gran escala

La carga de metadatos es primero un problema de latencia. El enemigo no es el “bajo throughput”, es la espera y el tiempo de bloqueo: una cadena de lecturas dependientes donde cada fallo en ARC se convierte en una IO a disco, demasiado pequeña para amortizar la búsqueda/traducción, que luego bloquea la siguiente búsqueda.

Este es el cambio de mentalidad: en cargas pesadas en metadatos, tu “presupuesto de IOPS” y “presupuesto de aciertos en caché” importan más que tu “presupuesto de MB/s”. Puedes tener mucho ancho de banda y aun así estar completamente inutilizado.

Dnodes 101: la pequeña estructura que decide tu destino

¿Qué es un dnode?

Un dnode es la estructura de metadatos por objeto en ZFS. Para un archivo, el dnode describe su tipo, metadatos de tamaño de bloque, bonus data y cómo encontrar los bloques de datos del archivo (a través de punteros de bloque). Para directorios y otros objetos ZFS, cumple el mismo rol de “este objeto existe y aquí está cómo acceder a él”.

Eso suena inocente. En la práctica, los dnodes están implicados en casi cada operación de metadatos que te importa: búsquedas, stat, permisos, atributos extendidos y recorrido de bloques indirectos.

Por qué importa dnodesize

dnodesize controla cuán grande puede ser un dnode. Los dnodes más grandes pueden almacenar más información “bonus” en línea (especialmente útil para atributos extendidos). Pero los dnodes mayores también aumentan la huella de metadatos. Más huella significa menos dnodes en ARC por la misma RAM, más fallos, más IO a disco y operaciones de metadatos más lentas.

Esto no es teórico. Es una de las razones más comunes por las que un sistema ZFS “va bien con archivos grandes” y “va fatal con archivos pequeños”. El equipo de la app lo llama regresión de almacenamiento. Tú lo llamas matemáticas.

Datos bonus y xattrs: el multiplicador oculto

ZFS puede almacenar atributos extendidos (xattrs) de distintas maneras. Cuando los xattrs se almacenan como sa (system attributes), viven en el área bonus del dnode cuando es posible. Eso es excelente para evitar IO adicional por atributo, hasta que subes dnodesize en todas partes y descubres que acabas de hacer más pesado el metadato de cada objeto, aunque no lo necesitara.

Broma #1: Los metadatos son como el papeleo de la oficina: a nadie le gustan más, pero de alguna forma se siguen generando mientras duermes.

“Pero puse dnodesize=auto, así que estoy seguro.” No necesariamente.

dnodesize=auto permite que ZFS aumente el tamaño del dnode para acomodar bonus data. Eso a menudo ayuda en cargas con muchos xattrs. Pero también significa que tu dataset puede acumular dnodes más grandes a medida que se crean archivos. Si luego cambias a “millones de archivos pequeños sin xattrs”, podrías seguir pagando la tasa de huella de caché por decisiones anteriores, snapshots y clones. ZFS es honesto; recuerda.

Dónde viven los metadatos y por qué son caros

ARC: tu amortiguador de metadatos

El ARC (Adaptive Replacement Cache) no es solo una caché de lectura; es tu acelerador de metadatos. En cargas con muchos metadatos, la tasa de aciertos en ARC es la diferencia entre “suficientemente rápido” y “paginando por el pool una lectura de 4K a la vez”. El acceso a metadatos tiende a ser aleatorio y difícil de predecir. Así que la caché es reina.

Pero ARC es un recurso compartido. Los bloques de datos, bloques de metadatos, streams de prefetch y las listas MFU/MRU compiten. Si tu ARC está lleno de lecturas en streaming o bloques grandes, los metadatos pueden quedar comprimidos. Entonces el sistema “trabaja más” emitiendo más lecturas pequeñas, lo que aumenta la latencia, hace que el usuario se queje, provoca más diagnósticos y añade más carga. Felicidades: construiste un bucle de retroalimentación.

Bloques indirectos, punteros de bloque y persecución de punteros

ZFS es copy-on-write y usa un árbol de punteros de bloque. Una operación de metadatos suele convertirse en: leer dnode → leer bloque(s) indirectos → leer bloques ZAP → quizá leer bloques SA spill → repetir. Cada paso depende del anterior. Si estos bloques no están en caché, la operación se convierte en una serie de lecturas aleatorias y síncronas.

Por eso NVMe “arregla” a menudo el dolor de metadatos: no porque la carga se vuelva secuencial, sino porque la latencia de lectura aleatoria cayó un orden de magnitud. Con discos rotativos, puedes tener muchos platos y aun así perder porque la cadena de dependencias serializa el throughput efectivo.

Special vdev: separar metadatos (y bloques pequeños) a propósito

OpenZFS soporta una clase de asignación “special” vdev (a menudo llamada special vdev) que puede almacenar metadatos y, opcionalmente, bloques pequeños. Hecho bien, es una de las herramientas más potentes para convertir un sistema limitado por metadatos en uno tolerable.

Hecho mal, se convierte en un punto único de drama de “por qué el pool está suspendido”, porque perder un special vdev puede hacer que el pool no sea importable si ahí está el metadato. Trátalo como almacenamiento de primer nivel: en espejo, monitorizado y correctamente dimensionado.

Escrituras síncronas y el ZIL

Las operaciones de metadatos suelen incluir semánticas síncronas: creaciones, renombrados, patrones con fsync intensivo, bases de datos que insisten en orden. ZFS usa el ZIL (ZFS Intent Log) para satisfacer escrituras síncronas de forma segura. Un dispositivo SLOG separado puede reducir la latencia de escrituras síncronas, pero no hace que las lecturas de metadatos sean rápidas. Ayuda cuando tu cuello de botella es “latencia de escritura síncrona”, no “latencia de lectura aleatoria de metadatos”.

En otras palabras: SLOG no es una curita para un recorrido lento de directorios. Es una curita para aplicaciones que exigen escrituras síncronas y son sensibles a la latencia.

Snapshots: el viaje en el tiempo de metadatos no es gratis

Los snapshots son baratos hasta que los usas como un sistema de control de versiones para sistemas de archivos enteros. Cada snapshot preserva viejos punteros de bloque. Eso incrementa los metadatos que deben mantenerse accesibles, aumenta la fragmentación y puede elevar el costo de las eliminaciones (porque borrar no es borrar; es “liberar cuando ningún snapshot lo referencia más”). Cuantos más snapshots y más churn, más metadatos participan en el mantenimiento diario.

Modos de fallo: cómo aparecen los cuellos de botella de metadatos en producción

Síntoma A: “El almacenamiento está lento, pero iostat parece bien.”

Clásico. Verás ancho de banda moderado, IOPS moderadas y aun así latencia para el usuario. ¿Por qué? Porque el cuello de botella es la latencia por operación, no el throughput agregado. Un escaneo de directorio puede estar haciendo una o dos lecturas dependientes a la vez, sin construir suficiente profundidad de cola para “parecer ocupado”, pero cada lectura tarda lo suficiente como para arruinar la experiencia.

Síntoma B: “La CPU está alta en tiempo de sistema; la app está bloqueada.”

Las cargas con muchos metadatos estresan el DMU, la capa vnode y los caminos de checksum/compresión. Alta CPU en sistema más muchos cambios de contexto pueden ocurrir incluso cuando los discos no están saturados—especialmente si el sistema está thrashing por fallos de caché y haciendo muchas finalizaciones de IO pequeñas.

Síntoma C: “Todo iba bien hasta que añadimos snapshots / activamos xattrs / cambiamos dnodesize.”

Estos son multiplicadores. Los snapshots preservan historia, que preserva la alcanzabilidad de metadatos. Los xattrs almacenados como SA pueden aumentar el uso de bonus data. Dnodes más grandes incrementan la huella en caché. Cada uno es racional por sí solo. Juntos, pueden convertir “suficientemente rápido” en “¿por qué ls tarda 10 segundos?”.

Síntoma D: “Los scrubs/resilvers vuelven inutilizable el cluster.”

Scrub y resilver tocan metadatos y datos ampliamente. Si ya estás cerca del límite, la IO en background te empuja a latencia visible al usuario. Los fallos de metadatos aumentan porque ARC es desplazado por lecturas de scrub. Si tu special vdev está sobrecargado o tus metadatos están en discos lentos, el efecto es peor.

Síntoma E: “Borrar es lento; liberar espacio tarda una eternidad.”

Borrados bajo carga de snapshots significan recorrer metadatos, actualizar spacemaps y diferir liberaciones reales. Si estás liberando millones de archivos pequeños, el sistema realiza una enorme cantidad de actualizaciones de metadatos. Si tu metaslab está muy fragmentado, las asignaciones y liberaciones se vuelven más caras.

Datos interesantes y contexto histórico (lo que explica las rarezas de hoy)

  • ZFS nació a mediados de los 2000 en Sun Microsystems con checksums end-to-end y copy-on-write como puntos de diseño centrales; la seguridad de metadatos fue un objetivo de primera clase, no un añadido.
  • Copy-on-write significa que las actualizaciones de metadatos son escrituras: actualizar un puntero de bloque o una entrada de directorio desencadena nuevos bloques, no ediciones in-place, lo que es más seguro pero puede amplificar IO en cargas con churn de metadatos.
  • El ARC fue diseñado para cachear tanto datos como metadatos, y en la práctica el cacheo de metadatos suele ser el uso de RAM con mayor retorno de inversión en cajas ZFS.
  • System attributes (“SA”) se introdujeron para reducir IO de xattr empaquetando atributos en bonus buffers, acelerando las operaciones para cargas con muchos atributos.
  • Los feature flags permitieron que ZFS evolucionara sin bifurcar el formato en disco: los sistemas modernos de OpenZFS negocian características como extensible_dataset y otras, cambiando las capacidades de metadatos con el tiempo.
  • Los vdevs de clase special llegaron para resolver un dolor real: los metadatos en HDD eran un limitador de rendimiento incluso cuando el throughput de datos estaba bien.
  • ZAP (ZFS Attribute Processor) es el formato “diccionario” de ZFS usado para directorios y propiedades; puede ser micro-optimizable en caché y se vuelve un hotspot en cargas con muchos directorios.
  • El ajuste de recordsize suele malinterpretarse: afecta bloques de datos de archivos, no a los dnodes, y por sí solo no arreglará la lentitud en el recorrido de directorios.
  • NVMe no solo hizo ZFS más rápido; cambió los modos de fallo: la latencia de lectura aleatoria mejoró tanto que la CPU y la contención de locks pueden volverse visibles donde antes los discos lo ocultaban.

Guion de diagnóstico rápido

Cuando un sistema ZFS “se siente lento”, debes decidir si está limitado por datos, por metadatos o por escrituras síncronas. No lo compliques. Ejecuta el triage en orden.

1) Primero: confirma que es latencia e identifica lectura vs escritura

  • Comprueba latencia del pool y colas con zpool iostat -v y, si está disponible, columnas de latencia por vdev.
  • Busca patrones de IO pequeños: muchas ops, bajo ancho de banda, alta espera.
  • Pregunta: ¿el problema aparece en ls -l, find, descomprimir, operaciones de git? Eso es metadatos hasta que se demuestre lo contrario.

2) Segundo: revisa la salud del ARC y la presión de metadatos

  • ¿El tamaño del ARC es estable y cercano al objetivo? ¿Está thrashing?
  • ¿Los fallos de metadatos son altos? ¿El prefetch está contaminando el ARC?
  • ¿La RAM es adecuada para el conteo de objetos y el working set?

3) Tercero: encuentra dónde aterrizan físicamente los metadatos

  • ¿Tienes special vdev? ¿Está en espejo? ¿Está saturado?
  • ¿Los metadatos y bloques pequeños están en HDD mientras los datos están en SSD (o viceversa)?
  • ¿El pool está fragmentado o con alta utilización, generando más sobrecarga de metadatos?

4) Cuarto: aísla un “microbenchmark de metadatos” a partir de los síntomas de producción

  • Mide el tiempo de un recorrido de directorio.
  • Mide un bucle de creación/eliminación intensivo en un dataset de prueba.
  • Correlaciónalo con zpool iostat y estadísticas ARC mientras corre.

5) Quinto: valida la ruta de escritura síncrona si la app es fsync-heavy

  • Comprueba la propiedad sync, la presencia de SLOG y la latencia de escritura.
  • Confirma que la carga realmente emite escrituras síncronas (no lo supongas).

Tareas prácticas: comandos, salidas, qué significa y la decisión que tomas

Estos son los comandos que ejecuto cuando alguien dice “ZFS está lento” y quiero dejar de adivinar. Las salidas son representativas; tus números serán distintos. Lo importante es lo que concluyes.

Tarea 1: Identificar disposición del pool y presencia de special vdev

cr0x@server:~$ zpool status -v tank
  pool: tank
 state: ONLINE
config:

        NAME                         STATE     READ WRITE CKSUM
        tank                         ONLINE       0     0     0
          raidz2-0                   ONLINE       0     0     0
            sda                      ONLINE       0     0     0
            sdb                      ONLINE       0     0     0
            sdc                      ONLINE       0     0     0
            sdd                      ONLINE       0     0     0
          special
            mirror-1                 ONLINE       0     0     0
              nvme0n1                ONLINE       0     0     0
              nvme1n1                ONLINE       0     0     0

errors: No known data errors

Qué significa: Hay un special vdev en espejo. Los metadatos (y posiblemente bloques pequeños) pueden estar yendo a NVMe, lo cual es bueno. Si no hay special vdev y tienes HDDs, la latencia de metadatos es un sospechoso principal.

Decisión: Si los metadatos están en rust lenta y la carga es metadatos-intensiva, planifica un special vdev en espejo o mueve la carga a flash.

Tarea 2: Medir latencia a nivel de pool y forma de IO

cr0x@server:~$ zpool iostat -v tank 1 5
              capacity     operations     bandwidth
pool        alloc   free   read  write   read  write
----------  -----  -----  -----  -----  -----  -----
tank        7.12T  3.88T    950    420  12.3M  8.1M
  raidz2-0  7.12T  3.88T    120     80  12.0M  8.0M
    sda         -      -     30     20  3.0M   2.0M
    sdb         -      -     30     20  3.0M   2.0M
    sdc         -      -     30     20  3.0M   2.0M
    sdd         -      -     30     20  3.0M   2.0M
  special      80G   720G    830    330   0.3M  0.1M
    mirror-1     -      -    415    165   0.3M  0.1M
      nvme0n1    -      -    210     85   0.1M  0.0M
      nvme1n1    -      -    205     80   0.1M  0.0M

Qué significa: Muchas operaciones y ancho de banda pequeño en el special vdev: eso grita metadatos. El vdev grande muestra ancho de banda moderado; tu dolor probablemente viene de latencia en IO pequeñas y cadenas de dependencia.

Decisión: Enfócate en la tasa de aciertos del ARC, la saturación del special vdev y los datasets intensivos en metadatos. No pierdas tiempo persiguiendo throughput secuencial.

Tarea 3: Revisar propiedades del dataset que influyen en metadatos

cr0x@server:~$ zfs get -o name,property,value -s local,received -r atime,recordsize,xattr,dnodesize,primarycache,secondarycache,sync tank/app
NAME      PROPERTY        VALUE
tank/app  atime           off
tank/app  recordsize      128K
tank/app  xattr           sa
tank/app  dnodesize       auto
tank/app  primarycache    all
tank/app  secondarycache  all
tank/app  sync            standard

Qué significa: xattr=sa y dnodesize=auto pueden ser excelentes para cargas con muchos xattrs, pero también pueden inflar la huella de metadatos. primarycache=all permite datos y metadatos en ARC; a veces quieres metadata para cargas de streaming que expulsan metadatos.

Decisión: Si este dataset se usa para operaciones con muchos archivos pequeños, mantén la caché activada y considera si primarycache=metadata es apropiado para cargas mixtas.

Tarea 4: Verificar tamaño del ARC y comportamiento básico

cr0x@server:~$ sudo arcstat 1 3
    time  read  miss  miss%  dmis  dm%  pmis  pm%  mmis  mm%  arcsz     c
21:10:01   480    22      4    10   45    12   55     6   27   62G   64G
21:10:02   510    28      5    14   50    14   50     8   29   62G   64G
21:10:03   495    30      6    18   60    12   40    10   33   62G   64G

Qué significa: ARC está cerca del objetivo (arcsz cerca de c). La tasa de fallos es moderada. Si ves que miss% sube y se mantiene alta durante operaciones de metadatos, irás al disco por metadatos.

Decisión: Si ARC es demasiado pequeño o está contaminado por datos, cambia la estrategia de caché o añade RAM. Sí, de verdad. Si los fallos son mayormente de metadatos (mm% alto), el tuning de special vdev y metadatos se vuelve urgente.

Tarea 5: Confirmar si las lecturas realmente alcanzan disco o caché

cr0x@server:~$ sudo zpool iostat -r tank 1 3
                 read
pool          ops/s   bandwidth
------------  -----  ---------
tank            980     12.6M
  raidz2-0      130     12.2M
  special       850      0.4M

Qué significa: Muchas ops de lectura hacia el special vdev sugieren que los metadatos no están suficientemente en ARC. Si esto fuera una carga con caché caliente, esperarías menos lecturas físicas durante recorridos repetidos de directorios.

Decisión: Investiga la expulsión del ARC, ajustes de caché y presión de memoria. Considera fijar el comportamiento de caché por dataset.

Tarea 6: Comprobar utilización del pool y riesgo de fragmentación

cr0x@server:~$ zpool list -o name,size,alloc,free,cap,frag,health tank
NAME  SIZE  ALLOC  FREE  CAP  FRAG  HEALTH
tank  11T   7.12T  3.88T 64%  38%   ONLINE

Qué significa: 64% lleno no es alarmante, pero 38% de fragmentación sugiere que las asignaciones están menos contiguas. El churn intensivo en metadatos puede aumentar esto, encareciendo futuras operaciones.

Decisión: Si la ocupación es >80% y la frag es alta, planifica remediación de espacio, reescritura o expansión del pool. El rendimiento de metadatos suele desplomarse a altas utilizaciones.

Tarea 7: Identificar datasets de “archivos pequeños” y contar objetos

cr0x@server:~$ zfs list -o name,used,refer,logicalused,compressratio,usedsnap -r tank
NAME            USED   REFER  LUSED  RATIO  USEDSNAP
tank            7.12T  256K   7.10T  1.18x  1.4T
tank/app        2.20T  2.00T  2.30T  1.12x  600G
tank/ci-cache   1.10T  920G   1.30T  1.05x  480G
tank/backups    3.60T  3.50T  3.40T  1.24x  320G

Qué significa: Datasets con gran USEDSNAP y alto churn (CI caches) a menudo disparan dolor de metadatos: las eliminaciones se vuelven caras, el espacio no vuelve inmediatamente y las operaciones de directorio se ralentizan.

Decisión: Dirige la revisión de políticas de snapshot y caché a tank/ci-cache, y considera moverlo a su propio pool o asignación special vdev.

Tarea 8: Observar latencia en vivo y profundidad de cola por vdev

cr0x@server:~$ zpool iostat -v tank 1 3
              capacity     operations     bandwidth
pool        alloc   free   read  write   read  write
----------  -----  -----  -----  -----  -----  -----
tank        7.12T  3.88T   1100    600  14.0M  10.2M
  raidz2-0  7.12T  3.88T    150    120  13.7M  10.1M
  special      80G   720G    950    480   0.3M   0.1M

Qué significa: Si las ops del special vdev saltan durante recorridos de directorio o tormentas de creación de archivos, está haciendo su trabajo—pero puede convertirse en cuello de botella si está subdimensionado o si los dispositivos son de gama doméstica y se ahogan con IO aleatoria sostenida.

Decisión: Si el special vdev está saturado, mejóralo (NVMe más rápido, más espejos) o reduce IO de metadatos (política de snapshots, aislar la carga).

Tarea 9: Comprobar si la carga es intensiva en escrituras síncronas

cr0x@server:~$ zpool status tank
  pool: tank
 state: ONLINE
  scan: scrub repaired 0B in 06:12:44 with 0 errors on Sun Dec 22 03:10:01 2025
config:

        NAME        STATE     READ WRITE CKSUM
        tank        ONLINE       0     0     0
          raidz2-0  ONLINE       0     0     0
        logs
          nvme2n1   ONLINE       0     0     0
        special
          mirror-1  ONLINE       0     0     0

errors: No known data errors

Qué significa: Hay un dispositivo SLOG (logs). Eso ayuda la latencia de escrituras síncronas. No hace nada por las lecturas de metadatos. La gente confunde esos casos a diario.

Decisión: Si los usuarios se quejan de latencia por fsync, valida la salud y latencia del SLOG. Si se quejan de find y ls, deja de mirar el SLOG.

Tarea 10: Buscar recuentos patológicos de snapshots y retención

cr0x@server:~$ zfs list -t snapshot -o name,used,refer,creation -s creation | tail -n 5
tank/ci-cache@auto-2025-12-26-0900    0B   920G  Fri Dec 26 09:00 2025
tank/ci-cache@auto-2025-12-26-1000    0B   920G  Fri Dec 26 10:00 2025
tank/ci-cache@auto-2025-12-26-1100    0B   920G  Fri Dec 26 11:00 2025
tank/ci-cache@auto-2025-12-26-1200    0B   920G  Fri Dec 26 12:00 2025
tank/ci-cache@auto-2025-12-26-1300    0B   920G  Fri Dec 26 13:00 2025

Qué significa: Snapshots frecuentes pueden estar bien, pero en datasets con churn preservan bloques muertos e incrementan metadatos a recorrer. “0B usados” por snapshot puede ocultar complejidad de metadatos según el churn.

Decisión: Para caches de CI y datasets temporales: acorta la retención, reduce la frecuencia o no los snapshots en absoluto.

Tarea 11: Identificar si el ARC está siendo contaminado por lecturas en streaming

cr0x@server:~$ zfs get -o name,property,value primarycache -r tank/backups
NAME         PROPERTY      VALUE
tank/backups primarycache  all

Qué significa: Los datasets de backups suelen ser lecturas en streaming. Si se les permite cachear datos en ARC, pueden expulsar metadatos necesarios para cargas sensibles a latencia.

Decisión: Considera poner primarycache=metadata en datasets de streaming para que ARC mantenga metadatos calientes sin desperdiciar RAM en datos masivos que no vas a releer pronto.

Tarea 12: Aplicar un cambio de caché dirigido (con cuidado)

cr0x@server:~$ sudo zfs set primarycache=metadata tank/backups

Qué significa: ZFS dejará de cachear bloques de datos de archivos para este dataset en ARC, pero seguirá cacheando metadatos. Esto suele mejorar la latencia de metadatos en otras áreas del pool.

Decisión: Si datasets sensibles comparten el pool con cargas en streaming, este es uno de los mandos más limpios y de menor riesgo.

Tarea 13: Inspeccionar xattr y dnodesize en el dataset más caliente

cr0x@server:~$ zfs get -o name,property,value xattr,dnodesize tank/ci-cache
NAME          PROPERTY  VALUE
tank/ci-cache xattr     sa
tank/ci-cache dnodesize auto

Qué significa: Esta combinación es comúnmente correcta para cargas con muchos xattrs (los contenedores pueden serlo). Pero puede inflar la huella de metadatos. Si tu carga no usa muchos xattrs, puede ser innecesario.

Decisión: No cambies esto a la ligera en un dataset existente esperando resultados instantáneos; afecta a objetos nuevos. Considera hacer pruebas A/B en un dataset nuevo.

Tarea 14: Prueba básica “¿es metadatos?” con tiempo de recorrido de directorio

cr0x@server:~$ time find /tank/ci-cache/workdir -type f -maxdepth 3 -printf '.' >/dev/null

real    0m28.412s
user    0m0.812s
sys     0m6.441s

Qué significa: Alto tiempo en sistema y largo tiempo de pared para un simple recorrido indican fuertemente metadatos y espera de IO. Si lo repites y es mucho más rápido, ARC ayudó; si no, ARC no es lo bastante grande o está siendo expulsado.

Decisión: Repite inmediatamente y compara. Si la segunda ejecución no es mucho más rápida, estás fallando en ARC y golpeando discos/special vdev con fuerza.

Tres mini-historias corporativas (anonimizadas, plausibles, dolorosamente familiares)

1) El incidente causado por una suposición errónea: “Tenemos NVMe, así que los metadatos no pueden ser el problema”

Una compañía SaaS de tamaño medio operaba una granja de builds en ZFS. Tenían NVMe moderno para “el pool”, así que el equipo de infra asumió que la latencia de almacenamiento estaba básicamente resuelta. El sistema de builds seguía sufriendo paradas esporádicas de 30–90 segundos en horas pico. Los ingenieros culparon al orquestador de CI y lo descartaron como “Kubernetes siendo Kubernetes”.

En guardia finalmente correlacionaron las paradas con jobs que desempaquetaban grandes árboles de dependencias: decenas de miles de archivos pequeños. Durante esas ventanas, el bandwidth promedio del pool era bajo y los dispositivos NVMe estaban lejos de saturarse. Todos miraban los dashboards y concluían: no es almacenamiento.

El error: sólo miraron throughput y utilización agregada. No vieron la distribución del tamaño de IO, la tasa de aciertos en caché ni el hecho de que las operaciones de metadatos eran seriales y dominadas por latencia. El ARC era pequeño respecto al working set de objetos, y un job de backup concurrente estaba haciendo streaming a través del ARC, expulsando metadatos.

La solución fue aburrida: poner primarycache=metadata en datasets de backups, añadir RAM y mover los metadatos del dataset de CI a un special vdev en espejo correctamente dimensionado. Las “paradas misteriosas” desaparecieron. El orquestador no cambió. Las suposiciones sí.

2) La optimización que salió mal: “Aumentemos dnodesize para acelerar xattrs”

Una gran empresa ejecutaba un servicio de archivos sensible a cumplimiento. Guardaban muchas etiquetas y atributos de auditoría como xattrs. Alguien leyó que dnodes más grandes ayudan a mantener atributos inline y reducir IO. Verdadera afirmación. Media verdad peligrosa.

Aplicaron una plantilla de dataset que ponía dnodesize=auto ampliamente, incluyendo datasets que no lo necesitaban: staging de logs, artefactos de build, scratch temporal. Con el tiempo, esos datasets acumularon objetos con dnodes mayores. La eficiencia del ARC empeoró. Los fallos de caché aumentaron. Las operaciones de directorio se ralentizaron y luego la latencia cliente subió, y los tickets comenzaron a llegar.

El retroceso no fue un simple toggle porque los objetos existentes mantienen su estructura de dnode. Tuvieron que crear datasets nuevos con valores por defecto sensatos, migrar datos y tratar los viejos como residuos hasta retirarlos. La lección no fue “nunca uses dnodes grandes”. Fue: aplícalo donde lo midas, no donde esté de moda.

3) La práctica aburrida pero correcta que salvó el día: tratar el special vdev como almacenamiento crítico de producción

Una organización financiera ejecutaba OpenZFS con un special vdev para mantener metadatos en flash. Lo hicieron bien: NVMe empresariales en espejo, monitorización estricta y la regla de que la capacidad del special nunca debe “llenarse accidentalmente”.

Durante un problema de firmware del proveedor que causaba timeouts intermitentes en NVMe bajo carga aleatoria sostenida, el special vdev empezó a reportar errores. La monitorización alertó temprano porque vigilaban contadores de error y latencia de la clase special por separado, no solo la salud del pool.

Degradaron el pool con gracia: ralentizaron jobs no esenciales, pospusieron scrubs y migraron las cargas de metadatos más calientes fuera del sistema antes de que la situación escalara. Tenían unidades de reemplazo preparadas y ya habían probado el procedimiento para intercambiar miembros del special vdev sin pánico.

La mayoría de equipos descubren que “los metadatos son especiales” cuando el pool no se puede importar. Este equipo lo descubrió un martes en una ventana de cambios y todos comieron. La fiabilidad suele parecer paranoia con mejor documentación.

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

1) ls y find son lentos, pero lecturas de archivos grandes son rápidas

Síntoma: Lecturas/escrituras en streaming alcanzan el ancho de banda esperado; el recorrido de directorios y herramientas con muchos stat se arrastran.

Causa raíz: Fallos de metadatos en ARC; metadatos en vdevs lentos; IOPS insuficientes para lecturas aleatorias pequeñas.

Solución: Aumentar la efectividad del ARC (más RAM, primarycache=metadata en datasets de streaming), desplegar un special vdev en espejo, reducir churn de snapshots.

2) “Añadimos SLOG y nada mejoró”

Síntoma: Latencia de la app sin cambios tras instalar un log device rápido.

Causa raíz: El cuello de botella son lecturas de metadatos o operaciones asíncronas; SLOG solo ayuda escrituras síncronas.

Solución: Confirma la tasa de escrituras síncronas; mantén el SLOG si hace falta, pero arregla la ruta de metadatos (ARC, special vdev, conteo de objetos, snapshots).

3) Picos de latencia durante scrubs/resilvers

Síntoma: IO de usuario lenta durante operaciones de mantenimiento.

Causa raíz: Desplazamiento del ARC por lecturas de scrub; contención en vdevs; lecturas de metadatos empujadas a medios más lentos; saturación del special vdev.

Solución: Programa scrubs fuera de pico, limita donde se pueda, asegura que el special vdev no esté subdimensionado y evita escaneos pesados en horas críticas.

4) Borrar archivos es lento y el espacio no vuelve

Síntoma: rm -rf tarda una eternidad; el pool sigue lleno.

Causa raíz: Snapshots mantienen bloques vivos; actualizaciones de metadatos masivas; frees diferidos entre TXGs.

Solución: Reduce retención/frecuencia de snapshots en datasets con churn; considera rotación periódica de datasets (nuevo dataset + migración) para scratch.

5) “Aumentamos recordsize y sigue lento”

Síntoma: Ajustar recordsize no mejoró operaciones dominadas por metadatos.

Causa raíz: Recordsize afecta bloques de datos de archivos, no operaciones de directorio ni la huella de dnodes.

Solución: Enfócate en metadatos: ARC, special vdev, estrategia xattr/dnodesize, política de snapshots y aislamiento de cargas.

6) El special vdev se llena inesperadamente

Síntoma: La asignación en special vdev crece hasta casi llenarse.

Causa raíz: Se están asignando bloques pequeños a special por special_small_blocks, o crecimiento masivo de metadatos por explosión de objetos.

Solución: Reevalúa el umbral de special_small_blocks, expande la capacidad del special vdev (añade otro espejo) y controla el crecimiento de objetos (políticas de limpieza).

Broma #2: Un special vdev es “especial” de la misma manera que un punto único de fallo es “especial”: llama la atención a las 3 a.m.

Listas de verificación / plan paso a paso

Paso 1: Clasifica la carga (no adivines)

  1. Ejecuta zpool iostat -v durante la ralentización.
  2. Temporiza una operación intensiva en metadatos (recorrido de directorio) y una intensiva en datos (lectura secuencial).
  3. Si las ops son altas y el ancho de banda bajo, trátalo como IO pequeña/metadatos.

Paso 2: Haz que el ARC trabaje para ti

  1. Chequea tamaño y tasa de fallos del ARC (usa arcstat).
  2. Si cargas en streaming comparten el pool, pon primarycache=metadata en esos datasets.
  3. Si el ARC es simplemente demasiado pequeño para el working set, añade RAM antes de comprar más discos. Sí, en serio.

Paso 3: Pon los metadatos en el medio correcto

  1. Si tienes pools en HDD que sirven cargas intensivas en metadatos, planifica un special vdev en espejo sobre NVMe empresariales.
  2. Dimensiona con holgura. Los metadatos crecen con objetos y snapshots, no sólo con “bytes usados”.
  3. Monitorízalo por separado: latencia, errores y asignación.

Paso 4: Controla el churn y los snapshots como un adulto

  1. Identifica datasets con alto churn (CI caches, staging temporal, capas de contenedores).
  2. Reduce frecuencia y retención de snapshots en esos datasets.
  3. Considera rotación de datasets: crea uno nuevo, cambia la carga y destruye el antiguo cuando sea seguro. Esto evita metadatos patológicos de larga vida.

Paso 5: Prueba cambios con un microbenchmark realista

  1. Usa un árbol de directorios representativo.
  2. Mide comportamiento con caché fría y caliente.
  3. Valida con zpool iostat y estadísticas ARC durante la prueba, no después.

Paso 6: Higiene operativa que reduce incidentes “misteriosos”

  1. Mantén la utilización del pool bajo control (evita vivir por encima de ~80% a menos que te gusten las sorpresas).
  2. Mantén horarios de scrub previsibles.
  3. Documenta valores por defecto de datasets: xattr, dnodesize, primarycache, políticas de snapshot.

Una cita que debería estar en tu runbook

Idea parafraseada, atribuida a John Allspaw: “En sistemas complejos, el éxito y el fracaso vienen del mismo lugar: trabajo normal y decisiones normales.”

Preguntas frecuentes

1) ¿Qué es exactamente un dnode, en términos operativos?

Un dnode es la entrada de metadatos para un objeto ZFS (archivo, directorio, objeto de dataset, etc.). Si tu carga hace muchas stat(), búsquedas, creaciones, eliminaciones o xattrs, el acceso a dnode está en la ruta crítica.

2) ¿Los metadatos siempre están en el special vdev si lo tengo?

Normalmente los metadatos se asignan a la clase special cuando está presente, pero el comportamiento puede variar con features y ajustes. Además, los “bloques pequeños” pueden ir allí si special_small_blocks está activado. Debes asumir que el special vdev es crítico y monitorizarlo en consecuencia.

3) ¿Siempre debo habilitar special_small_blocks?

No. Es potente, pero puede llenar tu special vdev y convertir un acelerador de metadatos en un problema de capacidad. Úsalo cuando tu carga esté dominada por bloques pequeños y tengas suficiente flash en espejo con holgura.

4) ¿Aumentar recordsize ayuda a metadatos?

Poco. recordsize trata sobre bloques de datos de archivo. Las operaciones de metadatos están dominadas por dnodes, ZAP, bloques indirectos y otras estructuras de metadatos.

5) ¿Añadir un SLOG arreglará recorridos de directorio lentos?

No. SLOG ayuda la latencia de escrituras síncronas. La lentitud en recorridos de directorios suele ser lecturas de metadatos y comportamiento de caché. Mantén el SLOG si tu carga lo necesita, pero no esperes que arregle bloqueos de lectura.

6) ¿Por qué la segunda ejecución de find a veces es mucho más rápida?

Porque el ARC se calentó. La primera ejecución trajo metadatos a la caché. Si la segunda no es más rápida, o el working set no cabe en ARC o alguna otra carga lo está expulsando.

7) ¿Cómo afectan los snapshots al rendimiento de metadatos?

Los snapshots preservan punteros de bloque antiguos, lo que incrementa la cantidad de metadatos alcanzables. En datasets con churn, eso puede amplificar el costo de eliminaciones y aumentar la fragmentación, lo que con el tiempo ralentiza operaciones de metadatos.

8) ¿Cuál es la “victoria rápida” más segura cuando los metadatos son el cuello de botella?

Frecuentemente: poner primarycache=metadata en datasets de streaming que están contaminando el ARC y reducir retención de snapshots en datasets con churn. Estos dos cambios pueden mejorar las tasas de aciertos de metadatos sin cambiar formatos en disco.

9) ¿Debo cambiar dnodesize en un dataset existente?

Con cuidado. Afecta objetos nuevos y los objetos existentes no necesariamente “encogerán”. Si necesitas un estado limpio, crea un dataset nuevo con las propiedades deseadas y migra.

10) Si en papel tengo muchas IOPS, ¿por qué las operaciones de metadatos siguen bloqueándose?

Porque los metadatos suelen requerir lecturas dependientes en secuencia. No puedes paralelizar una cadena de “leer puntero para encontrar el siguiente puntero” como puedes paralelizar lecturas grandes. La latencia domina.

Conclusión: siguientes pasos que realmente mueven la aguja

Si sacas una lección operacional del comportamiento de metadatos en ZFS, que sea esta: deja de tratar el “rendimiento de almacenamiento” como un problema de ancho de banda. Para muchas cargas reales, es un problema de caché y latencia disfrazado.

Haz lo siguiente:

  • Ejecuta el guion de diagnóstico rápido durante el próximo incidente y clasifica correctamente el cuello de botella.
  • Protege el ARC de cargas en streaming con primarycache=metadata a nivel de dataset donde proceda.
  • Si eres intensivo en metadatos sobre HDD, deja de negociar con la física: usa un special vdev en espejo sobre flash empresarial, dimensionado con holgura.
  • Audita las políticas de snapshot en datasets con churn y deja de snapshotear scratch como si fuera historia invaluable.
  • Documenta los valores por defecto de los datasets y aplícalos. La mayoría de desastres de metadatos empiezan por “solo un cambio de propiedad”.

ZFS hará exactamente lo que le pidas, y lo seguirá haciendo mucho después de que lo hayas olvidado. Haz que la ruta de metadatos sea aburrida, predecible y rápida. Tu yo del futuro tendrá menos pagers y mejores fines de semana.

← Anterior
CPU de Docker al 100%: identifica el contenedor ruidoso y limita correctamente
Siguiente →
Errores de permisos de archivos en WordPress: Qué deben ser 755/644 y por qué

Deja un comentario