ZFS para PostgreSQL: la estrategia de datasets y sincronización que funciona

¿Te fue útil?

PostgreSQL es honesto: cuando va lento, normalmente te dice por qué. ZFS también es honesto —solo que no siempre en el mismo dialecto.
Juntos pueden producir una base de datos aburridamente fiable… o un sistema que luce bien en los paneles hasta que deja de hacerlo.

El punto doloroso es siempre el mismo: escrituras síncronas. Durabilidad del WAL. Picos de latencia que aparecen solo bajo carga con muchos commits.
Alguien sugiere “simplemente pon sync=disabled” y de pronto tienes una reunión no planificada sobre la “postura de integridad de datos”.

La estrategia: qué hacer y qué no hacer

Si quieres que PostgreSQL sobre ZFS funcione en producción, deja de pensar en consejos genéricos para sistemas de ficheros y empieza a pensar
en límites de durabilidad. El límite de durabilidad de PostgreSQL es WAL + comportamiento de fsync (más checkpoints y replicación).
El límite de durabilidad de ZFS es el ZIL (y el SLOG si existe), los grupos de transacción y dónde aterrizan las escrituras síncronas.

La estrategia de datasets y sincronización que funciona no es exótica:

  • Mantén datos de PostgreSQL y WAL en datasets separados (e idealmente en vdevs separados si puedes).
  • Deja activadas las funciones de seguridad de PostgreSQL: fsync=on, full_page_writes=on y elecciones sensatas de synchronous_commit.
  • Usa sync=standard de ZFS para la mayoría de cosas. Respeta la semántica POSIX de sync; Postgres la usa por una razón.
  • Si la latencia de commit es el cuello de botella, arregla correctamente la ruta de sync: proporciona un dispositivo SLOG adecuado (con protección ante pérdida de energía) o cambia la necesidad de durabilidad de la aplicación de forma intencional (por ejemplo, synchronous_commit=off para cargas específicas).
  • Usa compresión. Normalmente ayuda la latencia y el rendimiento en páginas de base de datos; es uno de los raros “almuerzos gratis” que realmente rinde en producción.
  • No pongas sync=disabled en el dataset de la base de datos a menos que hayas documentado el alcance del fallo y tengas aprobación explícita de quienes serán alertados a las 3 a.m.

La manera más fácil de perder tu trabajo es “optimizar la durabilidad” sin avisar a nadie. La segunda más fácil es comprar dispositivos rápidos
y luego hacer que ZFS espere en I/O de sync lento de todos modos.

Chiste #1: Si pones sync=disabled en una base de datos y lo llamas “eventualmente durable”, has inventado una nueva capa de almacenamiento: esperanza.

Hechos y contexto que cambian decisiones

Esto no es trivia por el gusto de la trivia. Cada punto debería empujar una elección de diseño real.

  1. ZFS es copy-on-write (desde el trabajo original de Sun Microsystems). PostgreSQL ya espera sobrescribir páginas de 8 KB; ZFS convierte eso en nuevas asignaciones de bloques. Eso afecta la amplificación de escritura y la fragmentación.
  2. El ZIL siempre está presente, incluso sin un SLOG. Un SLOG no es “activar el ZIL”, es mover el dispositivo de registro a algo más rápido y seguro para escrituras síncronas.
  3. El tamaño de página por defecto de PostgreSQL es 8 KB. El recordsize por defecto de ZFS suele ser 128 KB. Ese desajuste no es inherentemente incorrecto, pero determina comportamiento de lectura-modificación-escritura en actualizaciones y cuánto dato se ensucia por escritura.
  4. Históricamente, muchas afirmaciones de “ZFS es lento para bases de datos” provinieron de pools mal configurados: ashift incorrecto en discos con sectores de 4K, sin SLOG para cargas con muchas sync, o comportamiento de ARC sin suficiente RAM.
  5. El WAL de PostgreSQL es algo secuencial pero no puramente secuencial. Es intensivo en append, pero el comportamiento de fsync puede crear ráfagas, y el archivado/replicación puede generar presión de lectura en momentos incómodos.
  6. ZFS comprueba suma cada bloque. Eso significa que puedes detectar corrupción silenciosa que otras pilas ignorarían hasta que la base de datos “misteriosamente” falle después. Para bases de datos, esto no es un lujo opcional; es integridad.
  7. La compresión se volvió “segura por defecto” en muchas implementaciones de ZFS solo después de que LZ4 maduró (y los CPUs se hicieron más rápidos). Hoy, dejar la compresión apagada suele ser dejar rendimiento en la mesa.
  8. La durabilidad de PostgreSQL no es un interruptor binario. Puedes elegir durabilidad a nivel de transacción (synchronous_commit) o a nivel de replicación (replicación síncrona), en lugar de sabotear el sistema de ficheros.

Diseño de datasets que resiste cargas reales

