Optimización de pools ZFS solo NVMe: latencia, IRQs y los límites reales

¿Te fue útil?

Si migraste ZFS a “NVMe rápido” y esperabas que la latencia desapareciera, probablemente te hayas topado con los dos villanos del almacenamiento moderno: latencia extrema y contención invisible. Tus benchmarks parecen heroicos, pero en producción todavía hay pausas de 200–800 ms en el peor momento posible. A los usuarios no les importa que tu mediana sea 80 µs cuando su página de pago sufre en el percentil 99.9.

Los pools solo NVMe eliminan un cuello de botella y exponen el resto. Ahora los límites son la planificación de CPU, la distribución de interrupciones, la encolación, el comportamiento del grupo de transacciones ZFS (TXG) y la diferencia entre “dispositivo rápido” y “sistema rápido”. Afinemos para la realidad: latencia estable bajo carga mixta, no solo gráficos bonitos de fio.

Los límites reales: NVMe hace obvio lo demás

En discos magnéticos o incluso SSD SATA, la latencia del almacenamiento domina y oculta muchos pecados. En NVMe, el almacenamiento ya no es “la parte lenta” la mayor parte del tiempo. Eso es una buena y una mala noticia.

Buena noticia: puedes obtener IOPS increíbles y gran ancho de banda con muy poco ajuste, especialmente para lecturas y escrituras asíncronas. Mala noticia: el cuello de botella se mueve. Si ves picos de latencia en pools ZFS solo NVMe, tus primeros sospechosos son:

  • Planificación de CPU y manejo de interrupciones: un core haciendo todo el trabajo mientras otros están inactivos.
  • Encolación en la capa de bloques o en el driver NVMe: colas profundas ocultan la congestión hasta que dejan de hacerlo.
  • Cadencia del grupo de transacciones ZFS (TXG): ráfagas de writeback cada pocos segundos, además de las semánticas de sync.
  • Presión de memoria y comportamiento del ARC: tormentas de reclaim, kswapd y hilos bloqueados.
  • Suposiciones del “camino rápido”: por ejemplo, cargas centradas en metadatos, escrituras aleatorias pequeñas y aplicaciones intensivas en sync.

Estás afinando un sistema distribuido en una sola máquina: hilos de aplicación, hilos del kernel, interrupciones, etapas del pipeline ZFS y un controlador con su propio firmware. NVMe no elimina la complejidad; solo deja de enmascararla.

Hechos e historia interesantes (que importan)

  • ZFS no nació en la era NVMe. El diseño original asumía discos giratorios y la coalescencia de escrituras durante segundos; el comportamiento de TXG todavía refleja eso.
  • NVMe fue diseñado para el paralelismo. Existen múltiples colas de envío/completado específicamente para reducir la contención por locks y las tormentas de interrupciones frente a AHCI/SATA.
  • La coalescencia de interrupciones es anterior a NVMe. Las NICs llevan tiempo intercambiando latencia por rendimiento al agrupar interrupciones; los dispositivos NVMe y sus drivers hacen cosas similares.
  • Las 4K no siempre fueron la norma. Advanced Format y luego las realidades de los bloques de borrado de los SSD forzaron conversaciones sobre alineación; el ashift de ZFS es básicamente “no confío en que el dispositivo diga la verdad”.
  • TRIM/Discard solía asustar. Los SSD tempranos podían bloquearse fuertemente con discard; “no activar TRIM” fue un consejo razonable en su momento. Los NVMe modernos lo manejan mejor en general, pero no siempre de forma elegante bajo carga.
  • El checksum no es un impuesto que puedas ignorar. En medios rápidos, CRC y compresión pueden convertirse en coste CPU de primer orden, especialmente con IOs pequeños y alta concurrencia.
  • ZIL no es una caché de escritura. El intent log existe para satisfacer las semánticas POSIX de sync; malinterpretarlo conduce a ajustes raros y a equipos financieros decepcionados.
  • Los números de IOPS se volvieron un arma. “Millones de IOPS” de marketing hizo que la gente olvidara la latencia de cola y las cargas mixtas; la gente de operaciones tuvo que arreglar eso.

Modelo mental: de dónde viene la latencia en ZFS sobre NVMe

Cuando la latencia se dispara, necesitas un mapa. Aquí está el mapa.

ZFS write pipeline, simplificado

  1. Application write() golpea la caché de páginas / lógica ARC y el DMU de ZFS.
  2. Si se requiere sync, ZFS debe registrar la intención en el ZIL (en-pool o SLOG) antes de confirmar.
  3. Fase open del TXG: las escrituras se acumulan en memoria.
  4. Fase sync del TXG: los datos se escriben en el pool principal, las metaslabs asignan espacio, se calculan checksums, se aplica compresión, etc.
  5. Flush/FUA/barriers: el NVMe subyacente debe prometer durabilidad para las rutas síncronas.

