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=ony elecciones sensatas desynchronous_commit. - Usa
sync=standardde 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=offpara 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=disableden 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.
- 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.
- 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.
- El tamaño de página por defecto de PostgreSQL es 8 KB. El
recordsizepor 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. - Históricamente, muchas afirmaciones de “ZFS es lento para bases de datos” provinieron de pools mal configurados:
ashiftincorrecto en discos con sectores de 4K, sin SLOG para cargas con muchas sync, o comportamiento de ARC sin suficiente RAM. - 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.
- 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.
- 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.
- 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ólicamentepg_walaquí o usapostgresql.confdonde sea soportado).tank/pg/tmp: tablas temporales / ordenaciones si colocastemp_tablespacesaquí (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)
ashiftcorrecto 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=16Korecordsize=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=disabledpuede 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_statementsy 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 statuspara errores y actividad de resilver/scrub.zpool iostat -v 1para ver a dónde van las escrituras (vdevs principales vs logs).
Tercero: encontrar el dispositivo que realmente está lento
iostat -x 1y busca altoawaity 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_buffersde Postgres vs caché del SO vs interacciones con ARC.
Quinto: revisar fragmentación y señales de amplificación de escritura
zpool listpara 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
- 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.
- Elige alineación de sectores correcta: confirma
ashift=12al crear el pool. - Añade un SLOG solo si lo necesitas (cargas con muchas sync) y solo si tiene protección PLP. Hazlo en espejo.
- Crea datasets separados:
tank/pg/data,tank/pg/wal, opcionalmentetank/pg/tmpytank/pg/backups. - Configura propiedades por dataset: compression, recordsize, logbias, atime, xattr.
- Conecta directorios de Postgres correctamente y verifica la ubicación de WAL.
- Benchmark con
pgbenchy un conjunto de consultas similar a producción. Mide percentiles de latencia de commit. - Activa scrubs y monitoriza errores. Trata los errores de checksum como alarmas de humo, no como sugerencias.
- Snapshot con intención: coordina con archivado/retención WAL y tus objetivos de recuperación.
- Documenta el contrato de durabilidad: qué ajustes están permitidos (
synchronous_commit), y qué está prohibido (sync=disableden 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.
- Separa datasets al menos en data y WAL. Aplica propiedades de forma intencional.
- Mide la latencia de commit bajo carga de escritura y correlaciónala con
zpool iostatyiostat -x. - 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.
- Mantén la compresión activada y deja de pagar por atime.
- 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.