El error más común es poner todo bajo un mismo dataset, tunearlo una vez y asumir que ya está.
PostgreSQL tiene al menos tres personalidades de I/O: archivos de datos, WAL y “cosas importantes pero que no justifican optimizar a costa de durabilidad”
(logs, dumps, staging de backups base, etc.).

Un árbol de datasets pragmático

Aquí hay un diseño que mapea cómo se comporta Postgres y cómo ZFS puede ser afinado sin sabotearse:

  • tank/pg: dataset padre para todo el almacenamiento de PostgreSQL.
  • tank/pg/data: directorio de datos principal (PGDATA) excluyendo WAL si lo reubicas.
  • tank/pg/wal: directorio WAL (enlaza simbólicamente pg_wal aquí o usa postgresql.conf donde sea soportado).
  • tank/pg/tmp: tablas temporales / ordenaciones si colocas temp_tablespaces aquí (opcional; a menudo mejor en NVMe local y aceptable perderlo).
  • tank/pg/backups: backups y staging de basebackup (objetivos de snapshot, spool de archivo WAL, etc.).

¿Por qué separar datasets? Porque recordsize, logbias, compression y a veces primarycache requieren decisiones distintas.
WAL quiere baja latencia y comportamiento de sync predecible. Los archivos de datos quieren eficiencia de lectura, cacheo y control sensato de fragmentación.

Dónde colocar físicamente el WAL

Si puedes permitírtelo, el WAL merece almacenamiento rápido y una ruta de sync limpia. Opciones:

  • Mismo pool, dataset separado: lo más sencillo, sigue permitiéndote afinar propiedades; el rendimiento depende de la disposición de vdevs.
  • vdevs mirror separados para WAL: bueno cuando el pool principal es RAIDZ y la latencia de WAL sufre.
  • pool separado para WAL: a veces se hace, pero es más pesado operativamente (más cosas que monitorizar, más dominios de fallo).

No muevas el WAL a almacenamiento “rápido pero dudoso” sin protección ante pérdida de energía si dependes de durabilidad síncrona. El WAL es el diario de lo que pasó; no escribas tu diario con tinta que desaparece.

Semántica de sync: PostgreSQL vs ZFS (y dónde encaja el SLOG)

PostgreSQL escribe registros WAL y usa fsync() (o fdatasync()) para asegurar durabilidad cuando una transacción hace commit, según la configuración.
Cuando Postgres dice “sync”, significa: “Si perdemos la energía ahora mismo, la transacción comprometida seguirá comprometida después de la recuperación por crash.”

ZFS con sync=standard respeta las peticiones síncronas usando el ZIL. El ZIL no es una caché de escritura para todo; es un registro de escrituras síncronas para poder reproducirlas tras un crash. Sin un dispositivo SLOG dedicado, el ZIL vive en el pool principal, lo que significa que las escrituras síncronas aterrizan en tus discos de datos. Con un SLOG, aterrizan en el SLOG (aun así escritas de forma segura) y luego se comitean al pool principal con el comportamiento normal de los grupos de transacción.

Qué significa realmente sync en un dataset

  • sync=standard: respetar las solicitudes sync. Este es el valor por defecto que quieres para bases de datos.
  • sync=always: tratar todas las escrituras como síncronas, incluso si la aplicación no lo pidió. Por lo general peor para Postgres; convierte escrituras no críticas en un impuesto de latencia.
  • sync=disabled: mentir a la aplicación sobre durabilidad. Rápido. También una decisión que limita la carrera cuando se corta la energía.

SLOG: qué es y qué no es

Un SLOG es un dispositivo dedicado para el ZIL. Mejora la latencia de escrituras síncronas al proporcionar un lugar rápido para comprometerlas.
Pero debe ser el tipo correcto de dispositivo: baja latencia, alta resistencia a escrituras y—no negociable—protección ante pérdida de energía.

Si el SLOG miente sobre durabilidad (SSD de consumo con caché volátil y sin PLP), has movido efectivamente la apuesta de sync=disabled al hardware.
Puede “funcionar” hasta que no funcione, lo cual es el patrón de fiabilidad menos útil en operaciones.

Una cita para colgar en la revisión de un incidente

idea parafraseada — John Allspaw: la fiabilidad viene de hacer la falla visible y soportable, no de creer que no ocurrirá.

Propiedades ZFS recomendadas para Postgres (con razonamiento)

Estos son valores por defecto con opinión que funcionan en muchas instalaciones reales. Puedes desviarte, pero hazlo deliberadamente y prueba con tu carga.

Prerequisitos a nivel de pool (antes de los datasets)

  • ashift correcto para tus discos (típicamente 12 para sectores de 4K). Si lo haces mal compras amplificación de escritura permanente.
  • Mirrors para cargas sensibles a latencia (incluyendo Postgres con mucho WAL). RAIDZ puede servir para capacidad y throughput, pero la latencia de pequeñas I/O síncronas suele ser mejor en mirrors.
  • Suficiente RAM para ARC, pero no dejes sin memoria shared buffers de Postgres. ARC crecerá con gusto; tu OOM killer no estará impresionado.