Read pipeline, simplificado

  1. Hit en ARC: barato, mayormente comportamiento de CPU/caché.
  2. Miss en ARC: ZFS emite IO; el dispositivo devuelve datos; se verifican checksums; descompresión opcional.

NVMe path, simplificado

  1. IO de bloque encolado en el kernel.
  2. El driver NVMe lo envía a la(s) cola(s) del dispositivo.
  3. El dispositivo completa; la interrupción (o polling) notifica a la CPU; se procesa la finalización.
  4. Wakeups y cambios de contexto entregan los datos a los hilos que esperan.

Los picos de latencia ocurren cuando cualquier etapa se serializa. En NVMe, la serialización suele estar del lado de la CPU: una cola, un core, un lock, una configuración equivocada. O es la propia cadencia de ZFS: sincronizaciones de TXG, contención de metaslab o flushes de escrituras síncronas.

Una cita para mantenerte honesto. La esperanza no es una estrategia. — General Gordon R. Sullivan

Guía rápida de diagnóstico (primero/segundo/tercero)

Esta es la guía de “abrir el portátil a las 2 a. m.”. Estás tratando de responder a una pregunta: ¿dónde se va el tiempo?

Primero: confirma que el síntoma es latencia de almacenamiento (no app o red)

  • Comprueba si los hilos de la aplicación están bloqueados en IO (estado D) o en CPU.
  • Comprueba la latencia de extremo a extremo alrededor del incidente (p99/p999) y correlaciónala con IO wait y carga de interrupciones.

Segundo: decide si es el camino de lectura, escritura asíncrona o escritura síncrona

  • Las cargas intensivas en sync (bases de datos, NFS con sync, imágenes de VM) se comportan distinto que los escritores asíncronos orientados a logs.
  • Busca actividad del ZIL y comportamiento de flush si los picos de latencia son periódicos o coinciden con fsync.

Tercero: identifica si el cuello de botella es CPU/IRQs, encolación o TXG

  • Si una CPU está saturada con interrupciones/softirqs: arregla la afinidad de IRQ y el mapeo de colas.
  • Si las colas de IO están profundas y los tiempos de espera suben: estás encolando; reduce la concurrencia, ajusta el scheduler o afronta límites del firmware/controlador.
  • Si los bloqueos se alinean con el sync del TXG: no te has “acabado el NVMe”, se te ha acabado la capacidad del pipeline de ZFS (a menudo presión de metaslab/asignación o memoria).

Broma #1: La latencia de cola es como un gato doméstico—tranquilo todo el día, luego a las 3 a. m. corre por tu dashboard sin motivo.

Tareas prácticas: comandos, significado de la salida y decisiones

Estas no son “ejecuta comandos por diversión”. Cada una termina con una decisión. Ejecútalas durante carga normal y otra vez durante la ventana del incidente si es posible.

Task 1: Confirmar topología del pool y ashift (alineación)

cr0x@server:~$ sudo zpool status -v
  pool: tank
 state: ONLINE
  scan: scrub repaired 0B in 00:18:21 with 0 errors on Mon Dec 23 02:10:01 2025
config:

        NAME                        STATE     READ WRITE CKSUM
        tank                        ONLINE       0     0     0
          mirror-0                  ONLINE       0     0     0
            nvme0n1p2               ONLINE       0     0     0
            nvme1n1p2               ONLINE       0     0     0

errors: No known data errors
cr0x@server:~$ sudo zdb -C tank | egrep -i 'ashift|vdev_tree' -n | head
55:        ashift: 12

Qué significa: ashift: 12 implica sectores de 4K. Para muchos discos NVMe, 4K es correcto; algunos prefieren alineación de 8K/16K, pero ZFS no cambia ashift después de la creación.

Decisión: Si ashift es 9 (512B) en SSD/NVMe, planifica una reconstrucción. Ningún sysctl inteligente te salvará de las penalizaciones read-modify-write.

Task 2: Revisar propiedades de dataset que comúnmente afectan la latencia

cr0x@server:~$ sudo zfs get -o name,property,value -s local,default recordsize,compression,atime,sync,logbias,primarycache,secondarycache xattr,dnodesize tank
NAME  PROPERTY        VALUE
tank  recordsize      128K
tank  compression     lz4
tank  atime           off
tank  sync            standard
tank  logbias         latency
tank  primarycache    all
tank  secondarycache  all
tank  xattr           sa
tank  dnodesize       legacy

Qué significa: Esta es la verdad base para el comportamiento. sync=standard suele ser correcto; logbias=latency indica “optimizar sync”.

Decisión: Cambia propiedades solo según la carga, no por superstición. Si alojas bases de datos, considera recordsize más pequeño (16K/32K) y deja compression=lz4 activado a menos que la CPU esté claramente limitada.

Task 3: Medir latencia del pool y encolación desde la perspectiva de ZFS

cr0x@server:~$ sudo zpool iostat -v tank 1 5
              capacity     operations     bandwidth
