Todo va bien. El QPS es estable. La latencia es aburrida. Entonces llega un despliegue, se vacía una cola o se lanza un trabajo por lotes “solo por hoy”, y el p99 de MySQL se convierte en una historia de terror. Los gráficos no suben de forma educada; se desploman y luego quedan en “timeout”.
Si ejecutas MySQL sobre ZFS, puedes conseguir un rendimiento excelente y una gran seguridad operativa. También puedes construir la máquina perfecta para convertir ráfagas cortas de escritura en colapsos largos de latencia. ZFS es honesto: hará exactamente lo que tú pediste, no lo que querías.
El modelo mental: dónde MySQL se encuentra con ZFS y las cosas se complican
Empieza con la verdad incómoda: MySQL se preocupa más por la consistencia de la latencia que por el rendimiento pico. Tu aplicación no llama a nadie porque perdiste un 10% de throughput. Te llama porque el p99 subió de 5 ms a 800 ms y las conexiones empezaron a expirar.
ZFS no es una capa fina sobre los discos. Es un sistema de archivos transaccional copy-on-write con su propio cache (ARC), recolección de escrituras, verificación por checksums y un mecanismo de log de intenciones separado (ZIL) para semánticas síncronas. Es brillante para hacer que las escrituras sean seguras y verificables. También es capaz de convertir un flujo suave de pequeñas escrituras síncronas en un problema de colas, especialmente cuando el pool está ocupado, fragmentado o mal aprovisionado para latencia.
De dónde vienen las escrituras en MySQL
InnoDB realiza varios tipos de escrituras. Bajo cargas con ráfagas, las que más perjudican son:
- Escrituras de redo log (secuenciales, a menudo sincronizadas frecuentemente dependiendo de
innodb_flush_log_at_trx_commit). - Escrituras del doublewrite buffer (amplificación de escritura por diseño, para seguridad ante fallos).
- Flushes de páginas de datos (en segundo plano, pero ocurren ráfagas cuando la presión de páginas sucias se acumula).
- Escrituras de binlog (pueden fsyncarse, especialmente en configuraciones de replicación/GTID).
- Tablas temporales / archivos de ordenación (si están mal configurados, pueden generar mucha actividad en disco durante picos).
Dónde coloca ZFS esas escrituras
ZFS escribe en grupos de transacciones (TXGs). Los datos se acumulan en memoria y luego se confirman periódicamente (comúnmente cada pocos segundos). Ese alisado es genial hasta que deja de serlo —porque cuando ocurre un commit, ZFS tiene que volcar muchos datos y metadatos, y puede tener que hacerlo mientras tu base de datos también solicita garantías de durabilidad síncronas.
Síncrono es la palabra clave. Cuando MySQL hace un fsync() o abre archivos con O_SYNC, está exigiendo que los datos estén en almacenamiento estable antes de continuar. En ZFS, las escrituras síncronas se manejan a través del ZIL (ZFS Intent Log). Si añades un dispositivo de log separado (un SLOG), cambias dónde aterrizan esas escrituras de log síncronas.
ZIL no es una “caché de escritura” para todo el pool. Es un mecanismo para satisfacer semánticas sync de forma segura. La mayoría de las veces, las entradas del ZIL solo se reproducen tras un fallo; de otro modo se “quemarán” cuando el siguiente TXG se confirme. Para la latencia de MySQL, la ruta ZIL es donde los segundos se convierten en milisegundos —o los milisegundos en segundos.
Consejo con opinión: si ejecutas MySQL con configuraciones de durabilidad reales (y deberías), estás aceptando escrituras síncronas. Trata ZIL/SLOG y la latencia como ciudadanos de primera clase, no como ocurrencias tardías.
Idea parafraseada (atribuida): Werner Vogels ha impulsado la idea de que debes diseñar para la falla como condición normal, no como excepción.
Hechos y contexto histórico que explican los modos de fallo actuales
- ZFS nació en Sun como un sistema de archivos de integridad de datos de extremo a extremo: checksums en todo, autocuración con redundancia. Ese ADN de “integridad primero” afecta los compromisos de rendimiento hoy.
- Copy-on-write no es opcional en ZFS. Las sobrescrituras se convierten en asignar-nuevo-y-actualizar-metadatos. Genial para snapshots; potencialmente duro para bases de datos con muchas escrituras aleatorias cuando el espacio es escaso.
- El ZIL existe porque POSIX exige semánticas síncronas. No es una característica especial para bases de datos; es la plomería para el comportamiento correcto ante
fsync(). - SLOG es un dispositivo, no un modo. La gente habla de “activar SLOG”; en realidad estás añadiendo un vdev de log separado para almacenar registros ZIL más rápido y predeciblemente.
- ARC (Adaptive Replacement Cache) fue diseñado para superar al clásico LRU equilibrando recencia y frecuencia. Puede hacer que las lecturas parezcan mágicas —hasta que roba demasiada memoria al buffer pool de InnoDB y al SO.
- L2ARC llegó después para extender ARC a dispositivos rápidos. Ayuda lecturas, pero también cuesta memoria y ancho de banda de escritura para mantenerse, lo cual no es gratis durante ráfagas de escritura.
- El doublewrite buffer de MySQL es una respuesta a escrituras de página parcial tras un fallo. En sistemas de archivos con garantías de atomicidad a nivel de página, es redundante; en la mayoría, es protector. En ZFS, a menudo sigue ayudando la seguridad operativa, pero es IO extra.
- Las propiedades del dataset evolucionaron como rieles de seguridad porque los administradores seguían cometiendo errores. Cosas como
atime=off,compressionyrecordsizeexisten porque el comportamiento por defecto del sistema de archivos no es “inteligente para bases de datos”. - Los SSD modernos añadieron sus propias sorpresas de latencia: agotamiento de caché SLC, recolección de basura del firmware y amplificación de escritura variable. Tu SLOG “rápido” puede convertirse en calabaza bajo escrituras síncronas sostenidas.
Cómo las ráfagas de escritura se convierten en colapsos de latencia en ZFS
1) Amplificación de escrituras síncronas: tormentas de fsync contra IOPS limitados
Cuando MySQL está configurado para durabilidad —por ejemplo, innodb_flush_log_at_trx_commit=1 y binlog sync activado— cada commit puede requerir un fsync(). El group commit ayuda, pero bajo tráfico con ráfagas aún puedes ver una estampida de solicitudes síncronas.
Si ZFS tiene que colocar esas escrituras síncronas en el pool principal, tu latencia se convierte en la latencia del pool. Y el pool también está ocupado haciendo commits de TXG, actualizaciones de metadatos y quizás resilver/scrub. Así obtienes un sistema donde el throughput parece “bien” pero cada transacción espera en fila por almacenamiento durable.
2) Presión de commit de TXG: escrituras “suaves” que se vuelven dolor periódicamente
ZFS acumula datos sucios en memoria y los vacía en TXGs. Bajo ráfagas de escritura, puedes alcanzar límites de datos sucios, y ZFS empezará a desacelerar las escrituras entrantes para evitar uso descontrolado de memoria. Este comportamiento es correcto. También es el momento en que los hilos de tu base de datos dejan de hacer trabajo útil y empiezan a esperar al almacenamiento.
Cuando un flush de TXG es grande, compite con IO síncrono. Incluso con un SLOG, el pool todavía tiene que hacer el trabajo real de escribir datos y metadatos. El SLOG te ayuda a reconocer escrituras síncronas rápidamente, pero aún puedes colapsar más tarde si el pool no puede seguir el ritmo y el ZIL empieza a llenarse y esperar commits.
3) Espacio y fragmentación: el multiplicador de latencia silencioso
ZFS quiere espacio libre. No “algo de espacio libre”. Espacio libre real. A medida que los pools se llenan, la asignación se vuelve más difícil. Los bloques se fragmentan más. Las actualizaciones de metadatos se dispersan. Cada escritura comienza a parecerse a un problema de IO aleatorio pequeño.
Las bases de datos son excelentes en convertir espacio libre en “no libre”. Si tu pool ronda el 80–90% usado, estás poniéndolo a prueba de las algoritmos de asignación de ZFS durante el tráfico pico. No hagas eso en producción a menos que disfrutes escuchar el tono de tu paginador a las 3 a.m.
4) recordsize mal ajustado y demasiado trabajo de metadatos
Las páginas de InnoDB suelen ser de 16K. recordsize de ZFS tiene por defecto 128K para cargas generalistas. Si lo dejas en 128K para un dataset que almacena tablespaces de InnoDB, puedes aumentar la amplificación de escritura: cambiar 16K puede requerir reescribir bloques más grandes dependiendo de patrones de acceso y compresión.
No siempre es catastrófico —ZFS tiene cierta inteligencia y el IO secuencial aún puede rendir bien— pero bajo actualizaciones aleatorias y ráfagas de escritura, un recordsize equivocado hace que tu pool trabaje más para el mismo trabajo de base de datos.
5) El mito del SLOG: “añade un SSD y toda la latencia síncrona desaparece”
Un SLOG solo es tan bueno como su latencia de escritura durable. Los SSD de consumo pueden ser rápidos hasta que golpean un acantilado de escritura. También pueden mentir sobre el comportamiento de flush. Para un SLOG, quieres latencia predecible bajo escrituras sostenidas y protección contra pérdida de energía (o garantías empresariales equivalentes).
Broma #1: Comprar un SSD barato para SLOG es como contratar a un becario para sostener los cimientos del edificio: entusiasta, pero el departamento de física tendrá preguntas.
6) ARC vs buffer pool de InnoDB: la memoria es un campo de batalla compartido
ARC es agresivo y eficaz. El buffer pool de InnoDB también lo es. Si dejas que ambos luchen por la RAM, el kernel terminará eligiendo un vencedor, y no será tu uptime. Verás swapping, tormentas de reclaim y amplificación de IO cuando las páginas cacheadas churneen.
Para MySQL, suele ser mejor dimensionar intencionalmente el buffer pool de InnoDB y limitar ARC para que ZFS no consuma el resto. ZFS puede sobrevivir con un ARC más pequeño; MySQL sufriendo lecturas aleatorias no puede.
Guión de diagnóstico rápido (primero/segundo/tercero)
Este es el flujo “tienes cinco minutos antes de que la dirección entre al canal de incidentes”. No es elegante. Es efectivo.
Primero: confirma que el síntoma es latencia de almacenamiento, no CPU o bloqueos
- Revisa MySQL por esperas de locks y presión de flush: si los hilos están bloqueados en mutex/locks, el tuning de almacenamiento no ayudará.
- Revisa la carga del SO y el IO wait: un alto
%way carga creciente con CPU baja apunta a cola de IO. - Revisa la latencia IO del pool ZFS y la profundidad de cola: identifica si el cuello de botella es el pool o el SLOG.
Segundo: decide si las escrituras síncronas son el cuello de botella
- Mira tasas altas de escrituras síncronas (redo/binlog) y comportamiento de
fsync(). - Comprueba si tienes SLOG y si está saturado o lento.
- Confirma la propiedad
syncdel dataset (generalmente debe serstandardpara durabilidad; no “arregles” incidentes mintiendo).
Tercero: revisa la salud del pool y factores de “quemado lento”
- Ocupación del pool: si estás por encima del ~80% usado, has encontrado un contribuyente.
- Fragmentación: alta fragmentación se correlaciona con dolor por escrituras aleatorias.
- Scrub/resilver: si está en curso, puede convertir un pico manejable en un colapso.
- Tensión por datos sucios: ZFS puede estar desacelerándote intencionalmente para sobrevivir.
Sesgo de decisión: en un incidente por ráfagas de escritura, los tres principales culpables son (1) la ruta de escritura síncrona, (2) la saturación/latencia del pool, (3) la presión de memoria que causa churn en caches. No empieces cambiando recordsize a mitad del incidente a menos que te guste apostar con tus datos.
Tareas prácticas: comandos, salidas y decisiones (12+)
Estos son movimientos operativos reales: ejecuta un comando, interpreta la salida, decide qué hacer a continuación. Asume un host Linux con OpenZFS, pool tank, dataset tank/mysql y datos de MySQL en /var/lib/mysql.
Tarea 1: Ver si el pool está obviamente sobrecargado ahora mismo
cr0x@server:~$ zpool iostat -v tank 1 5
capacity operations bandwidth
pool alloc free read write read write
---------- ----- ----- ----- ----- ----- -----
tank 6.20T 1.10T 120 3800 18M 420M
raidz2 6.20T 1.10T 120 3700 18M 410M
sda - - 15 520 2.2M 58M
sdb - - 14 515 2.1M 57M
sdc - - 15 540 2.2M 60M
sdd - - 15 530 2.2M 59M
logs - - - 100 - 12M
nvme0n1 - - - 100 - 12M
Qué significa: las escrituras son intensas (3800 ops/s). El ancho de banda del pool es 420 MB/s con lecturas relativamente bajas. Si la latencia es alta, necesitas el siguiente comando: latencia IO, no solo ops.
Decisión: si las escrituras están disparándose y el pool está casi lleno, planifica reducir la carga de escrituras (restringir trabajos por lotes) e investiga sincronía y throttling de datos sucios inmediatamente.
Tarea 2: Revisar la latencia por vdev para encontrar el punto de bloqueo
cr0x@server:~$ zpool iostat -v -l tank 1 5
operations bandwidth total_wait disk_wait
pool read write read write read write read write
-------------------------- ----- ----- --------- --------- --------- --------- --------- ---------
tank 90 4200 12M 480M 3ms 120ms 1ms 110ms
raidz2 90 4100 12M 470M 3ms 125ms 1ms 115ms
sda 9 580 1.2M 68M 2ms 140ms 1ms 130ms
sdb 9 570 1.2M 67M 2ms 138ms 1ms 128ms
sdc 9 600 1.3M 69M 2ms 142ms 1ms 132ms
sdd 9 590 1.3M 69M 2ms 141ms 1ms 131ms
logs 0 120 0 14M 0ms 2ms 0ms 1ms
nvme0n1 0 120 0 14M 0ms 2ms 0ms 1ms
Qué significa: total_wait de escritura del pool es ~120 ms. El SLOG está bien (2 ms). El cuello de botella es el trabajo de flush/commit del pool principal, no el dispositivo de log.
Decisión: céntrate en la capacidad de escritura del pool, fragmentación, espacio libre y throttling de datos sucios. Un SLOG más rápido no solucionará este bloqueo específico.
Tarea 3: Comprobar ocupación y fragmentación del pool (predecir problemas)
cr0x@server:~$ zpool list -o name,size,alloc,free,capacity,frag,health tank
NAME SIZE ALLOC FREE CAPACITY FRAG HEALTH
tank 7.28T 6.20T 1.08T 85% 62% ONLINE
Qué significa: 85% de capacidad y 62% de fragmentación. Eso es un impuesto de latencia, especialmente para escrituras aleatorias y cargas con metadatos intensos.
Decisión: prioriza alivio de capacidad (añadir vdevs o migrar datos). Trata el “espacio libre” como una característica de rendimiento, no como un detalle contable.
Tarea 4: Comprobar si hay un scrub o resilver en curso
cr0x@server:~$ zpool status tank
pool: tank
state: ONLINE
scan: scrub in progress since Mon Dec 23 02:11:44 2025
3.12T scanned at 1.2G/s, 1.55T issued at 600M/s, 6.20T total
0B repaired, 25.00% done, 02:30:12 to go
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
logs
nvme0n1 ONLINE 0 0 0
errors: No known data errors
Qué significa: el scrub está emitiendo IO activamente. En un pool que ya lucha con latencia de escritura, el scrub puede empujarte al colapso.
Decisión: si estás en un incidente, considera pausar el scrub (según la política). Reanuda después de estabilizar la latencia.
Tarea 5: Pausar/reanudar un scrub durante un incidente (si tu política lo permite)
cr0x@server:~$ sudo zpool scrub -p tank
cr0x@server:~$ zpool status tank
pool: tank
state: ONLINE
scan: scrub paused since Mon Dec 23 03:01:02 2025
3.12T scanned at 1.2G/s, 1.55T issued at 600M/s, 6.20T total
0B repaired, 25.00% done, 02:30:12 to go
errors: No known data errors
Qué significa: la presión de IO debería reducirse. Estás intercambiando tiempo para detectar errores latentes por estabilidad en producción.
Decisión: hazlo solo si tu organización acepta el riesgo; documenta la acción y reanuda más tarde.
Tarea 6: Inspeccionar propiedades del dataset que afectan el comportamiento de MySQL
cr0x@server:~$ zfs get -o name,property,value -s local,default recordsize,compression,atime,sync,logbias,primarycache,secondarycache tank/mysql
NAME PROPERTY VALUE
tank/mysql recordsize 128K
tank/mysql compression off
tank/mysql atime on
tank/mysql sync standard
tank/mysql logbias latency
tank/mysql primarycache all
tank/mysql secondarycache all
Qué significa: recordsize es el 128K por defecto, compression está off, atime está on. Para InnoDB, recordsize a menudo debería ser más pequeño; atime normalmente debería estar off; compression frecuentemente es una ganancia en CPUs modernas.
Decisión: planifica cambios deliberadamente (especialmente recordsize). Desactiva atime rápidamente; considera habilitar compression; evalúa recordsize según el contexto de la carga de trabajo.
Tarea 7: Desactivar atime para el dataset de MySQL
cr0x@server:~$ sudo zfs set atime=off tank/mysql
cr0x@server:~$ zfs get -o name,property,value atime tank/mysql
NAME PROPERTY VALUE
tank/mysql atime off
Qué significa: las lecturas no generarán escrituras de metadatos para actualizar tiempos de acceso. Ganancia pequeña, pero constante.
Decisión: haz esto salvo que tengas una carga de auditoría que dependa de atime (raro para directorios de datos MySQL).
Tarea 8: Habilitar compresión (normalmente lz4) para reducir IO durante ráfagas
cr0x@server:~$ sudo zfs set compression=lz4 tank/mysql
cr0x@server:~$ zfs get -o name,property,value compression tank/mysql
NAME PROPERTY VALUE
tank/mysql compression lz4
Qué significa: ZFS comprimirá nuevos bloques. Para bases de datos, esto suele reducir el ancho de banda de escritura y puede mejorar la latencia, salvo que la CPU ya esté saturada.
Decisión: si hay margen de CPU, actívala. Si la CPU está al máximo durante incidentes, prueba primero.
Tarea 9: Comprobar tamaño de ARC y presión (evitar peleas por memoria)
cr0x@server:~$ arcstat 1 3
time read miss miss% dmis dm% pmis pm% mmis mm% arcsz c avail
12:10:01 980 120 10 40 33 60 50 20 17 58G 60G 3G
12:10:02 1020 140 12 55 39 70 50 15 11 59G 60G 2G
12:10:03 1005 160 14 70 44 75 47 15 9 60G 60G 1G
Qué significa: ARC está en su objetivo (c), y la memoria disponible colapsa. Si el host empieza a hacer swapping, MySQL sufrirá gravemente.
Decisión: limita ARC (zfs_arc_max) para que el buffer pool de MySQL tenga RAM estable. No intentes “ganar” dejando que ambas caches crezcan sin control.
Tarea 10: Comprobar swapping real y IO wait a nivel del SO
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
12 3 524288 31200 10240 88400 150 220 120 9800 2100 6400 12 8 62 18 0
10 2 524288 28000 10080 87000 180 260 110 10500 2200 6500 11 9 60 20 0
14 4 524288 25000 9980 86500 210 300 90 12000 2300 6700 10 10 58 22 0
11 3 524288 24000 9950 86000 190 280 100 11000 2250 6600 11 9 59 21 0
13 4 524288 23000 9900 85500 220 310 95 12500 2350 6800 10 10 56 24 0
Qué significa: swap in/out (si/so) está activo y wa es alto. Este es un clásico “la latencia empeoró porque la memoria empeoró”.
Decisión: reduce ARC, disminuye la presión de memoria de MySQL o añade RAM. A corto plazo, detén la fuente de la peor ráfaga de escritura y estabiliza.
Tarea 11: Confirmar los dispositivos ZIL/SLOG y su salud
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
logs
nvme0n1 ONLINE 0 0 0
errors: No known data errors
Qué significa: hay un SLOG (nvme0n1) y está sano.
Decisión: si tienes latencia síncrona pero SLOG está sano y con baja latencia, es probable que el pool principal sea el factor limitante (como en la Tarea 2).
Tarea 12: Verificar que el dataset esté montado donde MySQL realmente guarda datos
cr0x@server:~$ findmnt /var/lib/mysql
TARGET SOURCE FSTYPE OPTIONS
/var/lib/mysql tank/mysql zfs rw,xattr,noacl
Qué significa: MySQL está de hecho en el dataset ZFS que crees. Te sorprendería cuán a menudo no es así.
Decisión: si no lo está, deja de “tunear” el dataset equivocado y corrige el montaje/layout primero.
Tarea 13: Inspeccionar ajustes de durabilidad de MySQL que generan escrituras síncronas
cr0x@server:~$ mysql -e "SHOW VARIABLES WHERE Variable_name IN ('innodb_flush_log_at_trx_commit','sync_binlog','innodb_doublewrite','innodb_flush_method');"
+--------------------------------+------------+
| Variable_name | Value |
+--------------------------------+------------+
| innodb_doublewrite | ON |
| innodb_flush_log_at_trx_commit | 1 |
| innodb_flush_method | O_DIRECT |
| sync_binlog | 1 |
+--------------------------------+------------+
Qué significa: este es modo de durabilidad completa: redo flush en cada commit y binlog sincronizado en cada commit. Excelente para corrección. Más difícil para el almacenamiento durante ráfagas.
Decisión: no cambies esto a la ligera. Si el negocio acepta el riesgo (algunos lo hacen para sistemas caché, no para dinero), considera sync_binlog > 1 o innodb_flush_log_at_trx_commit=2, pero solo con aprobación explícita.
Tarea 14: Comprobar señales actuales de checkpoint/pressión de páginas sucias de MySQL
cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_pages_dirty'; SHOW GLOBAL STATUS LIKE 'Innodb_os_log_fsyncs'; SHOW GLOBAL STATUS LIKE 'Innodb_log_waits';"
+-------------------------------+--------+
| Variable_name | Value |
+-------------------------------+--------+
| Innodb_buffer_pool_pages_dirty| 245000 |
+-------------------------------+--------+
+---------------------+--------+
| Variable_name | Value |
+---------------------+--------+
| Innodb_os_log_fsyncs| 185000 |
+---------------------+--------+
+-----------------+------+
| Variable_name | Value|
+-----------------+------+
| Innodb_log_waits| 420 |
+-----------------+------+
Qué significa: muchas páginas sucias y Innodb_log_waits distinto de cero sugiere un cuello de botella en el flush del log. Bajo ráfagas de escritura, esto es consistente con presión en la ruta síncrona de almacenamiento.
Decisión: correlaciona con la latencia ZFS. Si la espera de escritura de ZFS es alta, arregla la ruta de almacenamiento; si ZFS parece bien, examina la configuración de logs de MySQL y el scheduling de CPU.
Tarea 15: Detectar procesos atascados en IO (rápido y sucio)
cr0x@server:~$ ps -eo pid,comm,state,wchan:30 | egrep 'mysqld|z_wr_iss|z_wr_int|z_wr|txg|sync' | head
2141 mysqld D io_schedule
2147 mysqld D io_schedule
2153 mysqld D io_schedule
1103 z_wr_iss D cv_wait
1104 z_wr_int D cv_wait
Qué significa: hilos de MySQL en estado D esperando en el scheduler de IO indica paradas en el almacenamiento. Hilos escritores de ZFS esperando también pueden indicar throttling interno/presión de commit.
Decisión: valida con zpool iostat -l y estadísticas de disco del SO. Si se confirma, reduce la carga de escritura y arregla los límites del pool.
Tarea 16: Comprobar latencia y saturación por dispositivo desde Linux (complementa la vista de ZFS)
cr0x@server:~$ iostat -x 1 3
avg-cpu: %user %nice %system %iowait %steal %idle
12.10 0.00 8.20 18.40 0.00 61.30
Device r/s w/s rKB/s wKB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
sda 9.0 580.0 1200 68000 228.0 45.0 135.0 3.0 137.0 1.6 99.0
sdb 9.0 570.0 1180 67000 228.0 44.0 132.0 3.0 134.0 1.6 98.5
nvme0n1 0.0 120.0 0 14000 233.0 0.3 2.0 0.0 2.0 0.2 2.4
Qué significa: los HDDs están al límite con ~99% de utilización y ~130 ms de await en escrituras. NVMe (SLOG) está bien. Esto coincide con la historia de ZFS: la latencia de escritura del pool es el problema.
Decisión: necesitas más IOPS (más vdevs, topología distinta), menos presión de escritura aleatoria (tuning, agrupamiento) o más margen (espacio libre).
Perillas de ZFS que importan para MySQL (y cuáles son trampas)
Recordsize: alinea con la carga, no con la ideología
Para InnoDB, la práctica común es recordsize=16K en el dataset que almacena los tablespaces. La razón: InnoDB modifica páginas de 16K, y registros más pequeños pueden reducir la amplificación de escritura y mejorar la latencia bajo actualizaciones aleatorias.
Pero no hagas cargo-cult. Si tu carga consiste mayormente en escaneos secuenciales grandes, backups o análisis que leen rangos grandes, un recordsize mayor puede ayudar. Si almacenas binlogs o backups en el mismo dataset (no lo hagas), tendrás necesidades en conflicto.
Postura práctica:
- Coloca los datos de MySQL en su propio dataset.
- Para OLTP intensivo en InnoDB, empieza con
recordsize=16K. - Para cargas mixtas, prueba 16K vs 32K.
Compresión: lz4 suele ser dinero gratis
La compresión reduce bytes escritos y leídos. Para ráfagas de escritura, eso importa porque buscas pasar menos IO físico por el mismo cuello de botella. En CPUs modernas, lz4 suele ser una ganancia neta.
Existen casos límite: si la CPU ya está saturada durante ráfagas, la compresión puede moverte de bound por IO a bound por CPU. Aún así a menudo es mejor (la saturación de CPU es más fácil de escalar que la latencia de almacenamiento), pero mide.
sync y logbias: sé honesto, pero elige tu veneno
sync=standard es la configuración normal y honesta: respeta las peticiones sync de la aplicación. sync=disabled es mentir a las aplicaciones: reconoce escrituras sync antes de que sean durables. A veces se usa para datos efímeros o caches, pero para bases de datos reales es una pistola para la pérdida de datos.
logbias=latency vs logbias=throughput se malinterpreta frecuentemente. Para MySQL, normalmente te importa la latencia. Si tienes un SLOG, logbias=latency es razonable. Si no tienes SLOG y tu pool es lento, cambiar logbias no creará hardware. Puede, eso sí, cambiar cuánto va a ZIL vs pool principal en ciertos casos.
Broma #2: Poner sync=disabled para “arreglar latencia” es como quitar la alarma de humo porque pita durante incendios.
SLOG: lo que realmente hace y cuándo ayuda
Un SLOG ayuda cuando:
- Tu carga hace muchas escrituras síncronas.
- Tu pool principal tiene peor latencia que un buen SSD/NVMe.
- El dispositivo SLOG tiene protección contra pérdida de energía y latencia de escritura baja y consistente.
Un SLOG no ayuda cuando:
- Estás limitado por commits TXG al pool principal (el pool no puede vaciarse lo suficientemente rápido).
- Tu carga es mayormente escrituras asíncronas (sin presión de fsync).
- Tu “SLOG” es un SSD de consumo que se colapsa bajo escrituras sostenidas o miente sobre flushes.
Consejo operativo: espeja el SLOG si tu perfil de riesgo lo exige. Un dispositivo SLOG fallido puede forzar al pool a un estado inseguro según plataforma y configuración; incluso cuando no lo hace, acabas de crear un disparador de incidentes. Para MySQL crítico, un SLOG en espejo es aburrido y correcto.
Vdev especial: metadata y bloques pequeños pueden ser tu ganancia oculta
Si tu pool usa HDD, considera un vdev especial (en SSD) para metadata y bloques pequeños. Las bases de datos generan churn de metadata: bloques indirectos, estructuras de directorio, mapas de espacio. Poner eso en medio rápido puede reducir latencia y mejorar consistencia.
Pero: el vdev especial no es redundancia opcional. Si muere y no es redundante, puedes perder el pool. Trátalo como un vdev de alto nivel con mirroring adecuado.
Primarycache/secondarycache: deja de duplicar caché hasta el swap
MySQL ya tiene un buffer pool. ZFS tiene ARC y potencialmente L2ARC. Si cacheas todo dos veces, desperdicias RAM y aumentas churn de expulsión bajo ráfagas.
Muchas empresas ponen primarycache=metadata en el dataset de MySQL para evitar que ARC cachee páginas de datos que InnoDB ya cachea, manteniendo la metadata caliente. Esto puede ayudar a estabilizar uso de memoria. No es universal; mide los patrones de lectura.
Ashift y layout de vdev: la regla “no puedes tunear geometría”
Si tu pool se construyó con una alineación de sector incorrecta (ashift demasiado pequeña), puedes sufrir amplificación de escritura para siempre. Si tu layout de vdev fue diseñado para capacidad en lugar de IOPS (por ejemplo, RAIDZ ancho para una base de datos OLTP sensible a latencia), puedes sufrir dolor predecible.
Regla con opinión: para MySQL sensible a latencia, los vdevs en espejo son la elección por defecto a menos que tengas una razón clara para no usarlos. RAIDZ puede funcionar, pero hace que las escrituras aleatorias pequeñas sean más difíciles y el comportamiento de rebuild más complejo.
Perillas de MySQL/InnoDB que interactúan con ZFS
Configuraciones de durabilidad: haz explícito el riesgo
La palanca más grande de latencia por ráfagas en MySQL es con qué frecuencia fuerzas flushes durables:
innodb_flush_log_at_trx_commit=1: redo durable en cada commit. Mejor seguridad, mayor presión de sync.innodb_flush_log_at_trx_commit=2: flush al SO en cada commit, fsync una vez por segundo. Menos presión de sync; riesgo de hasta 1s de transacciones en un crash.sync_binlog=1: fsync del binlog en cada commit. Fuerte corrección de replicación; presión de sync.
Si eliges relajar esto, hazlo como decisión de negocio, no como un arreglo de medianoche. Para muchas compañías, perder hasta un segundo de datos es aceptable para algunos sistemas (staging analytics) y inaceptable para otros (dinero, inventario, auth).
Método de flush: evita el doble buffering
innodb_flush_method=O_DIRECT se usa comúnmente para evitar la cache de páginas del SO para los archivos de datos de InnoDB, reduciendo el doble buffering. En ZFS, la interacción es matizada porque ZFS tiene ARC, no el mismo modelo de page cache, pero O_DIRECT aún se usa con éxito frecuentemente.
Lo que intentas evitar: MySQL escribe datos, el SO los cachea, ZFS los cachea otra vez, la memoria se evapora y luego el kernel empieza a swapear. Eso no es una estrategia de rendimiento; es un grito de ayuda.
Tuning de páginas sucias: las ráfagas se amplifican por el backlog
Cuando el buffer pool acumula demasiadas páginas sucias, MySQL tiene que flushar agresivamente. Eso puede convertir una ráfaga moderada en una estampida de almacenamiento. Ajusta:
innodb_max_dirty_pages_pctyinnodb_max_dirty_pages_pct_lwminnodb_io_capacityyinnodb_io_capacity_max(configura según la capacidad real de almacenamiento)innodb_flush_neighbors(a menudo0en pools SSD; más matizado en HDD)
No pongas innodb_io_capacity a “un número grande” porque compraste discos rápidos. Ponlo según lo que entregue el pool durante mezcla de lectura/escritura bajo carga, no la hoja de especificaciones.
Binlogs y archivos temporales: no coloques tu dolor en el mismo lugar
Pon los binlogs en un dataset afinado para escrituras secuenciales, potencialmente con recordsize mayor, y evita que compitan con los tablespaces si puedes. Lo mismo para tmpdir si haces ordenaciones pesadas. Colocar todo en un solo dataset es cómo creas “latencia misteriosa” durante ráfagas.
Tres microhistorias corporativas desde el terreno
Incidente causado por una suposición errónea: “Añadimos un SLOG, así que la latencia sync está solucionada”
La compañía tenía un primario MySQL en un pool ZFS respaldado por HDDs. Experimentaban picos ocasionales de p99 durante ráfagas de tráfico —principalmente alrededor de envíos de marketing y trabajos de fin de mes. Alguien hizo lo correcto a medias: añadieron un NVMe rápido como SLOG. La latencia mejoró en estado estable, así que el cambio se etiquetó como “arreglado”.
Meses después, llegó una ráfaga mayor. El p99 se disparó de nuevo, pero la persona de guardia estaba segura de que no podía ser almacenamiento porque “tenemos SLOG”. Persiguieron fantasmas en planes de consulta y pools de conexiones mientras el pool estaba a alta utilización. Los usuarios seguían reintentando, lo que generó más escrituras, más latencia —fallo autoalimentado clásico.
Cuando finalmente miraron zpool iostat -l, la historia fue directa: las escrituras del SLOG eran de baja latencia, pero la espera de escritura del pool principal era enorme. Los commits de TXG eran el cuello de botella. El ZIL podía reconocer escrituras rápido, pero no podía hacer que el pool vaciara los datos sucios más rápido.
La solución no fue otro dispositivo de log. Fue aburrida: añadir más vdevs para aumentar IOPS, reducir la ocupación del pool y separar cargas para que binlogs y archivos temporales no compitieran con los tablespaces. La suposición equivocada fue pensar que latencia síncrona = latencia SLOG; en realidad, la durabilidad síncrona sigue dependiendo de que el pool se mantenga por delante del trabajo sucio acumulado.
Una optimización que salió mal: “Subamos recordsize para throughput”
Otro equipo tenía un pool mayormente SSD y quería mejor rendimiento en cargas masivas. Cambiaron recordsize del dataset MySQL a 1M porque vieron que se recomendaba para cargas secuenciales grandes. Las cargas masivas fueron más rápidas. Declararon victoria y siguieron.
Luego la latencia OLTP empezó a oscilar. No siempre, pero durante ráfagas. Pequeñas actualizaciones a filas calientes empezaron a causar IO desproporcionado. ZFS reescribía registros grandes con más frecuencia y el churn de metadatos aumentó. El pool podía manejar el ancho de banda, pero la distribución de latencia empeoró. Al negocio no le importaba que las cargas nocturnas terminaran antes; le importaba que el checkout a veces tardara 900 ms.
Lo peor fue lo operativo: los snapshots crecieron y la replicación tardó más porque los cambios afectaban bloques más grandes. Nadie quiso empeorar el RTO, pero lo hicieron.
La solución fue segmentar: datasets separados para tablas cargadas en bloque con recordsize mayor, mantener los tablespaces OLTP en 16K o 32K y dejar de tratar “MySQL” como un patrón único de IO. El fallo no fue que recordsize grande sea siempre incorrecto; fue aplicar una sola perilla de tuning a cargas en conflicto.
Una práctica aburrida pero correcta que salvó el día: “Margen y throttling escalonado”
Una organización ejecutaba MySQL en ZFS para un sistema interno crítico. Nada espectacular: flujos de trabajo empresariales, algunas ráfagas durante el día laboral, backfills ocasionales. Tenían una regla estricta: el pool nunca supera un umbral conservador de ocupación, y cada trabajo intensivo en escritura tiene un control de throttling conectado al scheduler.
Cuando un bug hizo que un servicio upstream reintentara agresivamente, el tráfico de escritura se triplicó en minutos. Era el tipo de incidente que normalmente se convierte en un colapso de varias horas. Pero esta vez la base de datos se volvió lenta, no murió. La latencia subió, pero no se convirtió en timeouts.
¿Por qué? Dos motivos. Primero, había espacio libre y baja fragmentación, así que la asignación no degradó bajo presión. Segundo, la persona de guardia pudo reducir rápidamente el backfill y la cola de batch sin cambiar configuraciones de durabilidad. El sistema tenía salidas de emergencia que no implicaban mentir al sistema de archivos.
Después, el postmortem fue casi aburrido. Ese era el objetivo. La “práctica aburrida” fue margen de capacidad más controles operativos para ráfagas de escritura. No un magic sysctl. No una reconstrucción heroica. Solo planificación disciplinada y la capacidad de reducir intencionalmente la carga de escritura.
Errores comunes: síntoma → causa raíz → solución
1) Picos p99 durante ráfagas, SLOG parece estar bien
Síntoma: zpool iostat -l muestra baja latencia de log pero alta espera de escritura del pool; MySQL muestra log waits y timeouts.
Causa raíz: el pool principal no puede vaciar TXGs lo suficientemente rápido; el pool está saturado, fragmentado o con pocos vdevs para IOPS.
Solución: añade vdevs o cambia a topología mirror, reduce la ocupación del pool, reduce cargas competidoras, considera un special vdev para metadata, ajusta comportamiento de páginas sucias en MySQL.
2) La latencia empeora de repente después de que el pool cruza ~80% usado
Síntoma: sin cambios de configuración, las escrituras se vuelven más lentas con el tiempo; la fragmentación sube.
Causa raíz: penalizaciones de asignación y fragmentación en un sistema copy-on-write casi lleno; escrituras más dispersas y mayor overhead de metadatos.
Solución: añade capacidad (preferiblemente añadiendo vdevs, no reemplazando discos uno a uno), reequilibra migrando datasets, aplica SLOs de espacio libre.
3) “Arreglado” poniendo sync=disabled, luego un crash causa pérdida de datos
Síntoma: la latencia mejora hasta un reinicio inesperado; tras el arranque, tablas MySQL están corruptas o faltan commits recientes.
Causa raíz: se desactivaron semánticas síncronas; las aplicaciones recibieron ack antes de que los datos fueran durables.
Solución: pon sync=standard (o always si es necesario), usa un SLOG apropiado y arregla el verdadero cuello de botella de rendimiento.
4) El swapping comienza durante ráfagas y nunca se recupera completamente
Síntoma: vmstat muestra actividad de swap; MySQL se queda bloqueado incluso después de que la ráfaga termine; ARC sigue grande.
Causa raíz: ARC y buffer pool de MySQL compiten; la presión de memoria desencadena reclaim y swap, incrementando IO y latencia.
Solución: capea ARC, dimensiona correctamente el buffer pool de MySQL, evita L2ARC salvo que puedas asumir el overhead de memoria, añade RAM si hace falta.
5) Las escrituras son rápidas hasta que corre un scrub/resilver, entonces p99 explota
Síntoma: correlacionado con zpool status mostrando actividad de scan.
Causa raíz: IO de mantenimiento compite con escrituras de producción; el pool no tiene margen de IOPS.
Solución: programa scrubs fuera de horas pico, asegura margen de IOPS en el pool, usa controles de prioridad/IO donde existan, pausa scrubs durante incidentes según política.
6) Después de “añadir caché SSD,” el rendimiento empeoró bajo escrituras
Síntoma: L2ARC habilitado; bajo ráfagas, la latencia de escritura aumenta; uso de memoria sube.
Causa raíz: los costes de mantenimiento de L2ARC (metadata, escrituras hacia L2ARC) aumentan la presión; el overhead de memoria reduce el margen.
Solución: deshabilita L2ARC para sistemas OLTP con muchas escrituras salvo que tengas un problema probado de misses de lectura y RAM suficiente.
7) Paradas aleatorias cada pocos segundos como un reloj
Síntoma: picos periódicos de latencia alineados con los intervalos de commit de TXG.
Causa raíz: flushes de TXG causan ráfagas que generan colas; el pool no puede sostener el trabajo de flush de forma suave.
Solución: aumenta la capacidad de escritura del pool, reduce la producción de datos sucios (tuning de MySQL), busca trabajos en background que causen ráfagas, asegúrate de que SLOG no sea la única “solución”.
Listas de verificación / plan paso a paso
Paso a paso: construir (o reconstruir) ZFS específicamente para resiliencia frente a ráfagas de MySQL
- Elige un layout de vdev para IOPS primero: vdevs en espejo como base para OLTP. La capacidad viene con más mirrors, no con RAIDZ más ancho.
- Mantén el pool bajo un SLO de capacidad: establece una regla interna (por ejemplo, alertar al 70%, actuar al 80%). El número exacto depende de la carga, pero “llevarlo al 95%” no es ingeniería seria.
- Usa un SLOG real si necesitas rendimiento sync: baja latencia, protección contra pérdida de energía y preferiblemente en espejo.
- Considera un vdev especial en pools HDD: en espejo, dimensionado para metadata y bloques pequeños.
- Crea datasets separados:
tank/mysqlpara tablespaces InnoDBtank/mysql-binlogpara binlogstank/mysql-tmppara tmpdir si hace falta
- Define propiedades del dataset intencionalmente:
atime=offcompression=lz4recordsize=16K(punto de partida para OLTP tablespaces)logbias=latency(típico para datasets DB)
- Decide la política de caching: a menudo
primarycache=metadataen tablespaces, mantener el default en binlogs si los lees frecuentemente para replicación/backup. - Limita ARC según RAM total y buffer pool de MySQL: deja margen para el SO, tablas de páginas, conexiones y absorción de ráfagas.
- Prueba cargas de ráfagas: no solo carga media. Simula un patrón de ráfagas y mira p99, no solo throughput.
Checklist operativo: cuando despliegas un cambio intensivo en escrituras
- Verifica capacidad y fragmentación del pool. Si ya estás cerca del umbral, retrasa el trabajo intensivo o añade capacidad primero.
- Confirma que scrub/resilver no esté programado para solaparse.
- Asegura que los trabajos por lotes tienen throttles y pueden pausarse sin cambios de código.
- Baselínea
zpool iostat -ly métricas de fsync de MySQL antes del cambio. - Alerta sobre latencia p99,
Innodb_log_waitsy espera de escritura ZFS juntos. Las alertas de métrica única mienten.
Checklist de incidente: las reglas “no lo empeores”
- No pongas
sync=disableden el dataset de datos como mitigación de incidente a menos que aceptes explícitamente pérdida de datos. - No cambies recordsize a mitad del incidente esperando alivio instantáneo; afecta a escrituras nuevas y la causa raíz suele estar en otro sitio.
- Pausa scrubs/resilvers si tu política lo permite y el pool se está derritiendo.
- Reduce la carga de escritura: limita trabajos, reduce reintentos, descarta escrituras no críticas y detén backfills.
- Captura evidencia:
zpool iostat -l,iostat -x, contadores de estado MySQL. Tu yo futuro querrá recibos.
Preguntas frecuentes (FAQ)
1) ¿Debería ejecutar MySQL en ZFS?
Sí, si quieres garantías fuertes de integridad, snapshots y administración sensata. Pero debes diseñar para la latencia: layout de vdev adecuado, margen y un plan para escrituras síncronas.
2) ¿Es obligatorio un SLOG para MySQL?
No siempre. Si tu carga es mayormente escrituras asíncronas o aceptas durabilidad relajada, puede que no lo necesites. Si ejecutas con innodb_flush_log_at_trx_commit=1 y sync_binlog=1 bajo tráfico con ráfagas, un buen SLOG suele marcar la diferencia entre “bien” e “incidente”.
3) ¿Puedo arreglar la latencia por ráfagas poniendo sync=disabled?
Puedes reducir la latencia y también la verdad. Reconoce escrituras sync antes de que sean durables, lo que puede perder transacciones comprometidas en un crash. Úsalo solo para datos no críticos y reconstruibles.
4) ¿Qué recordsize debería usar para InnoDB?
Punto de partida común: recordsize=16K para el dataset de tablespaces. Prueba 16K vs 32K si tienes patrones mixtos. Mantén datasets separados para diferentes patrones de IO (binlogs, backups).
5) ¿La compresión ZFS ayuda a las bases de datos?
A menudo sí. lz4 puede reducir escrituras y lecturas físicas, lo que ayuda en ráfagas y reduce desgaste. Valida el margen de CPU y mide la latencia p99, no solo throughput.
6) ¿Debo habilitar L2ARC para MySQL?
Normalmente no para OLTP con muchas escrituras. L2ARC tiene overhead de memoria y costes de mantenimiento de escritura. Si tienes un problema probado de misses de lectura y mucha RAM, puede ayudar, pero no es un movimiento por defecto.
7) ¿Por qué el rendimiento empeora a medida que el pool se llena?
La asignación en copy-on-write se vuelve más difícil con menos espacio libre; la fragmentación y el overhead de metadata aumentan. Tu “escritura simple” se convierte en IO aleatorio y más contabilidad. Mantén margen libre.
8) ¿RAIDZ o mirrors para MySQL?
Para OLTP sensible a latencia, los mirrors son la opción segura por defecto porque proporcionan más IOPS y latencia más predecible. RAIDZ puede funcionar, pero es más fácil toparse con acantilados de latencia bajo escrituras aleatorias pequeñas y alta utilización.
9) ¿Cómo sé si el cuello de botella es escrituras síncronas o flushes en segundo plano?
Correlaciona Innodb_log_waits y contadores de fsync de MySQL con zpool iostat -l de ZFS (tiempos de espera de log y del pool). Si la latencia de log es alta, mira SLOG y la ruta sync. Si la espera del pool es alta, mira presión de flush TXG, ocupación del pool y layout de vdevs.
10) ¿Pueden ayudar los vdevs especiales a la latencia de MySQL?
Sí, especialmente en pools HDD, acelerando metadata y IO de bloques pequeños. Hazlos en espejo y trátalos como críticos; perder un vdev especial puede significar perder el pool.
Conclusión: pasos siguientes que realmente reducen el riesgo
Si ejecutas MySQL sobre ZFS y temes las ráfagas de escritura, no necesitas misticismo. Necesitas tres cosas: un modelo mental correcto, elecciones de durabilidad honestas y suficientes IOPS/margen para que ZFS haga su trabajo sin estrangular la base de datos.
Pasos prácticos siguientes:
- Ejecuta el guión de diagnóstico rápido en un día tranquilo. Captura baseline de
zpool iostat -l,iostat -xy contadores clave de MySQL. - Audita ocupación y fragmentación del pool. Si estás por encima del umbral seguro, trata la capacidad como una reparación urgente de rendimiento.
- Confirma tu ruta de escrituras síncronas: el dataset
synces honesto, el SLOG (si existe) es de grado empresarial y sano, y entiendes si el flush del pool es el verdadero cuello de botella. - Separa datasets por patrón de IO y define propiedades intencionalmente (atime off, lz4 on, recordsize apropiado).
- Limita ARC para que la memoria no sea el disparador oculto de incidentes de almacenamiento.
- Construye throttles operativos para trabajos intensivos en escritura. Tu mejor arreglo de latencia durante una ráfaga a menudo es “detener las escrituras extra”, no “cambiar el sistema de archivos en plena operación”.
Haz esto y la próxima ráfaga de escritura será una desaceleración controlada, no un colapso a medianoche. La meta no es una recuperación heroica. Son gráficos aburridos.