Dataset: tank/pg/data

  • recordsize=16K o recordsize=8K: Comienza con 16K para muchas cargas OLTP; 8K puede reducir lectura-modificación-escritura en actualizaciones pero puede perjudicar lecturas secuenciales grandes. Mide.
  • compression=lz4: Normalmente mejora IOPS efectivos y reduce amplificación de escritura. Es uno de los pocos controles que suele ser más seguro y más rápido.
  • atime=off: Postgres no necesita atime. Deja de pagarlo.
  • xattr=sa (si es soportado): mantiene xattrs en inodos; reduce I/O de metadata.
  • logbias=latency (por defecto): favorecer baja latencia para operaciones síncronas. Para el dataset de datos, dejar el valor por defecto está bien; ajustaremos el dataset WAL por separado.

Dataset: tank/pg/wal

  • recordsize=16K: Las escrituras WAL son tipo append; no necesitas un recordsize enorme. Evita 128K aquí.
  • compression=lz4: El WAL a veces comprime bien, pero la ganancia suele ser modesta. Aun así, normalmente no perjudica y puede reducir escrituras al dispositivo.
  • logbias=throughput:

Esto requiere explicación. logbias=throughput le dice a ZFS que tienda a favorecer escribir datos sync en el pool principal en lugar del SLOG en ciertos patrones.
En un sistema con un buen SLOG, a menudo quieres logbias=latency. En un sistema sin SLOG, el WAL ya vive en el ZIL del pool principal; el bias importa menos.
En la práctica: si tienes un SLOG correcto y la latencia de commit es un cuello de botella, mantén logbias=latency para WAL. Si estás saturando el SLOG o no es el cuello, considera throughput.
No lo apliques por copia; mide la latencia de commit y el comportamiento del ZIL.

  • sync=standard: Sí, todavía. Tu “modo rápido” debería ser a nivel de ajustes de Postgres, no mentiras del sistema de archivos.
  • primarycache=metadata (a veces): Si las lecturas de WAL están expulsando caché útil, limita la caché a metadata para WAL. Depende de tu RAM y carga.

Dataset: tank/pg/tmp (opcional)

  • sync=disabled puede ser aceptable aquí si es verdaderamente espacio temporal desechable y estás cómodo perdiéndolo tras un crash. Trátalo como un disco scratch.
  • compression=lz4, atime=off.

Chiste #2: Lo único más “temporal” que tank/pg/tmp es la confianza de quien no probó un failover.

Tareas prácticas: comandos, salidas y lo que decides

Estos son ejercicios de campo: qué ejecutar, qué significa la salida y qué decisión tomar. El objetivo es dejar de discutir por sensaciones.

Tarea 1: Confirmar salud del pool (porque todo lo demás es inútil si está roto)

cr0x@server:~$ sudo zpool status -v tank
  pool: tank
 state: ONLINE
  scan: scrub repaired 0B in 02:11:03 with 0 errors on Sun Dec 22 03:00:12 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
        logs
          nvme2n1p1                 ONLINE       0     0     0

errors: No known data errors

Qué significa: el pool está ONLINE, el scrub está limpio, el dispositivo SLOG está presente y online.
Decisión: procede con el tuning de rendimiento. Si ves DEGRADED, errores de checksum o un dispositivo de logs caído, detente y arregla la fiabilidad primero.

Tarea 2: Verificar ashift (asesino silencioso de rendimiento)

cr0x@server:~$ sudo zdb -C tank | grep -E "ashift|vdev_tree" -n | head
56:        vdev_tree:
74:            ashift: 12

Qué significa: ashift: 12 indica sectores de 4K. Bueno para SSD/HDD modernos.
Decisión: si ashift es 9 en discos de 4K, pagarás con amplificación de escritura para siempre. La solución es migración/reconstrucción, no un ajuste.

Tarea 3: Inspeccionar propiedades actuales de datasets

cr0x@server:~$ sudo zfs get -o name,property,value,source -r recordsize,compression,atime,sync,logbias,primarycache,xattr tank/pg
NAME          PROPERTY      VALUE     SOURCE
tank/pg       atime         off       local
tank/pg       compression   lz4       local
tank/pg       logbias       latency   default
tank/pg       primarycache  all       default
tank/pg       recordsize    128K      default
tank/pg       sync          standard  default
tank/pg       xattr         sa        local
tank/pg/data  recordsize    16K       local
tank/pg/wal   recordsize    16K       local
tank/pg/wal   logbias       latency   local

Qué significa: puedes ver qué se hereda y qué está explícitamente establecido.
Decisión: establece propiedades en el dataset que coincide con el comportamiento (datos vs WAL), no en el padre “porque es más cómodo.”

Tarea 4: Crear datasets con valores sensatos