pool        alloc   free   read  write   read  write
----------  -----  -----  -----  -----  -----  -----
tank        1.20T  2.30T   8.1K  12.4K   410M   980M
  mirror    1.20T  2.30T   8.1K  12.4K   410M   980M
    nvme0n1     -      -   4.0K   6.2K   205M   490M
    nvme1n1     -      -   4.1K   6.2K   205M   490M

Qué significa: Esto muestra la distribución de carga y si un dispositivo va rezagado. No muestra latencia por IO, pero sí muestra desequilibrio.

Decisión: Si un NVMe hace constantemente menos trabajo en un mirror, sospecha firmware que limita, problemas de enlace PCIe o throttling térmico.

Task 4: Comprobar salud NVMe e indicadores de throttling

cr0x@server:~$ sudo nvme smart-log /dev/nvme0n1 | egrep -i 'temperature|warning|critical|media|percentage|thm'
temperature                             : 71 C
critical_warning                        : 0x00
percentage_used                         : 4%
media_errors                            : 0
warning_temp_time                       : 12
critical_comp_time                      : 0

Qué significa: La temperatura y el tiempo acumulado por encima de umbrales de advertencia importan. Un disco puede estar “sano” y aun así regular su rendimiento bajo escrituras sostenidas.

Decisión: Si warning temp time sube durante los incidentes, arregla la ventilación/disipadores, reduce la amplificación de escritura sostenida (recordsize, patrones de sync) o mueve el dispositivo a una ranura más fresca.

Task 5: Verificar velocidad y ancho de enlace PCIe (el límite silencioso de throughput)

cr0x@server:~$ sudo lspci -s 0000:5e:00.0 -vv | egrep -i 'LnkCap|LnkSta'
LnkCap: Port #0, Speed 16GT/s, Width x4
LnkSta: Speed 16GT/s, Width x4

Qué significa: Se espera un enlace Gen4 x4 para muchos NVMe. Si ves x2 o Gen3 inesperadamente, has encontrado tu momento de “NVMe no es rápido”.

Decisión: Arregla la bifurcación de lanes en BIOS, la elección de slot, los risers o el cableado del backplane M.2. Esto es hardware, no ajuste.

Task 6: Inspeccionar latencia en la capa de bloques (await) y comportamiento de profundidad de cola

cr0x@server:~$ iostat -x -d 1 5 nvme0n1 nvme1n1
Device            r/s     w/s   rKB/s   wKB/s  rrqm/s  wrqm/s  r_await  w_await  aqu-sz  %util
nvme0n1         4100   6200  210000  505000     0.0     0.0     0.35     1.90    6.20   78.0
nvme1n1         4200   6200  210000  505000     0.0     0.0     0.36     1.85    6.05   77.5

Qué significa: await es la latencia promedio de la petición. aqu-sz muestra el tamaño medio de la cola. aqu-sz alto con await en aumento significa encolación (congestión) más que latencia del dispositivo.

Decisión: Si aqu-sz es alto y %util cercano a 100% con await subiendo, estás saturando un dispositivo o una cola. Reduce concurrencia, reparte carga o añade vdevs.

Task 7: Comprobar distribución de IRQs del driver NVMe (un core hace todo el trabajo)

cr0x@server:~$ grep -i nvme /proc/interrupts | head -n 12
  98:  10293812          0          0          0   PCI-MSI 524288-edge      nvme0q0
  99:   1938120     1980221     2018830     1999012   PCI-MSI 524289-edge      nvme0q1
100:   1912202     1923301     1899987     1901120   PCI-MSI 524290-edge      nvme0q2
101:   1908821     1910092     1903310     1899922   PCI-MSI 524291-edge      nvme0q3

Qué significa: Si una línea IRQ se dispara en CPU0 mientras las otras están planas, estás limitando el manejo de interrupciones. La cola 0 (a menudo admin + IO) puede ser especial; no te alarmes por nvme0q0 en solitario, pero vigila los desequilibrios.

Decisión: Si las interrupciones no están bien distribuidas, activa irqbalance (si procede) o asigna colas deliberadamente a CPUs alineadas con la localidad NUMA.

Task 8: Confirmar localidad NUMA y si el NVMe está “lejos” de la CPU que hace el trabajo

cr0x@server:~$ sudo cat /sys/class/nvme/nvme0/device/numa_node
1
cr0x@server:~$ numactl -H | sed -n '1,25p'
available: 2 nodes (0-1)
node 0 cpus: 0 1 2 3 4 5 6 7 8 9 10 11
node 1 cpus: 12 13 14 15 16 17 18 19 20 21 22 23
node distances:
node   0   1
  0:  10  21
  1:  21  10

Qué significa: Si NVMe está en el nodo NUMA 1 pero la mayoría de interrupciones y hilos ZFS corren en el nodo 0, pagas latencia cruzada de nodo y penalizaciones de ancho de banda de memoria.

Decisión: Alinea la afinidad de IRQ y, en casos extremos, la colocación de procesos (hilos de base de datos) con el nodo NUMA del NVMe.

Task 9: Comprobar estados de hilos ZFS y buscar presión de TXG sync

cr0x@server:~$ ps -eLo pid,tid,cls,rtprio,pri,psr,stat,wchan:20,comm | egrep 'txg|z_wr_iss|z_wr_int|arc_reclaim|dbu|zio' | head
  1423  1450 TS      -  19  12 S    txg_sync_thread  txg_sync
  1423  1451 TS      -  19  13 S    txg_quiesce      txg_quiesce
  1423  1462 TS      -  19  14 S    zio_wait        z_wr_int
  1423  1463 TS      -  19  15 S    zio_wait        z_wr_int
  1423  1488 TS      -  19  16 S    arc_reclaim_thr arc_reclaim

Qué significa: Ver hilos es normal. Ver muchos bloqueados en zio_wait durante picos sugiere presión en el backend; arc_reclaim ocupado sugiere presión de memoria.

Decisión: Si el reclaim se correlaciona con la latencia, deja de “tunear ZFS” y empieza a arreglar la memoria: límites de ARC, RSS de aplicaciones y comportamiento de reclaim del kernel.

Task 10: Vigilar presión de memoria y tormentas de reclaim

cr0x@server:~$ vmstat 1 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 4  0      0  52132  91244 8032100   0    0     2   180 8200 9900 22  9 66  3  0
 7  1      0  11200  88010 7941200   0    0     0  2500 14000 22000 28 14 50  8  0

Qué significa: Subida de b (bloqueados), disminución de free y aumento de context switches durante incidentes de latencia es un olor clásico de “el sistema está thrashing”. En NVMe, el dispositivo es lo bastante rápido como para hacer que los bucles de reclaim sean “interesantes”.

Decisión: Si el reclaim se correlaciona, limita ARC (o añade RAM), deja de sobreasignar y averigua qué proceso está creciendo sin control.

Task 11: Confirmar carga síncrona y si los flushes dominan

cr0x@server:~$ sudo zpool iostat -r -w tank 1 3
              capacity     operations     bandwidth
pool        alloc   free   read  write   read  write
----------  -----  -----  -----  -----  -----  -----
tank        1.20T  2.30T   2.2K  18.1K   120M  1.10G

                              read                            write
pool        ops   bytes   latency  disk latency    ops   bytes   latency  disk latency
----------  ---  -----  ---------  ------------   ---  -----  ---------  ------------
tank       2.2K   120M    420us       160us      18.1K  1.10G    3.8ms      2.9ms

Qué significa: La latencia a nivel ZFS frente a la latencia de disco ayuda a separar costes del pipeline ZFS del tiempo del dispositivo. Si la latencia ZFS es mucho mayor que la latencia de disco, estás pagando CPU/locking/encolación dentro de la pila.

Decisión: Si la latencia de disco es baja pero la de ZFS alta, mira CPU, IRQs y concurrencia de ZFS; no compres más NVMe.

Task 12: Medir tiempo de softirq por CPU (las completaciones de almacenamiento aparecen aquí)

cr0x@server:~$ mpstat -P ALL 1 3 | egrep -A1 'CPU|Average|  0| 12'
CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal  %guest  %gnice   %idle
  0     3.12    0.00    6.01    0.21    0.90   28.44    0.00    0.00    0.00   61.31
 12    15.90    0.00   10.82    0.12    0.10    3.20    0.00    0.00    0.00   69.86

Qué significa: CPU0 gastando 28% en softirq suele ser “demasiado trabajo de completación IO en un solo core”. Es un culpable frecuente cuando NVMe “debería ser rápido” pero no lo es.

Decisión: Arregla la afinidad de IRQ, habilita múltiples colas y alinea con NUMA. Si es necesario, considera el polling NVMe (con cuidado) para cargas sensibles a la latencia.

Task 13: Inspeccionar configuración de colas y elecciones del scheduler

cr0x@server:~$ cat /sys/block/nvme0n1/queue/scheduler
[none] mq-deadline kyber bfq
cr0x@server:~$ cat /sys/block/nvme0n1/queue/nr_requests
1023

Qué significa: Para NVMe, none es común y a menudo correcto. Los schedulers pueden ayudar a la equidad; también pueden añadir overhead. nr_requests afecta la profundidad de la cola en la capa de bloques, lo que puede afectar la latencia de cola.

Decisión: Si la latencia de cola es tu enemiga, evita colas enormes que oculten congestión. Considera mq-deadline para latencia más predecible si none produce p99 feo bajo IO mixto.

Task 14: Revisar tunables de ZFS que debes tratar como “última milla”, no magia

cr0x@server:~$ sudo sysctl kstat.zfs.misc.arcstats.size kstat.zfs.misc.arcstats.c kstat.zfs.misc.arcstats.mfu_size kstat.zfs.misc.arcstats.mru_size
kstat.zfs.misc.arcstats.size = 68412985344
kstat.zfs.misc.arcstats.c = 73014444032
kstat.zfs.misc.arcstats.mfu_size = 51200901120
kstat.zfs.misc.arcstats.mru_size = 15600930816