cr0x@server:~$ sudo zfs create -o mountpoint=/var/lib/postgresql tank/pg
cr0x@server:~$ sudo zfs create -o mountpoint=/var/lib/postgresql/16/main tank/pg/data
cr0x@server:~$ sudo zfs create -o mountpoint=/var/lib/postgresql/16/wal tank/pg/wal
cr0x@server:~$ sudo zfs create -o mountpoint=/var/lib/postgresql/tmp tank/pg/tmp

Qué significa: los datasets existen y montan donde Postgres espera (ajusta según tu distro/versión).
Decisión: mantén los límites del sistema de ficheros alineados con necesidades operativas: snapshots separadas, propiedades separadas y monitorización separada.

Tarea 5: Aplicar propiedades (datos)

cr0x@server:~$ sudo zfs set atime=off compression=lz4 xattr=sa recordsize=16K sync=standard tank/pg/data
cr0x@server:~$ sudo zfs get -o property,value -H atime,compression,xattr,recordsize,sync tank/pg/data
atime	off
compression	lz4
xattr	sa
recordsize	16K
sync	standard

Qué significa: el dataset de datos está afinado para comportamiento tipo página de Postgres y menor churn de metadata.
Decisión: comienza en 16K. Si las actualizaciones son intensas y ves amplificación de escritura, prueba 8K. No adivines.

Tarea 6: Aplicar propiedades (WAL)

cr0x@server:~$ sudo zfs set atime=off compression=lz4 recordsize=16K sync=standard logbias=latency tank/pg/wal
cr0x@server:~$ sudo zfs get -o property,value -H atime,compression,recordsize,sync,logbias tank/pg/wal
atime	off
compression	lz4
recordsize	16K
sync	standard
logbias	latency

Qué significa: WAL está preparado para comportamiento de sync de baja latencia.
Decisión: si la latencia de commit sigue alta, esto apunta al SLOG/latencia del dispositivo, no a “más tuning.”

Tarea 7: Confirmar dónde Postgres escribe WAL

cr0x@server:~$ sudo -u postgres psql -XAtc "show data_directory; show hba_file; show config_file;"
/var/lib/postgresql/16/main
/var/lib/postgresql/16/main/pg_hba.conf
/etc/postgresql/16/main/postgresql.conf
cr0x@server:~$ sudo -u postgres psql -XAtc "select pg_walfile_name(pg_current_wal_lsn());"
00000001000000020000003A
cr0x@server:~$ sudo ls -ld /var/lib/postgresql/16/main/pg_wal
lrwxrwxrwx 1 postgres postgres 26 Dec 25 10:44 /var/lib/postgresql/16/main/pg_wal -> /var/lib/postgresql/16/wal

Qué significa: el directorio WAL está redirigido correctamente.
Decisión: si el WAL sigue dentro del dataset de datos, pierdes aislamiento de tuning y las snapshots se vuelven más problemáticas de lo necesario.

Tarea 8: Comprobar si estás limitado por commits síncronos

cr0x@server:~$ sudo -u postgres psql -XAtc "select name, setting from pg_settings where name in ('fsync','synchronous_commit','wal_sync_method');"
fsync|on
synchronous_commit|on
wal_sync_method|fdatasync

Qué significa: Postgres se comporta de forma segura: fsync on, commits síncronos.
Decisión: si la latencia es inaceptable, arréglalo con un SLOG o con compensaciones de durabilidad a nivel de Postgres—no deshabilitando sync en ZFS.

Tarea 9: Medir actividad ZIL/SLOG y detectar presión de sync

cr0x@server:~$ sudo zpool iostat -v tank 1 5
                              capacity     operations     bandwidth
pool                        alloc   free   read  write   read  write
--------------------------  -----  -----  -----  -----  -----  -----
tank                         980G  2.65T    120   1800  9.2M  145M
  mirror-0                   980G  2.65T    120   1700  9.2M  132M
    nvme0n1p2                   -      -     60    850  4.6M   66M
    nvme1n1p2                   -      -     60    850  4.6M   66M
logs                             -      -      0    420    0  4.1M
  nvme2n1p1                      -      -      0    420    0  4.1M
--------------------------  -----  -----  -----  -----  -----  -----

Qué significa: el vdev logs está realizando escrituras. Eso es tráfico sync golpeando el SLOG.
Decisión: si las operaciones de escritura del SLOG son altas y la latencia de commit es alta, tu dispositivo SLOG puede ser demasiado lento, saturado o estar fallando.

Tarea 10: Vigilar latencia directamente con iostat (verdad a nivel de dispositivo)

cr0x@server:~$ iostat -x 1 3
Linux 6.8.0 (server)  12/25/2025  _x86_64_  (32 CPU)

Device            r/s   w/s  rkB/s  wkB/s  await  svctm  %util
nvme0n1          80.0  600.0  5120  65536   2.10   0.20  14.0
nvme1n1          78.0  590.0  5000  64500   2.05   0.19  13.5
nvme2n1           0.0  420.0     0   4200   0.35   0.05   2.5