Qué significa: El tamaño y objetivo del ARC importan. Si ARC compite con la aplicación por memoria, obtendrás churn de reclaim. Si ARC es demasiado pequeño, thrashing de lecturas y metadatos.

Decisión: Establece explícitamente el máximo de ARC en máquinas compartidas. En appliances de almacenamiento dedicados, deja que ARC respire a menos que tengas una razón concreta para no hacerlo.

Task 15: Confirmar comportamiento TRIM/autotrim y si coincide con picos

cr0x@server:~$ sudo zpool get autotrim tank
NAME  PROPERTY  VALUE     SOURCE
tank  autotrim  on        local
cr0x@server:~$ sudo zpool trim -v tank
trim operation for tank has completed
trim rate: 0B/s
total trimmed: 0B

Qué significa: Autotrim es generalmente bueno en SSD/NVMe, pero algunos dispositivos manejan mal la desasignación en segundo plano, especialmente bajo escritura intensa.

Decisión: Si puedes correlacionar picos con trimming, pon autotrim=off y programa trims en una ventana de baja actividad. Si el rendimiento empeora con el tiempo sin trim, el GC del dispositivo necesita ayuda—así que no lo apagues para siempre sin más análisis.

Ajustes de dataset y pool que valen la pena (y lo que es cargo cult)

Compresión: deja lz4 activado, luego mide CPU

En pools solo NVMe, la compresión suele ayudar más de lo que perjudica. Reduce bytes escritos y leídos, lo que reduce la presión en el backend y puede mejorar la latencia. El coste es CPU. En CPUs modernas, lz4 suele ser una ganga.

¿Cuándo perjudica? Datos de alta entropía (ya comprimidos), escrituras síncronas pequeñas o cuando estás limitado por CPU debido a interrupciones y checksumming ya intensos.

recordsize: deja de usar 128K por postura moral

recordsize trata la forma del IO. Bases de datos, imágenes de VM y apps orientadas a logs suelen preferir 8K–32K. Workloads de streaming (backups, media, scans analíticos) prefieren 128K o más.

En NVMe, recordsize grande aún puede estar bien, pero las actualizaciones pequeñas aleatorias a registros grandes provocan amplificación de escritura. El dispositivo es suficientemente rápido como para que no lo notes hasta que tu p99 muerde.

atime y xattr: toggles aburridos, ganancias reales

Desactivar atime reduce escrituras de metadatos. Almacenar xattrs como SA (xattr=sa) reduce IO para cargas con muchas ACL y metadatos. Ninguno es glamuroso; ambos suelen ser correctos.

dnodesize: cargas ricas en metadatos merecen mejores valores por defecto

Los dnodes grandes pueden empaquetar más metadatos en menos lecturas, lo que importa para recorridos de directorios, capas de imágenes de contenedores y ráfagas de archivos pequeños. La desventaja es un uso ligeramente mayor de espacio.

Hazlo por dataset cuando tengas una carga intensiva en metadatos y estés cansado de fingir que NVMe resuelve por sí solo el fan-out de metadatos.

sync=disabled: no lo hagas a menos que puedas perder datos y conservar el puesto

Deshabilitar sync convierte “durable” en “optimista”. Hace que los benchmarks parezcan que actualizaste las leyes de la física. También cambia el significado de fsync. Si sirves NFS, ejecutas bases de datos o alojas VMs, esto no es un control de ajuste; es una decisión contractual.

Broma #2: sync=disabled es el equivalente en almacenamiento de quitar el detector de humo porque hace ruido.

IRQs, colas y CPU: el cuello de botella poco glamuroso

NVMe está construido para el paralelismo, pero el sistema tiene que aceptar esa invitación. Si tus interrupciones caen en una CPU y el procesamiento de completaciones arde en softirq, tu “almacenamiento rápido” se convierte en una máquina de latencia monocore.

Cómo se ve “bien”

  • Varias colas IO NVMe están activas (nvme0q1..qN), no solo la cola 0.
  • Las interrupciones están distribuidas entre CPUs en el nodo NUMA correcto.
  • El tiempo de softirq es no trivial pero no está concentrado en un solo core.
  • Los hilos de aplicación y los hilos ZFS no compiten por las mismas pocas CPUs.

Qué puedes cambiar con seguridad

Afinidad de IRQ: asigna IRQs NVMe a un conjunto de CPUs locales al nodo NUMA del dispositivo. No asignes todo a CPU0 porque un post de 2017 dijo “CPU0 maneja interrupciones.” Ese post se escribió cuando la gente llevaba otros zapatos.

irqbalance: puede ayudar, puede perjudicar. Está bien para servidores de propósito general. Para almacenamiento crítico en latencia, a menudo prefieres pinning deliberado.

Scheduler: none está bien para throughput puro. Si tienes IO mixto y te importa p99, prueba mq-deadline. No asumas; mide.

Escrituras síncronas, mitos del SLOG y lo que cambia con NVMe