Qué significa: bajo await en el dispositivo SLOG sugiere que no es el cuello de botella.
Decisión: si await en el dispositivo de logs se dispara a milisegundos bajo carga de commits, arregla el SLOG (dispositivo, topología PCIe, firmware, PLP) o no uses uno.

Tarea 11: Verificar que la compresión realmente ayuda (y no quema CPU)

cr0x@server:~$ sudo zfs get -o name,property,value -H compressratio,compression tank/pg/data tank/pg/wal
tank/pg/data	compressratio	1.62x
tank/pg/data	compression	lz4
tank/pg/wal	compressratio	1.10x
tank/pg/wal	compression	lz4

Qué significa: los datos se comprimen bien, el WAL no tanto. Eso es normal.
Decisión: deja la compresión encendida; la ganancia en datos suele superar todo lo demás. Si la CPU está al máximo, mide antes de cambiar.

Tarea 12: Revisar presión de ARC y si ZFS compite por RAM con Postgres

cr0x@server:~$ sudo arcstat 1 3
    time  read  miss  miss%  dmis  dm%  pmis  pm%  mmis  mm%  arcsz     c
10:52:01   520    40      7    30   75    10   25     0    0   64G   96G
10:52:02   610    55      9    45   82    10   18     0    0   64G   96G
10:52:03   590    50      8    41   82     9   18     0    0   64G   96G

Qué significa: ARC es grande pero no está al tope, miss% es razonable. Si miss% es alto y Postgres también hace mucho cache, puede haber doble cacheo.
Decisión: ajusta el máximo de ARC si el sistema está haciendo swap o Postgres está hambriento; o reduce shared_buffers si ARC está haciendo mejor el trabajo para tu patrón de acceso.

Tarea 13: Revisar tiempo de grupos de transacción (fuente de latencia periódica)

cr0x@server:~$ sudo sysctl vfs.zfs.txg.timeout
vfs.zfs.txg.timeout: 5

Qué significa: el intervalo de commit TXG es 5 segundos (valor común). Las cargas con ráfagas pueden ver comportamiento periódico de flush.
Decisión: no lo cambies solo porque puedes. Si ves picos de latencia regulares alineados con commits TXG, investiga presión de escritura y latencia de dispositivos antes de tocarlo.

Tarea 14: Confirmar estado de autotrim (rendimiento SSD a lo largo del tiempo)

cr0x@server:~$ sudo zpool get autotrim tank
NAME  PROPERTY  VALUE     SOURCE
tank  autotrim  on        local

Qué significa: TRIM está activado; los SSD se mantienen más sanos bajo escrituras sostenidas.
Decisión: actívalo para pools de SSD salvo que tengas una razón específica para no hacerlo.

Tarea 15: Comprobación de política de snapshots (porque backups son una característica operativa)

cr0x@server:~$ sudo zfs list -t snapshot -o name,creation -S creation | head
NAME                               CREATION
tank/pg/data@hourly-2025-12-25-10   Thu Dec 25 10:00 2025
tank/pg/wal@hourly-2025-12-25-10    Thu Dec 25 10:00 2025
tank/pg/data@hourly-2025-12-25-09   Thu Dec 25 09:00 2025
tank/pg/wal@hourly-2025-12-25-09    Thu Dec 25 09:00 2025

Qué significa: existe una cadencia consistente de snapshots para ambos datasets.
Decisión: las snapshots no son backups por sí solas, pero permiten rollback rápido y replicación. Asegura que la retención/archivo WAL coincida con la cadencia de snapshots.

Tarea 16: Probar latencia de escrituras sync con pgbench y correlacionar

cr0x@server:~$ sudo -u postgres pgbench -i -s 50 benchdb
dropping old tables...
creating tables...
generating data...
vacuuming...
creating primary keys...
done.
cr0x@server:~$ sudo -u postgres pgbench -c 16 -j 16 -T 60 -N benchdb
transaction type: 
scaling factor: 50
query mode: simple
number of clients: 16
number of threads: 16
duration: 60 s
number of transactions actually processed: 920000
latency average = 1.043 ms
tps = 15333.201 (without initial connection time)

Qué significa: esto es mayormente lecturas (-N) y no debería estresar mucho commits sync.
Decisión: ejecuta también una prueba intensiva en escritura; si solo las pruebas de escritura/commit son lentas, enfócate en WAL + ruta de sync (SLOG, latencia del dispositivo, profundidad de cola).

Guía rápida de diagnóstico

Cuando producción está lenta, no tienes tiempo para filosofía. Necesitas un camino corto al cuello de botella.
Aquí está el orden que encuentra problemas reales rápidamente.

Primero: prueba que sea latencia de sync (o no)

  • Comprueba Postgres: ¿son lentos los commits o las consultas por otras razones?
  • Mira pg_stat_statements y la distribución de latencia de transacciones, no solo el TPS promedio.
  • Si ves picos de latencia de escritura alineados con commits/checkpoints, sospecha de la ruta de sync.