Los pools solo NVMe crean una tentación común: “Si el pool ya es NVMe, ¿necesito un SLOG?” La respuesta honesta: generalmente no. La respuesta más útil: depende de tu patrón de escrituras síncronas y qué significa “durable” en tu entorno.

Aclara qué hace ZIL/SLOG

El ZIL (ZFS Intent Log) existe para registrar suficiente intención para que las escrituras síncronas puedan ser reconocidas de forma segura antes del commit completo del TXG. En un crash, el replay hace que las últimas operaciones síncronas comprometidas parezcan duraderas.

Un SLOG es un dispositivo separado usado para almacenar entradas del ZIL. Ayuda cuando el pool principal es lento en escrituras sync o flushes. En un pool NVMe, el pool principal puede ya ser bueno en esto. Añadir un SLOG aún puede ayudar si:

  • tu NVMe del pool principal tiene pobre latencia de flush bajo carga,
  • tienes cargas intensivas en sync y quieres aislamiento,
  • quieres comportamiento de dispositivo con protección contra pérdida de energía.

El limitador real: comportamiento de flush y durabilidad

El rendimiento sync frecuentemente trata sobre las semánticas de flush de caché, no la velocidad bruta del medio. Un disco puede hacer 700k IOPS y aun así tener una latencia fsync fea si su firmware tiene un comportamiento conservador de flush bajo escrituras sostenidas.

Comportamiento TXG, bloqueos y por qué el “medio rápido” no te salva

Las escrituras de ZFS se organizan en grupos de transacciones que sincronizan periódicamente. Incluso en NVMe, este comportamiento de “batching” puede crear picos de latencia periódicos, especialmente cuando:

  • tienes ráfagas de escrituras aleatorias que causan presión en el allocator,
  • se acumulan actualizaciones de metadatos (snapshots, archivos pequeños, atime),
  • el sistema está limitado de memoria y no puede bufferizar de forma suave.

Reconocer síntomas relacionados con TXG

  • Los picos de latencia aparecen con una cadencia (a menudo en segundos).
  • El uso de CPU sube en hilos del kernel, no en espacio de usuario.
  • La latencia del dispositivo permanece moderada, pero la latencia a nivel ZFS aumenta.

Existe tuning de TXG, pero no es por donde empiezas. Empieza asegurando que el sistema no se ahoga con interrupciones, reclaim de memoria o formas patológicas de IO.

Special vdevs y metadatos en pools solo NVMe

Los special vdevs a menudo se promocionan como “pon metadatos en SSD”. En un pool solo NVMe, ya hiciste eso. ¿Entonces por qué hablar de ello?

Porque “solo NVMe” no significa “todos los NVMe se comportan igual”, y porque metadatos y bloques pequeños tienen perfiles de rendimiento diferentes que las escrituras secuenciales grandes. Un special vdev aún puede ayudar si:

  • quieres aislar metadatos y bloques pequeños en un dispositivo de menor latencia y con protección ante pérdida de energía,
  • quieres reducir la presión de fragmentación en los vdevs principales para cargas mixtas,
  • necesitas latencia consistente para IO pequeño incluso bajo escrituras de streaming grandes.

Pero los special vdevs son un compromiso: si lo pierdes sin redundancia, pierdes el pool. Míralo en espejo o no lo hagas.

Tres mini-historias corporativas desde las trincheras de latencia

Incidente causado por una suposición errónea: “mirror NVMe significa sin más latencia”

Una empresa SaaS mediana migró su cluster Postgres primario a un pool ZFS en mirror NVMe. El plan de migración fue cuidadoso: scrub antes del corte, snapshots, rollback. El gráfico de almacenamiento se veía precioso en staging. Todos durmieron bien.

En producción, cada pocos minutos la base de datos se congelaba el tiempo justo para disparar timeouts en la aplicación. No era un corte total—peor. Una lenta sangría de errores y reintentos. El on-call seguía mirando el throughput de disco, que no estaba saturado, y el SMART de NVMe, que parecía limpio.

La suposición errónea fue “disco rápido significa que IO no puede ser el problema”. El verdadero problema era un único core de CPU pegado en softirq durante el pico de tráfico. Las completaciones NVMe aterrizaban en un conjunto reducido de CPUs debido a una combinación de afinidad IRQ por defecto y colocación NUMA. Bajo carga normal, iba bien. Bajo pico, ese core se convirtió en cuello de botella de completaciones, la cola creció y los hilos de la base de datos se acumulaban detrás.

Lo arreglaron alineando las IRQ NVMe a CPUs locales al nodo NUMA del NVMe y moviendo el proceso Postgres fuera del conjunto de CPUs más calientes en interrupciones. El pool no cambió. Los discos no cambiaron. La latencia sí.

Optimización que salió mal: “Subamos profundidad de cola y deshabilitemos sync”

Otra compañía gestionaba hosts de virtualización en NVMe ZFS. Tuvieron un “empujón de rendimiento” trimestral, que en jerga corporativa significa “alguien vio un slide de benchmark”. Un ingeniero bienintencionado aumentó las colas de la capa de bloques y permitió más IOs pendientes, persiguiendo números más altos en una prueba sintética.