cr0x@server:~$ sudo -u postgres psql -XAtc "select checkpoints_timed, checkpoints_req, buffers_checkpoint, buffers_clean from pg_stat_bgwriter;"
120|8|981234|44321

Decisión: un alto checkpoints_req en relación con checkpoints temporales sugiere presión y stalls relacionados con checkpoints; investiga tuning de WAL/checkpoints y también latencia del almacenamiento.

Segundo: comprobar que ZFS está sano y no se está auto-limitando

  • zpool status para errores y actividad de resilver/scrub.
  • zpool iostat -v 1 para ver a dónde van las escrituras (vdevs principales vs logs).

Tercero: encontrar el dispositivo que realmente está lento

  • iostat -x 1 y busca alto await y alto %util.
  • Si existe SLOG, revísalo explícitamente.
  • Confirma la colocación PCIe y si el “dispositivo rápido” está en un bus compartido o detrás de un controlador raro.

Cuarto: revisar cachés y presión de memoria

  • Tamaño y misses de ARC.
  • Swap de Linux o tormentas de reclaim de memoria.
  • shared_buffers de Postgres vs caché del SO vs interacciones con ARC.

Quinto: revisar fragmentación y señales de amplificación de escritura

  • zpool list para capacidad; alta ocupación perjudica el rendimiento.
  • recordsize del dataset vs patrón real de I/O.
  • Autotrim, tendencias de ratio de compresión.

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

Estos son los que aparecen en rotaciones de guardia porque son sutiles, sobrevivibles… hasta que no lo son.

1) “Commits lentos e inestables” → sin SLOG (o uno malo) → añade un SLOG apropiado

Síntoma: TPS parece bien hasta que sube la concurrencia; la latencia de commit salta. Los usuarios ven pausas aleatorias.

Causa raíz: escrituras síncronas WAL aterrizan en un vdev RAIDZ ocupado o discos lentos; o el SLOG es de consumo sin PLP y muestra picos de latencia.

Solución: usa dispositivos SLOG en espejo, de baja latencia y con protección ante pérdida de energía; verifica con zpool iostat -v y iostat -x.

2) “El rendimiento empeoró tras tunear recordsize” → desajuste de recordsize y amplificación → revertir y probar

Síntoma: tras establecer recordsize=8K en todas partes, los scans secuenciales y backups se volvieron más lentos; subió la carga de CPU.

Causa raíz: recordsize pequeño aumenta overhead de metadata y reduce eficiencia de prefetch para lecturas grandes.

Solución: usa 16K (a veces 32K) para data, mantén WAL más pequeño; compara con tus consultas reales.

3) “Perdimos datos comprometidos tras un corte de energía” → sync=disabled (o SLOG inseguro) → restaurar durabilidad y revalidar

Síntoma: la base de datos reinicia limpio pero faltan escrituras recientes; réplicas en desacuerdo; logs de auditoría no coinciden.

Causa raíz: ZFS ackea escrituras síncronas sin almacenamiento estable. Puede ser sync=disabled explícito o un dispositivo que miente.

Solución: poner sync=standard; usar un SLOG adecuado; ejecutar comprobaciones de consistencia y validar suposiciones de la aplicación sobre durabilidad.

4) “Stalls periódicos de 5–10 segundos” → pool casi lleno o ráfagas de checkpoints → liberar espacio y suavizar patrón de escritura

Síntoma: stalls en intervalos aproximadamente regulares; I/O luce bien por lo demás.

Causa raíz: pool demasiado lleno (la asignación se vuelve cara), o checkpoints fuerzan grandes flushes.

Solución: mantén pools con espacio cómodo; ajusta checkpoints/WAL de Postgres; asegura que datasets de WAL y datos estén tuneados por separado.

5) “CPU alta, I/O bajo, consultas lentas” → compresión o checksumming culpados incorrectamente → revisar memoria y contención de locks

Síntoma: las gráficas de almacenamiento parecen calmadas, pero la latencia es alta y las CPU están ocupadas.

Causa raíz: no estás limitado por I/O; quizá hay contención de buffers, presión de vacuum o mal dimensionamiento de shared_buffers/combate con ARC.

Solución: usa vistas de Postgres (pg_stat_activity, pg_stat_bgwriter, pg_stat_io en versiones recientes) para encontrar el cuello real. No culpes a ZFS sin pruebas.

Tres mini-historias del mundo corporativo

Mini-historia 1: El incidente causado por una suposición equivocada

Una compañía SaaS mediana migró su clúster primario Postgres de ext4 en RAID hardware a ZFS en un par de mirrors NVMe relucientes.
Hicieron la migración con cuidado: ensayaron, midieron, incluso hicieron un corte durante el fin de semana. Las gráficas se veían bien. Todos se fueron a casa.

Una semana después, un mantenimiento eléctrico no salió como se planeó. Los racks volvieron, Postgres arrancó, la replicación se puso al día y todo parecía “bien.”
Luego comenzaron los tickets de soporte: faltaban actualizaciones de los últimos minutos antes del corte. No era corrupción. No era un clúster caído. Simplemente… transacciones ausentes.

La suposición equivocada fue sutil y muy humana: creyeron “NVMe es rápido y ZFS es seguro, así que podemos deshabilitar sync para igualar el rendimiento de ext4.”
Habían puesto sync=disabled en el dataset, pensando que Postgres seguiría seguro porque fsync=on en Postgres. Pero ZFS estaba devolviendo éxito
sin forzar almacenamiento estable. Postgres había hecho lo correcto; el sistema de archivos se negó a participar.

La solución post- incidente fue aburrida: restaurar sync=standard, añadir un SLOG en espejo con PLP y documentar que el “modo rápido” para subconjuntos de cargas es
synchronous_commit=off en sesiones específicas —no una mentira a nivel de sistema de archivos.

La lección real no fue “nunca optimizar.” Fue: no optimices cambiando el significado de “committed.” Tu aplicación ya tiene un control para eso.

Mini-historia 2: La optimización que salió mal

Otra organización—grande, regulada y aficionada a comités—tenía un sistema Postgres de reporting que ejecutaba grandes scans secuenciales y escrituras por lotes nocturnas.
Leyeron un post sobre casar recordsize con el tamaño de página de la base de datos y decidieron imponer recordsize=8K en todo el pool “por consistencia.”
Se presentó un cambio. Todos asintieron. La consistencia da confianza.

En una semana, los jobs nocturnos comenzaron a tardar más. Luego la ventana de “ponerse al día por la mañana” empezó a invadir horas pico.
El sistema de batch no era más intensivo en escrituras que antes; simplemente hacía más trabajo por byte almacenado.

La razón: la carga tenía grandes lecturas secuenciales (consultas analíticas) y el recordsize pequeño aumentó overhead de metadata y redujo eficiencia de lectura.
La prefetch y agregación de bloques de ZFS no podían hacer bien su trabajo. La compresión seguía activada, pero ahora comprimía muchos registros pequeños y los seguía individualmente. La CPU subió. Los discos no estaban saturados; el sistema gastaba más tiempo en gestión.

Lo arreglaron separando datasets por comportamiento: tablespaces de data-warehouse en un dataset con recordsize=128K (a veces 256K para scans muy grandes),
y datasets OLTP en 16K. WAL quedó en 16K. Todos conservaron su “consistencia”, solo que no a nivel de pool completo.

La conclusión silenciosa: un número no puede describir “una carga de base de datos.” Si no puedes nombrar el patrón de I/O, no puedes tunearlo.

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

Un equipo de servicios financieros ejecutaba Postgres sobre ZFS con una configuración conservadora: mirrors, compresión activada, sync standard y un SLOG en espejo.
Nada espectacular. Su mejor ingeniero lo describió como “agresivamente poco interesante.” Era un elogio.

También tenían un hábito que otros equipos ridiculizaban: scrubs semanales y un panel que mostraba errores de checksum incluso cuando “todo funciona.”
En papel, los scrubs eran “I/O extra.” En reuniones, los scrubs eran “¿por qué nos lo hacemos?”. En realidad, los scrubs eran el canario.

Una semana, el scrub informó un pequeño número de errores de checksum en un único dispositivo. Aún no había errores en aplicaciones. No había síntomas de clientes.
Reemplazaron el dispositivo según programa, resilverizó limpio y siguieron. Semanas después, un SSD de modelo similar fue hallado con un problema de firmware bajo una transición de estado de energía específica. Los equipos sin detección proactiva lo descubrieron de la forma emocionante.

Su resultado fue poco glamuroso: sin incidente, sin corte, sin pérdida de datos. El mejor tipo de historia en operaciones es la que casi no cuentas.
Pero cuando tu trabajo es mantener promesas al negocio, “aburrido” es una característica.

Listas de verificación / plan paso a paso

Paso a paso: construir un host Postgres-on-ZFS de forma sensata

  1. Diseña el pool priorizando latencia: mirrors para OLTP primario si te importa la latencia de commit. RAIDZ es para eficiencia de capacidad y throughput.
  2. Elige alineación de sectores correcta: confirma ashift=12 al crear el pool.
  3. Añade un SLOG solo si lo necesitas (cargas con muchas sync) y solo si tiene protección PLP. Hazlo en espejo.
  4. Crea datasets separados: tank/pg/data, tank/pg/wal, opcionalmente tank/pg/tmp y tank/pg/backups.
  5. Configura propiedades por dataset: compression, recordsize, logbias, atime, xattr.
  6. Conecta directorios de Postgres correctamente y verifica la ubicación de WAL.
  7. Benchmark con pgbench y un conjunto de consultas similar a producción. Mide percentiles de latencia de commit.
  8. Activa scrubs y monitoriza errores. Trata los errores de checksum como alarmas de humo, no como sugerencias.
  9. Snapshot con intención: coordina con archivado/retención WAL y tus objetivos de recuperación.
  10. Documenta el contrato de durabilidad: qué ajustes están permitidos (synchronous_commit), y qué está prohibido (sync=disabled en datasets críticos).