Los números mejoraron. El throughput subió. Todos aplaudieron. Luego llegó el lunes. Aparecieron cargas reales: lecturas y escrituras mixtas, metadatos y ráfagas de sync desde filesystems de invitados.

La latencia de cola se hundió. Las colas profundas ocultaron la congestión hasta que fue demasiado tarde, y cuando el sistema se quedó atrás, lo hizo en trozos grandes y feos. También probaron sync=disabled en datasets de VM “solo por rendimiento”. Ese cambio mejoró fsync en invitados—hasta que un crash del host convirtió “solo por rendimiento” en una fiesta de reparación de filesystems para múltiples VMs.

Revirtieron los cambios de profundidad de cola, bajaron la concurrencia y restauraron las semánticas correctas de sync. El rendimiento ya no lucía espectacular en slides, pero el pager dejó de sonar.

Práctica aburrida pero correcta que salvó el día: pinning, observabilidad y runbook

Un equipo empresarial que corría un store de objetos interno tenía la costumbre de hacer tres cosas aburridas: fijar el máximo de ARC explícitamente, documentar la afinidad de IRQ y ejecutar una “prueba de latencia” semanal donde capturaban 10 minutos de iostat/mpstat/zpool iostat durante las horas pico del negocio.

Una semana, la latencia empezó a subir sutilmente. No lo suficiente para causar outages, pero suficiente para que las alarmas de SLO despertaran a alguien. Sus datos de prueba mostraron un patrón nuevo: el w_await de un dispositivo NVMe se estaba doblando bajo writeback sostenido, mientras su pareja en mirror se mantenía estable. El pool seguía en línea; nada “rompió”.

Porque ya tenían líneas base, no debatieron si era “normal”. Cambiaron el dispositivo en la próxima ventana de mantenimiento. El postmortem sugirió throttling térmico debido a una ruta de flujo de aire parcialmente obstruida tras un cambio rutinario de cableado.

Nada heroico. Ningún tunable mágico. Solo disciplina aburrida: líneas base, pinning y reemplazar hardware antes de que sea un incidente.

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

  • Síntoma: Gran latencia promedio, p99 horrible bajo carga.
    Causa raíz: Encolación + concentración de interrupciones; colas profundas ocultan congestión.
    Solución: Revisa iostat -x por aqu-sz en aumento; redistribuye IRQs NVMe; considera mq-deadline; reduce concurrencia donde sea posible.
  • Síntoma: “Stutters” periódicos cada pocos segundos durante escrituras intensas.
    Causa raíz: Ráfagas de TXG sync + presión de allocator/metaslab, a veces empeorado por churn de metadatos.
    Solución: Reduce escrituras de metadatos (atime=off, xattr=sa), ajusta recordsize por carga, asegura RAM suficiente; verifica latencia ZFS vs disco con zpool iostat -r -w.
  • Síntoma: NVMe muestra latencia de disco baja, pero la app ve alta latencia IO.
    Causa raíz: Cuello de botella CPU en checksum/compresión/manejo de interrupciones; contención por locks.
    Solución: Usa mpstat para hotspots de softirq; distribuye IRQs; confirma margen de CPU; evita compresiones pesadas; no sobre-paralelices IO pequeño.
  • Síntoma: Carga intensiva en sync (fsync) lenta incluso en NVMe.
    Causa raíz: Latencia de flush/FUA bajo carga; firmware; a veces capas de virtualización.
    Solución: Mide IO sync por separado; considera un dispositivo de log PLP en espejo; evita sync=disabled salvo que la política permita pérdida de datos.
  • Síntoma: Un dispositivo en un mirror rinde menos o se queda atrás.
    Causa raíz: Throttling térmico, degradación de enlace PCIe, peculiaridades de firmware.
    Solución: Revisa SMART temp time, estado de enlace con lspci y compara con iostat -x por dispositivo; arregla refrigeración o configuración de slot/BIOS.
  • Síntoma: Picos de latencia durante trims o después de borrados grandes.
    Causa raíz: Interacción TRIM/GC; firmware del dispositivo que se bloquea durante desasignación bajo carga.
    Solución: Desactiva autotrim temporalmente y programa trims; valida que el rendimiento a largo plazo no se degrade.

Listas de verificación / plan paso a paso