Lista operativa: antes de cambiar cualquier propiedad ZFS en producción

  • ¿Puedes revertir? (Los cambios de propiedades son fáciles; las regresiones de rendimiento no lo son.)
  • ¿Tienes reproducción de la carga? (perfil pgbench, replay o al menos un conjunto de consultas de prueba conocido).
  • ¿Cambiará la semántica de durabilidad? Si sí, ¿tienes aprobación explícita?
  • ¿Capturaste antes/después: latencia de commit p95/p99, await de dispositivo, zpool iostat?
  • ¿El pool está sano, scrubeado y no está resilverizando?

Preguntas frecuentes

1) ¿Debería ejecutar PostgreSQL sobre ZFS?

Sí, si valoras integridad de datos, snapshots y herramientas operativas, y estás dispuesto a entender el comportamiento de sync.
Si tu equipo trata el almacenamiento como una caja negra, lo aprenderás —solo que será durante un incidente.

2) ¿Necesito un SLOG para PostgreSQL?

Solo si la latencia de escrituras síncronas es tu cuello de botella. Muchas cargas lectoras o con replicación asíncrona no se benefician mucho.
Si lo necesitas, usa dispositivos con protección PLP y móntalos en espejo.

3) ¿Es sync=disabled alguna vez aceptable?

En datasets críticos de Postgres: no. En datasets temporales verdaderamente desechables: quizá, si eres explícito sobre perderlos en un crash.
Si quieres menos durabilidad para un subconjunto de operaciones, usa los controles de Postgres como synchronous_commit.

4) ¿Qué recordsize debo usar para datos de Postgres?

Empieza en 16K. Considera 8K para OLTP muy orientado a escrituras si mides reducción de amplificación. Considera mayor recordsize (64K–128K)
para tablespaces analíticos con scans grandes. Datasets separados facilitan esto.

5) ¿Debería poner el WAL en un dataset separado?

Sí. Te da tuning dirigido y límites operativos más claros. El aislamiento de rendimiento es una ventaja; la claridad es la ganancia principal.

6) ¿La compresión ZFS ayuda a bases de datos?

Usualmente sí. LZ4 es el valor común porque es rápido y de bajo riesgo. Reduce I/O físico, que es lo que la mayoría de bases de datos realmente esperan.

7) ¿Cómo interactúan las snapshots con la consistencia de PostgreSQL?

Las snapshots ZFS son consistentes ante crashes a nivel de sistema de ficheros. Para backups consistentes a nivel aplicación, coordina con Postgres: usa base backups,
archivado WAL o quiesce apropiadamente. Las snapshots son gran infraestructura; no son salsa mágica de consistencia.

8) ¿Debo desactivar atime?

Sí para datasets de Postgres. Las actualizaciones de atime son ruido de escritura. Déjalo apagado salvo que tengas un requerimiento de cumplimiento que dependa de atime.

9) ¿Mirrors o RAIDZ para Postgres?

Mirrors cuando la latencia importa (OLTP, mucha carga de commits). RAIDZ cuando importa eficiencia de capacidad y tu carga es más secuencial o tolera latencia.
También puedes mezclar: tener un vdev mirror “carril rápido” para WAL o datos calientes y un vdev RAIDZ para datos fríos, pero cuidado con la complejidad.

10) ¿Cuál es la configuración segura más simple que rinde bien?

Pool en mirror, compression=lz4, atime=off, dataset de datos en 16K recordsize, dataset WAL en 16K recordsize, sync=standard.
Añade SLOG en espejo solo si la latencia de commit lo exige.

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

Si ya ejecutas Postgres sobre ZFS, no necesitas una refactorización heroica. Necesitas dos cosas: límites de datasets que reflejen la realidad y una ruta de sync que
coincida con tus promesas de durabilidad.

  1. Separa datasets al menos en data y WAL. Aplica propiedades de forma intencional.
  2. Mide la latencia de commit bajo carga de escritura y correlaciónala con zpool iostat y iostat -x.
  3. Si sync es el cuello de botella, arréglalo correctamente: SLOG espejo PLP o cambios dirigidos de durabilidad en Postgres—no mentiras del sistema de archivos.
  4. Mantén la compresión activada y deja de pagar por atime.
  5. Haz de los scrubs y la monitorización de errores un hábito. “No incidentes” no es suerte; es detección más disciplina aburrida.

El objetivo es un sistema que no requiera un experto en almacenamiento para operar día a día. ZFS puede ofrecer eso para PostgreSQL.
Pero tienes que tratar la semántica de sync como un contrato, no como una sugerencia.

← Anterior
Etiquetas ZFS corruptas: solucionar fallos de importación correctamente
Siguiente →
Copias de seguridad de correo: el ejercicio de restauración que debes ejecutar (o tus respaldos son falsos)

Deja un comentario