Paso a paso: estabilizar la latencia en un pool ZFS solo NVMe

  1. Base primero: captura zpool iostat, iostat -x, mpstat y /proc/interrupts durante la carga pico habitual.
  2. Confirma la realidad del hardware: velocidad/anchura PCIe; temperaturas NVMe; versiones de firmware si tu organización las rastrea.
  3. Arregla cuellos de botella de CPU obvios: distribución de IRQ; alineación NUMA; evita dejar a los hilos ZFS sin CPU.
  4. Valida semánticas de dataset: asegura que sync coincide con requisitos de durabilidad; pon atime=off donde convenga; mantén compression=lz4 salvo que la CPU demuestre ser el cuello de botella.
  5. Empareja recordsize con la carga: bases de datos/VMs generalmente más pequeñas; streaming más grande. Mide con tamaños IO parecidos a producción.
  6. Vigila presión de memoria: fija máximo de ARC si la máquina aloja apps; evita tormentas de reclaim.
  7. Re-prueba p99: no te quedes en el throughput promedio. Ejecuta una prueba mixta y mira las colas.
  8. Sólo entonces considera perillas exóticas: schedulers, profundidades de cola, polling. Cada cambio debe tener plan de rollback.

Lista de verificación: antes de culpar a ZFS

  • El enlace PCIe está a la velocidad/anchura esperada para cada NVMe.
  • Ningún NVMe está en territorio de advertencia térmica persistente.
  • Las IRQs están distribuidas y locales al nodo NUMA correcto.
  • El softirq de CPU no está concentrado en un solo core.
  • Hay suficiente RAM y no hay thrash por swap/reclaim.
  • Puedes explicar si el dolor es por sync, async, lectura o metadatos.

Preguntas frecuentes

1) ¿Necesito un SLOG en un pool solo NVMe?

Generalmente no. Añade un SLOG solo si tienes problemas medibles de latencia en escrituras sync y un dispositivo apropiado de baja latencia con protección ante pérdida de energía. Si no, solo añades complejidad por aparentar.

2) ¿Debo poner sync=disabled por rendimiento?

Sólo si la empresa acepta pérdida de datos en un crash y puedes demostrar que la aplicación es segura con ese riesgo. Para bases de datos, NFS y almacenamiento de VM, normalmente es una mala elección.

3) ¿Sigue siendo útil compression=lz4 en NVMe?

Sí, a menudo. Reduce bytes movidos y escritos, lo que puede mejorar la latencia de cola. Desactívala sólo después de confirmar que la CPU es el factor limitante.

4) ¿Por qué mi NVMe hace “solo” una fracción de las IOPS anunciadas?

Porque tu sistema no es un rig de benchmark del vendedor. Manejo de IRQ, NUMA, encolación, checksums de ZFS y mezcla de cargas reducen los números de portada. Además, el comportamiento de mirror y las semánticas de sync importan.

5) ¿Debo usar mq-deadline o none para NVMe?

Empieza con none. Si tienes p99 feo bajo IO mixto, prueba mq-deadline. Usa la latencia de cola medida, no el throughput, para decidir.

6) ¿Cómo sé si estoy limitado por CPU en lugar de por almacenamiento?

Si la latencia de disco es baja pero la latencia en la app/ZFS es alta, y ves alto %soft o un core saturado, estás limitado por CPU. Confírmalo con mpstat y la distribución de interrupciones.

7) ¿Más profundidad de cola siempre mejora el rendimiento?

No. Puede mejorar el throughput y empeorar la latencia. Las colas profundas cambian predictibilidad por movimiento a granel. Si tu carga es orientada al usuario, a menudo quieres colas controladas y colas de cola estables.

8) ¿Cuál es la causa más común de la “latencia misteriosa” de NVMe en ZFS?

Procesamiento de interrupciones y completaciones concentrado en muy pocas CPUs, a menudo empeorado por mismatch NUMA. Es vergonzosamente común y solucionable.

9) ¿Debo ajustar parámetros TXG para arreglar stutters?

Sólo después de descartar problemas de IRQ/CPU, presión de memoria y formas patológicas de IO. Los knobs de TXG pueden ayudar en casos límite, pero son fáciles de aplicar mal y difíciles de razonar.

Conclusión: próximos pasos que puedes hacer esta semana

Si quieres menos páginas de pager y mejor p99 en pools ZFS solo NVMe, haz primero el trabajo poco sexy. Verifica enlaces PCIe, evita throttling térmico, distribuye IRQs y alinea NUMA. Luego ajusta propiedades de dataset al workload: recordsize donde importa, atime off donde es ruido y mantén lz4 salvo que hayas demostrado que la CPU es el cuello de botella.

Próximos pasos:

  1. Captura una línea base durante el pico: iostat -x, zpool iostat -r -w, mpstat y /proc/interrupts.
  2. Arregla cualquier obviedad de IRQ/NUMA que encuentres. Re-mide p99.
  3. Audit datasets: identifica cuáles son de base de datos/VM/log/streaming y ajusta recordsize en consecuencia.
  4. Decide explícitamente si necesitas semánticas estrictas de sync; no cambies la durabilidad “accidentalmente”.
  5. Escribe un pequeño runbook: qué gráficos/comandos revisas primero y cómo se ve “bien” para tu flota.
← Anterior
Respuesta a ransomware en ZFS: el playbook de snapshots que te salva
Siguiente →
Debian/Ubuntu Timeouts aleatorios: Rastrear la ruta de red con mtr y tcpdump (Caso #4)

Deja un comentario