MariaDB vs PostgreSQL en HDD: ¿Quién sufre más bajo presión de disco (y por qué)?

¿Te fue útil?

Los discos giratorios no fallan de forma elegante. Fallan como una invitación a una reunión: al principio silenciosamente, y luego de repente todos llegan tarde y nadie sabe por qué.
Cuando MariaDB o PostgreSQL se ejecutan en HDD y la carga deja de caber en memoria, obtienes un tipo específico de miseria: latencias altas, commits bloqueados,
timeouts de la aplicación, y un panel que parece un monitor cardíaco.

La pregunta práctica no es “¿Qué base de datos es más rápida?” Es: ¿cuál se degrada menos cuando el disco se convierte en el cuello de botella?
y qué ajustes realmente funcionan cuando no puedes “simplemente añadir SSDs” este trimestre.

La respuesta directa: quién sufre más en HDD

Bajo presión de disco en HDD, ambos sufren. Pero sufren de maneras diferentes, y la respuesta de “quién sufre más” depende de
qué tipo de presión de disco estás viendo.

  • Presión de lecturas aleatorias (el conjunto de trabajo no cabe en RAM, muchas búsquedas puntuales):
    MariaDB/InnoDB suele degradarse más. La caída de rendimiento de InnoDB en HDD es pronunciada cuando los misses del buffer pool se convierten en búsquedas aleatorias.
  • Presión de escrituras/commits (muchas transacciones, durabilidad activada):
    PostgreSQL puede parecer peor si el WAL (y el comportamiento de fsync) están en el mismo dispositivo lento y el ajuste de checkpoints está descuidado.
    Verás picos de latencia en commits y “tormentas de checkpoints”.
  • OLTP mixto (carga web típica):
    MariaDB tiende a ser más sensible al I/O aleatorio y a patrones de índices secundarios; PostgreSQL suele ser más sensible al pacing de WAL/checkpoint/vacuum.

Si me obligas a un titular: MariaDB tiende a “sufrir más” en HDD cuando estás limitado por IOPS en lecturas aleatorias;
PostgreSQL tiende a “sufrir más” cuando estás limitado por fsync/WAL/checkpoints en escrituras y no ajustas nada.
En producción real, puedes empeorar cualquiera de los dos con la combinación adecuada de valores por defecto, esquema y pensamiento optimista.

Guía operativa con postura: si debes correr en HDD, deberías inclinarte hacia
menos I/Os aleatorios, más memoria, flushing más lento pero constante, e aislamiento entre rutas de escritura secuencial y rutas de lectura aleatoria.
Ambas bases de datos pueden volverse soportables en HDD, pero ninguna perdona cuando tu carga se convierte en una orgía de seeks.

Qué odia el HDD (y qué tolera)

HDD no es “lento”. Es inconsistente. El rendimiento secuencial puede verse bien en papel. El asesino es la latencia de seek y el encolamiento.
Una vez que haces mucho I/O aleatorio con bloques pequeños, un único disco se comporta como una pequeña puerta por la que todos intentan pasar a la vez.

HDD es alérgico al I/O aleatorio

Las lecturas aleatorias son caras porque la cabeza debe moverse. Añade concurrencia y no obtienes “paralelismo”, obtienes “sillas musicales”.
Los patrones OLTP (búsquedas puntuales, sondas de índices secundarios, nested loops) se convierten en lecturas dispersas y el disco se vuelve el mutex global.

HDD tolera escrituras secuenciales… hasta que no

Las escrituras de registro secuenciales pueden estar bien en HDD: append-heavy, mayormente lineales. Ahí es donde PostgreSQL WAL y los redo logs de InnoDB
pueden ser tus aliados—si el sistema los mantiene secuenciales. Pero los checkpoints, doublewrite, page flushing y vacuum pueden convertir
lo “secuencial-ish” en “aleatorio-ish”, y tu profundidad de cola se vuelve un chiste.

Broma #1: Ejecutar una base de datos con mucha escritura en un solo HDD es como hacer sistemas distribuidos con notas adhesivas—técnicamente posible, socialmente caro.

El cuello de botella de HDD que realmente observas: latencia, no ancho de banda

En incidentes de producción, la señal suele ser:
los tiempos de espera aumentan, la latencia p99 de la aplicación sube, y el throughput cae.
La gente mira gráficos de “MB/s” y se pierde la historia real: un seek de 10ms se vuelve 100ms cuando la cola se acumula, y ahora el
fsync de tu transacción espera detrás de un vacuum y un checkpoint.

Por qué InnoDB y PostgreSQL se comportan distinto bajo presión

InnoDB (MariaDB): los misses del buffer pool se vuelven lecturas aleatorias

InnoDB es un motor basado en páginas. Datos e índices viven en B-trees. Cuando una página no está en el buffer pool, se trae del disco.
En SSD pagas una pequeña penalización. En HDD, cada miss puede significar un seek.

Las fortalezas de InnoDB—adaptive hash index, change buffering, flushing en background—ayudan, pero no pueden anular la física.
Si tu conjunto caliente no cabe en RAM y el patrón de acceso es aleatorio, InnoDB se convierte en un generador de I/O aleatorio.

  • Doublewrite buffer: protege contra escrituras parciales de páginas (importante en HDD), pero añade sobrecarga de escritura.
  • Redo log: secuencial-ish, pero las políticas de flush pueden forzar fsyncs frecuentes.
  • Change buffer (insert buffer): puede diferir parte del mantenimiento de índices secundarios, reduciendo escrituras aleatorias, pero las fusiones posteriores pueden detonar picos de I/O.

PostgreSQL: WAL es secuencial; los checkpoints deciden si dormirás

PostgreSQL también usa páginas de tamaño fijo (blocks) y un cache compartido. Pero su ruta de escritura está centrada en WAL:
el commit escribe registros WAL, y las páginas de datos se escriben después por background writer / checkpointer.

En HDD, que WAL sea secuencial es bueno. La trampa es que el modelo de durabilidad y el checkpointing de PostgreSQL pueden crear ráfagas:
un checkpoint grande puede flushar muchas páginas sucias rápidamente, provocando escrituras aleatorias y contención con las lecturas. Si no ajustas bien,
obtienes sierras de latencia: calma, luego caos, repitiéndose en horario.

  • WAL fsync: la latencia de commit depende del comportamiento de fsync y del rendimiento del dispositivo WAL.
  • Checkpoints: fuerzan páginas sucias a disco; demasiado frecuentes o demasiado repentinos equivalen a tormentas de I/O.
  • Autovacuum: mantiene la bloat a raya pero puede crear carga de lectura/escritura en background; mal configurado, se convierte en un impuesto silencioso al disco.

Dónde se esconde el dolor en HDD: amplificación de escritura y trabajo en background

Cuando la gente dice “presión de disco”, a menudo quieren decir “la base de datos hace más I/O del que pienso”.
Eso suele ser amplificación de escritura:

  • Demasiadas páginas sucias flushadas por unidad de trabajo útil (mal comportamiento de checkpoints/flushing).
  • Bloat y fragmentación que hace que lecturas/escrituras toquen más páginas.
  • Índices que multiplican escrituras (cada insert/update toca varios B-trees).
  • Configuraciones de replicación/logging que fuerzan comportamiento síncrono de disco con mayor frecuencia de la necesaria.

Una cita de confiabilidad que debería estar en cada pared de ops

La esperanza no es una estrategia. — Gen. Gordon R. Sullivan

En HDD, esperar que la caché del kernel o “algún ajuste después” te salve es como terminar en un incidente haciendo respuesta con el almuerzo frío.

Modos de fallo por presión de disco que realmente verás

1) El incidente “todo va lento pero la CPU está inactiva”

La CPU parece aburrida, el load average está alto, y los hilos de la base de datos están “ejecutándose” pero no hacen nada productivo.
En Linux, eso suele ser iowait y tareas bloqueadas. La profundidad de cola del HDD sube, el await aumenta, y tu p95 se convierte en una disculpa p95tésima.

2) Picos de latencia en commits (stalls de fsync)

En PostgreSQL, esto suele aparecer como commits lentos cuando WAL fsync se retrasa por saturación del disco.
En MariaDB/InnoDB, los flushes y fsync del redo log pueden igualmente bloquear commits según las configuraciones de flush.

3) El mantenimiento en background pelea con tu tráfico foreground

Checkpoints, vacuum/autovacuum y los limpiadores de páginas de InnoDB son “útiles” hasta que no lo son.
En HDD, el peor patrón es: el trabajo en background se vuelve agresivo, la latencia del disco se dispara, y entonces el trabajo de foreground se ralentiza,
lo que causa más backlog, lo que causa más páginas sucias, lo que hace que el background sea más agresivo. Es un bucle de retroalimentación con paginación.

4) La sorpresa “añadimos un índice y todo empeoró”

Los índices aceleran lecturas pero multiplican escrituras. En HDD, ese multiplicador de escrituras es un impuesto que pagas con intereses: más páginas ensuciadas, más flushing,
más I/O aleatorio, más bloat. Si no mides, te optimizas hasta causar una caída.

5) Replicación lenta y backlog del “IO thread”

La saturación del disco ralentiza la escritura de WAL/binlogs y la aplicación de cambios. Las réplicas se atrasan, y el failover se vuelve emocionante de la peor manera.
HDD te da menos margen para ser descuidado sobre dónde aterrizan los logs y cuántos fsyncs exiges por segundo.

Guía rápida de diagnóstico

Este es el orden que uso cuando una base de datos en HDD se está derritiendo. Está diseñado para encontrar el cuello de botella rápidamente, no para ser “completo”.
Puedes hacerlo en 5–10 minutos si tienes acceso.

Primero: confirma que es latencia de disco (no CPU, ni locks)

  1. Revisa await y %util por dispositivo. Si await está alto y util está al máximo, estás limitado por I/O.
  2. Verifica si la DB está esperando en fsync o en buffer I/O frente a esperar en locks.
  3. Confirma presión de memoria: si estás intercambiando (swapping) o la tasa de aciertos de caché cayó en picado, se le está pidiendo al HDD que haga lecturas aleatorias que no puede manejar.

Segundo: identifica si lecturas o escrituras impulsan el problema

  1. Observa IOPS de lectura/escritura y el tamaño medio de solicitud. Las lecturas aleatorias aparecen como lecturas pequeñas con await alto.
  2. Revisa estadísticas de la base de datos: ratio de aciertos del buffer cache, generación de WAL/redo, actividad de checkpoints, flush de páginas sucias.

Tercero: elige la mitigación menos mala

  1. Si son lecturas aleatorias: aumenta la caché efectiva (RAM, buffer pool/shared_buffers), reduce el conjunto de trabajo, arregla consultas/índices.
  2. Si son ráfagas de escritura: suaviza checkpoints/flushing, separa WAL/redo en su propio dispositivo si es posible, reduce la frecuencia de sync solo si es aceptable.
  3. Si el mantenimiento es el matón: limita vacuum o page cleaners, programa tareas pesadas fuera de pico, y trata el bloat/fragmentación correctamente.

Tareas prácticas: comandos, salidas, decisiones (12+)

Estos son chequeos de grado producción. Cada uno incluye: comando, qué significa la salida y la decisión a tomar.
ejecútalos en el host de la base de datos (y a veces dentro de psql/mariadb). Ajusta rutas y credenciales a tu entorno.

Task 1: Check per-disk latency and saturation

cr0x@server:~$ iostat -x 1 5
Linux 6.5.0 (db01) 	12/30/2025 	_x86_64_	(8 CPU)

Device            r/s     w/s   rMB/s   wMB/s  avgrq-sz avgqu-sz await r_await w_await  %util
sda              35.0   120.0    1.2     6.8      128     7.50   58.2   42.1    62.9  99.4

Significado: %util cerca de 100% más await en decenas de ms indica que el disco está saturado. avgqu-sz muestra una cola.

Decisión: trata esto como un incidente de I/O. Deja de culpar a la CPU. Luego, determina si dominan lecturas o escrituras y si WAL/redo comparte el mismo dispositivo.

Task 2: See if the kernel is spending time in iowait

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
 3 12      0  51200  12000 820000    0    0   980  5400  900 1800 10  6 40 44  0

Significado: alto b (blocked) y alto wa significa que los hilos esperan por disco.

Decisión: procede al análisis de esperas en la DB; también revisa si hay swapping (si/so) porque swap en HDD es incendio de rendimiento.

Task 3: Confirm you’re not swapping (or worse, thrashing)

cr0x@server:~$ free -h
               total        used        free      shared  buff/cache   available
Mem:            31Gi        26Gi       120Mi       1.1Gi       4.9Gi       2.2Gi
Swap:          4.0Gi       1.8Gi       2.2Gi

Significado: el uso de swap en un host de BD es una bandera roja; available está peligrosamente bajo.

Decisión: reduce la huella de memoria, ajusta cachés de BD para evitar OOM pero prevenir swapping, o añade RAM. En HDD, el uso de swap a menudo correlaciona con tormentas de lecturas aleatorias.

Task 4: Identify top I/O consumers at process level

cr0x@server:~$ sudo iotop -oPa
Total DISK READ: 18.25 M/s | Total DISK WRITE: 72.10 M/s
  PID  PRIO  USER     DISK READ  DISK WRITE  SWAPIN     IO>    COMMAND
 2211 be/4  postgres   2.10 M/s  18.50 M/s   0.00 %  89.00 % postgres: checkpointer
 1998 be/4  postgres   0.50 M/s   9.20 M/s   0.00 %  45.00 % postgres: autovacuum worker
 1870 be/4  mysql      1.20 M/s  22.30 M/s   0.00 %  70.00 % mariadbd

Significado: puedes ver si checkpointer/autovacuum (Postgres) o mariadbd (InnoDB flushing) domina las escrituras.

Decisión: si los procesos en background dominan, ajusta checkpoint/vacuum (Postgres) o settings de flushing (InnoDB), y separa rutas de log/data si es posible.

Task 5: Verify filesystem mount options (barriers, atime)

cr0x@server:~$ mount | egrep ' /var/lib/(postgresql|mysql) '
/dev/sda2 on /var/lib/postgresql type ext4 (rw,relatime,errors=remount-ro)
/dev/sda3 on /var/lib/mysql type ext4 (rw,relatime,errors=remount-ro)

Significado: relatime está bien; noatime puede reducir escrituras de metadata. La cuestión más importante es si logs y datos comparten spindles.

Decisión: considera noatime cuando proceda; más importante aún, planifica la separación de dispositivos para WAL/redo y datos en HDD.

Task 6: Check PostgreSQL wait events for I/O vs locks

cr0x@server:~$ sudo -u postgres psql -c "select wait_event_type, wait_event, count(*) from pg_stat_activity where state='active' group by 1,2 order by 3 desc;"
 wait_event_type |  wait_event   | count
-----------------+---------------+-------
 IO              | DataFileRead  |    18
 IO              | WALWrite      |     6
 Lock            | relation      |     2

Significado: muchas sesiones atascadas en DataFileRead indican lecturas aleatorias; WALWrite indica presión en la escritura de logs.

Decisión: si DataFileRead domina, persigue la tasa de aciertos de caché, consultas malas y bloat. Si WALWrite domina, céntrate en el dispositivo WAL y el ajuste de checkpoints.

Task 7: PostgreSQL checkpoint behavior (are you causing storms?)

cr0x@server:~$ sudo -u postgres psql -c "select checkpoints_timed, checkpoints_req, checkpoint_write_time, checkpoint_sync_time, buffers_checkpoint from pg_stat_bgwriter;"
 checkpoints_timed | checkpoints_req | checkpoint_write_time | checkpoint_sync_time | buffers_checkpoint
------------------+-----------------+-----------------------+----------------------+-------------------
              120 |             310 |              9876543  |              432109 |           9823410

Significado: alto checkpoints_req vs timed sugiere que estás alcanzando max_wal_size o que los disparadores de checkpoint son frecuentes; grandes tiempos de write/sync sugieren flushes dolorosos.

Decisión: aumenta max_wal_size, ajusta checkpoint_timeout y checkpoint_completion_target, y considera mover WAL a un spindle dedicado.

Task 8: PostgreSQL cache hit ratio (quick and dirty)

cr0x@server:~$ sudo -u postgres psql -c "select sum(blks_hit) as hit, sum(blks_read) as read, round(100.0*sum(blks_hit)/nullif(sum(blks_hit)+sum(blks_read),0),2) as hit_pct from pg_stat_database;"
    hit    |  read   | hit_pct
-----------+---------+---------
 987654321 | 5432109 | 99.45

Significado: ~99% aún puede ser malo si el 1% restante es enorme a tu QPS. En HDD, ese 1% puede ser tu caída entera.

Decisión: si hit_pct cae durante el incidente, aumenta la eficacia de la memoria y reduce el conjunto de trabajo (índices, planes de consulta, particionado, cache en la app).

Task 9: PostgreSQL find heavy queries causing reads/writes

cr0x@server:~$ sudo -u postgres psql -c "select queryid, calls, rows, shared_blks_read, shared_blks_hit, (shared_blks_read*8) as read_kb, left(query,120) from pg_stat_statements order by shared_blks_read desc limit 5;"
 queryid  | calls | rows  | shared_blks_read | shared_blks_hit | read_kb | left
----------+-------+-------+------------------+-----------------+---------+-------------------------------
 12345678 |  9200 | 18400 |          812345  |        91234567 | 6498760 | SELECT * FROM events WHERE ...

Significado: unas pocas consultas a menudo dominan las lecturas. En HDD, esa es tu lista objetivo para corregir consultas/índices.

Decisión: arregla a los principales ofensores: añade/ajusta índices (con cuidado), elimina patrones N+1, evita grandes sorts/hashes en disco, reduce columnas retornadas.

Task 10: MariaDB check InnoDB buffer pool and read pressure

cr0x@server:~$ sudo mariadb -e "SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%';"
+---------------------------------------+------------+
| Variable_name                         | Value      |
+---------------------------------------+------------+
| Innodb_buffer_pool_read_requests      | 9876543210 |
| Innodb_buffer_pool_reads              | 43210987   |
+---------------------------------------+------------+

Significado: Innodb_buffer_pool_reads son lecturas físicas. En HDD, si esto sube rápido, estás provocando seeks de disco.
La razón da una pista; la tasa durante el incidente es más importante que el total de por vida.

Decisión: si las lecturas físicas se disparan, aumenta innodb_buffer_pool_size (dentro de los límites de RAM), arregla índices/consultas, y reduce table scans.

Task 11: MariaDB check redo log flush behavior and fsync pressure

cr0x@server:~$ sudo mariadb -e "SHOW GLOBAL VARIABLES LIKE 'innodb_flush_log_at_trx_commit'; SHOW GLOBAL STATUS LIKE 'Innodb_os_log_fsyncs';"
+-------------------------------+-------+
| Variable_name                 | Value |
+-------------------------------+-------+
| innodb_flush_log_at_trx_commit| 1     |
+-------------------------------+-------+
+---------------------+----------+
| Variable_name       | Value    |
+---------------------+----------+
| Innodb_os_log_fsyncs| 55443322 |
+---------------------+----------+

Significado: innodb_flush_log_at_trx_commit=1 es lo más seguro (fsync en cada commit). En HDD con alto TPS, esto puede ser brutal.
Innodb_os_log_fsyncs indica cuán a menudo.

Decisión: no cambies la durabilidad a la ligera. Si el negocio lo permite, 2 reduce la frecuencia de fsync; de lo contrario, mueve redo log a almacenamiento más rápido o reduce la tasa de commits (batching).

Task 12: MariaDB inspect InnoDB dirty page flushing and stalls

cr0x@server:~$ sudo mariadb -e "SHOW ENGINE INNODB STATUS\G" | egrep -i 'Modified db pages|buffer pool size|free buffers|Pending writes|Page cleaner'
Modified db pages   245678
Buffer pool size    2097152
Free buffers        1024
Pending writes: LRU 0, flush list 37, single page 0
Page cleaner: 1000ms intended loop took 8450ms

Significado: muchas páginas modificadas + el loop del page cleaner tardando mucho más de lo previsto implica que InnoDB no puede flushar lo suficientemente rápido.
En HDD esto a menudo precede a stalls y explosiones de latencia súbitas.

Decisión: ajusta innodb_io_capacity/innodb_io_capacity_max para que coincida con la realidad del HDD (no con la fantasía), reduce la tasa de escritura y asegura que el tamaño del redo log sea sensato.

Task 13: Check whether WAL/redo and data are on the same device

cr0x@server:~$ lsblk -o NAME,SIZE,TYPE,MOUNTPOINTS
NAME   SIZE TYPE MOUNTPOINTS
sda   1.8T  disk
├─sda1 512M part /boot
├─sda2 900G part /var/lib/postgresql
└─sda3 900G part /var/lib/mysql

Significado: todo en un solo disco significa que WAL/redo y escrituras de datos compiten por movimiento de cabeza. En HDD, eso es una herida autoinfligida.

Decisión: separa: coloca WAL/binlog/redo en un spindle diferente (o conjunto RAID10) si puedes. Incluso un disco dedicado “mediocre” puede estabilizar la latencia.

Task 14: Identify disk queueing and scheduler

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

Significado: el scheduler afecta la latencia bajo cargas mixtas. Para HDD, un scheduler tipo deadline suele comportarse mejor que “none”.

Decisión: si estás usando none en HDD, considera mq-deadline (prueba primero). No hagas cargo-cult; mide await y la latencia de cola.

Task 15: PostgreSQL WAL volume and location

cr0x@server:~$ sudo -u postgres psql -c "show data_directory; show wal_level; show synchronous_commit;"
      data_directory
-------------------------
 /var/lib/postgresql/16/main
(1 row)

 wal_level
-----------
 replica
(1 row)

 synchronous_commit
-------------------
 on
(1 row)

Significado: las configuraciones de WAL determinan cuánto WAL se genera y cuándo esperan los commits. Si synchronous_commit está on (por defecto), los commits esperan al flush de WAL.

Decisión: mantén synchronous_commit on a menos que aceptes explícitamente perder transacciones recientes. Para alivio en HDD, mueve WAL a dispositivo separado y suaviza checkpoints.

Task 16: PostgreSQL bloat indicator (quick table size reality check)

cr0x@server:~$ sudo -u postgres psql -c "select relname, pg_size_pretty(pg_total_relation_size(relid)) as total, n_live_tup, n_dead_tup from pg_stat_user_tables order by pg_total_relation_size(relid) desc limit 5;"
  relname  | total  | n_live_tup | n_dead_tup
-----------+--------+------------+------------
 events    | 220 GB | 410000000  | 98000000
 sessions  |  85 GB | 150000000  | 72000000

Significado: muchas tuplas muertas implica bloat. En HDD, el bloat son páginas adicionales = más seeks = más sufrimiento.

Decisión: ajusta autovacuum para esa tabla, considera particionado, y programa vacuum/rewrite con cuidado (son operaciones intensivas en I/O).

Tres micro-historias del mundo corporativo

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

Una empresa SaaS mediana corría MariaDB en un par de grandes HDD en RAID1. La carga era OLTP clásico: sesiones, eventos de facturación,
y un par de consultas “analíticas” que se filtraban a producción.
El equipo asumió que RAID1 significaba “el rendimiento de lectura está básicamente bien”. También asumieron que su ratio de aciertos del buffer pool era “suficiente”
porque usualmente estaba por encima del 98%.

Entonces marketing lanzó una función que añadió un endpoint de “búsqueda por prefijo de email”. Hacía una consulta que parecía inofensiva,
pero en la práctica fallaba en usar índices bajo ciertas collations y producía escaneos de rango en una tabla grande.
Durante horas punta, esos escaneos provocaron una ola de churn en el buffer pool. La tasa de misses no parecía dramática en porcentaje.
El número absoluto de lecturas físicas sí lo era.

En HDD, las lecturas aleatorias fueron letales. No porque el disco no pudiera hacer throughput—podía leer muchos megabytes por segundo.
Pero no podía buscar lo suficientemente rápido. La latencia subió, las consultas se acumularon y los pools de conexiones se saturaron. La app empezó a reintentar.
Los reintentos duplicaron la presión. Ya conoces el resto.

La solución no fue exótica. Añadieron el índice compuesto correcto, forzaron a la consulta a usarlo, y luego aumentaron el buffer pool
para que el conjunto caliente realmente cupiera. También pusieron los endpoints “analíticos” en una réplica donde las consultas lentas podían fallar
sin convertir al primario en un generador de I/O aleatorio.

La lección que quedó: RAID1 no hace que los seeks aleatorios sean baratos. Solo te da dos juegos de cabezas para decepcionarte.

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

Una plataforma relacionada con pagos corría PostgreSQL en HDD porque el entorno de cumplimiento era “estable” y la provisión de hardware avanzaba a ritmo de glaciar.
Fueron golpeados por la latencia de commits durante ventanas de importación por lotes.
Alguien propuso una “solución simple”: hacer que los checkpoints ocurran más frecuentemente para que cada checkpoint escriba menos y termine más rápido.

Redujeron checkpoint_timeout y mantuvieron max_wal_size pequeño. Los checkpoints sí terminaron “más rápido.”
También ocurrieron constantemente. Eso convirtió lo que podía haber sido un patrón de escritura de fondo más suave en un estado casi permanente de flushing.
El checkpointer y las escrituras de backend compitieron con las lecturas. El autovacuum seguía existiendo, porque la entropía no pierde.

El síntoma visible por el usuario fue brutal: la latencia p99 desarrolló un patrón rítmico.
Cada pocos minutos, la API se ralentizaba lo suficiente como para disparar timeouts.
La persona on-call veía mensajes de “checkpoint complete” en los logs como un metrónomo infernal.

La solución final fue la opuesta: permitir que WAL se acumule más (mayor max_wal_size), aumentar
checkpoint_completion_target para que las escrituras se distribuyan, y mover WAL a su propio disco.
Las ventanas de importación volvieron a ser aburridas. Nadie hace fiesta por lo “aburrido”, pero eso es lo que buscas.

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

Un gran sistema interno ejecutaba MariaDB y PostgreSQL lado a lado para diferentes servicios.
Todo vivía en arreglos HDD porque el entorno se había construido años atrás y la “capa rápida” estaba reservada para otros sistemas.
El equipo no podía cambiar hardware rápido, así que hicieron algo poco atractivo: hicieron el I/O aburrido.

Separaron rutas de logs de rutas de datos donde fue posible. WAL de PostgreSQL obtuvo spindles dedicados. Los redo logs y binlogs de MariaDB también se separaron.
También hicieron cumplir una regla: no cambios de esquema sin revisión en horas pico, y cada nuevo índice requería medir el coste en escrituras en staging
con volúmenes de datos similares a producción.

Programaron operaciones pesadas de vacuum y el mantenimiento de InnoDB fuera de pico, y vigilaron métricas de “trabajo en background” como halcones:
checkpoint write time, buffer pool reads, páginas sucias y lag de replicación.
Cuando ocurrieron incidentes, tenían una guía rápida de diagnóstico y la gente realmente la usaba en lugar de iniciar un protocolo de enrutamiento de culpas.

El resultado no fue “rápido”. Fue estable. El negocio no notó que los discos eran lentos porque el sistema dejó
de oscilar entre bien y en llamas. El equipo durmió. Dormir es una característica.

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

1) Síntoma: p99 de latencia sube cada pocos minutos (PostgreSQL)

Causa raíz: ráfagas de checkpoints (max_wal_size demasiado pequeño, settings de checkpoint demasiado agresivos, WAL y datos peleando).

Solución: aumenta max_wal_size, sube checkpoint_completion_target (distribuye las escrituras), y mueve WAL a un disco separado si es posible.

2) Síntoma: SELECTs “simples” lentos, CPU baja, await de disco alto (MariaDB)

Causa raíz: misses del buffer pool creando lecturas aleatorias; índices faltantes o incorrectos; conjunto caliente no cabe en RAM.

Solución: aumenta innodb_buffer_pool_size, corrige índices/planes de consulta, reduce table scans, considera particionado o caching.

3) Síntoma: commits se bloquean intermitentemente; “fsync” aparece por todas partes

Causa raíz: dispositivo de logs saturado; demasiadas transacciones forzando fsyncs frecuentes; WAL/binlog/redo compartiendo HDD con datos.

Solución: separa logs en spindles dedicados; agrupa escrituras; mantén ajustes de durabilidad a menos que aceptes perder datos.

4) Síntoma: lag de replicación aumenta en pico

Causa raíz: disco primario saturado; réplica aplicando cambios no puede seguir; mantenimiento en background se lleva el presupuesto de I/O.

Solución: limita mantenimiento, ajusta paralelismo de apply donde aplique, separa log/data y reduce amplificación de escritura (índices, bloat).

5) Síntoma: después de añadir un índice, todo se ralentiza

Causa raíz: amplificación de escritura (mantenimiento de más índices), más páginas sucias, más flushing y I/O aleatorio.

Solución: añade solo índices que se paguen a sí mismos; considera índices parciales/covering (Postgres), elimina índices no usados y valida con pruebas de carga de escritura.

6) Síntoma: autovacuum “golpea” el rendimiento aleatoriamente (PostgreSQL)

Causa raíz: acumulación de bloat y tuplas muertas; autovacuum se ve forzado a trabajar más durante picos, escaneando heaps e índices.

Solución: ajusta autovacuum a nivel de tabla; mantén bajas las tuplas muertas; considera particionado; programa vacuum/rewrite manual cuando sea necesario.

7) Síntoma: InnoDB se queda con advertencias de “page cleaner”

Causa raíz: el flushing no puede mantenerse al día; acumulación de páginas sucias; presión de edad de checkpoint fuerza flushing agresivo.

Solución: ajusta innodb_io_capacity apropiadamente, incrementa la capacidad de redo log donde proceda, reduce picos de escritura y asegura que el buffer pool no esté sobredimensionado para el disco.

8) Síntoma: “Tenemos mucho MB/s disponible” pero la latencia es horrible

Causa raíz: estás limitado por IOPS/seeks, no por ancho de banda. El I/O pequeño y aleatorio te mata mientras los gráficos de MB/s se muestran orgullosos.

Solución: céntrate en await/profundidad de cola, tasas de aciertos de caché y reducir accesos aleatorios; deja de usar gráficos de throughput como cobijo reconfortante.

Listas de verificación / plan paso a paso (supervivencia en HDD)

Paso a paso: estabilizar un incidente en curso

  1. Confirma saturación de I/O: iostat -x muestra await alto + util alta.
  2. Detén la hemorragia: limita temporalmente jobs por lotes, reports pesados y mantenimiento de larga duración.
  3. Separa lecturas de escrituras donde sea posible: enruta analítica a réplicas; pausa escrituras no críticas.
  4. Identifica consultas top: usa pg_stat_statements o slow query log; mata a los peores ofensores si es seguro.
  5. Reduce agresividad de checkpoint/vacuum (Postgres): evita ajustes de emergencia que aumenten la frecuencia de checkpoints.
  6. Revisa presión de memoria: si hay swapping, reduce cachés o reinicia de forma segura tras corregir configuración (con cuidado) para recuperar memoria.

Checklist: prevenir espirales de muerte por lecturas aleatorias

  • Dimensiona cachés de forma realista: MariaDB innodb_buffer_pool_size, Postgres shared_buffers más caché del SO.
  • Mantén conjuntos de trabajo pequeños: elimina índices no usados, archiva datos fríos, particiona tablas grandes append-only.
  • Corrige planes de consulta: evita sequential scans en rutas calientes; vigila nested loops que hagan muchas búsquedas por índice.
  • Prefiere menos y mejores índices en vez de “indexar todo”.

Checklist: prevenir desastres por ráfagas de escritura

  • Separa WAL/redo/binlog del data en HDD.
  • Postgres: aumenta espacio para WAL y reparte checkpoints (max_wal_size, checkpoint_completion_target).
  • MariaDB: monitorea páginas sucias, retraso del page cleaner, presión del redo log; ajusta la I/O capacity para que refleje el dispositivo real.
  • Considera autovacuum e InnoDB background flushing como cargas de primera clase, no “magia de fondo”.

Checklist: cuando HDD es innegociable

  • Usa RAID10 sobre RAID5 para cargas aleatorias intensivas en escritura si debes elegir.
  • Mantén fsync y durabilidad activados a menos que el negocio acepte perder transacciones.
  • Mide la latencia en cola, no solo el throughput.
  • Documenta la guía de “diagnóstico rápido” y practícala.

Datos interesantes & breve historia (el contexto que la gente olvida)

  1. InnoDB no siempre fue el predeterminado en MySQL: MyISAM solía ser común, y se comportaba muy distinto bajo presión de escritura y recuperación tras fallos.
  2. MariaDB se bifurcó de MySQL en 2009: preocupaciones sobre gobernanza tras la adquisición Sun/Oracle empujaron a muchos equipos a cubrirse.
  3. El diseño WAL de PostgreSQL es antiguo y comprobado: la recuperación por WAL ha sido central durante décadas, y es por eso que el I/O secuencial de logs importa tanto.
  4. InnoDB usa doublewrite buffer por defecto: está pensado para proteger contra escrituras parciales de páginas—más relevante en entornos propensos a pérdida de energía y la era HDD.
  5. Los checkpoints de PostgreSQL son intencionalmente ajustables: los valores por defecto priorizan seguridad, no “HDD con carga media y sin paciencia”.
  6. Autovacuum se introdujo para reducir carga operativa: pero un autovacuum mal ajustado es una fuente clásica de I/O inesperado en discos giratorios.
  7. Los schedulers de I/O de Linux cambiaron con blk-mq: lo que funcionaba en kernels antiguos no siempre es lo mejor ahora; HDD sigue beneficiándose de scheduling consciente de latencia.
  8. El firmware de HDD miente a veces: caches de escritura y reordenamiento pueden hacer la latencia impredecible; las bases de datos compensan con semánticas de fsync y supuestos conservadores.

Broma #2: La forma más rápida de aprender sobre el tiempo de seek de un HDD es poner tu base de datos en uno y ver cómo tu carrera busca nuevas oportunidades.

Preguntas frecuentes

1) Si estoy atrapado con HDD, ¿debería elegir MariaDB o PostgreSQL?

Elige según tu carga de trabajo y madurez operativa. Si puedes controlar consultas y mantener el conjunto caliente en RAM, ambos pueden comportarse.
Si esperas presión de lecturas aleatorias, PostgreSQL a menudo degrada de forma más predecible; si esperas altas tasas de commit y no puedes separar WAL, MariaDB puede sentirse más suave hasta que la presión de fsync golpee.
La respuesta real: escoge el que tu equipo pueda ajustar y operar bien, y luego diseña para menos I/Os aleatorios.

2) ¿Cuál es el mejor cambio de hardware en HDD?

Separar escrituras de logs de lecturas de datos. Pon PostgreSQL WAL (y redo/binlog de MariaDB) en spindles dedicados o en una matriz separada.
Reduce la contención de cabezas y estabiliza la latencia de commits.

3) ¿Debería apagar fsync o relajar durabilidad para sobrevivir?

Solo con aprobación explícita del negocio. PostgreSQL fsync=off es invitar a corrupción tras un crash.
MariaDB innodb_flush_log_at_trx_commit=2 puede ser aceptable en algunos casos (arriesgas perder hasta ~1 segundo de transacciones),
pero hazlo como una decisión de producto, no como un parche de medianoche.

4) ¿Por qué mis gráficos muestran MB/s decentes pero la app hace timeouts?

Porque HDD está limitado por IOPS/latencia. Pocos MB/s de lecturas aleatorias pequeñas pueden saturar seeks y crear enormes colas.
Observa await, profundidad de cola y eventos de espera de la BD—no solo el throughput.

5) ¿Aumentar shared_buffers de PostgreSQL arregla el dolor en HDD?

A veces, pero no es mágico. PostgreSQL también depende mucho de la page cache del SO. Sobredimensionar shared_buffers puede reducir la caché efectiva del SO y salir contraproducente.
En HDD, la meta es: mantener el conjunto caliente en caché en algún lugar y evitar swap.

6) ¿Aumentar innodb_buffer_pool_size siempre ayuda a MariaDB en HDD?

Ayuda hasta que provoca swapping o deja al SO sin memoria. En HDD, el swapping es catastrófico. Aumenta el buffer pool con cuidado y mide lecturas físicas y latencia.
Además asegúrate de que tus consultas realmente se beneficien; una consulta mala igual escaneará lo que tengas en caché.

7) ¿Cuál es el error de tuning más común en PostgreSQL para HDD?

Subasignar espacio a WAL y disparar checkpoints solicitados con frecuencia, luego preguntarse por qué la latencia sube rítmicamente.
Da más espacio a WAL y dispersa el I/O de checkpoints.

8) ¿Cuál es el error de tuning más común en InnoDB para HDD?

Fingir que el disco es más rápido de lo que es. Poner innodb_io_capacity demasiado alto puede provocar patrones agresivos de flushing que compiten con las lecturas de foreground.
Ajústalo según la capacidad medida del dispositivo y vigila el comportamiento del page cleaner.

9) ¿Puede RAID arreglar esto?

RAID puede ayudar añadiendo spindles (más cabezas) y mejor redundancia, pero no convertirá I/O aleatorio en I/O barato.
RAID10 suele ser más amigable para cargas mixtas aleatorias que RAID con paridad. Aun así, las mayores ganancias suelen venir de que la caché quepa y de moldear la carga.

10) ¿Hay patrones de esquema especialmente dolorosos en HDD?

Sí: tablas amplias con muchos índices secundarios y actualizaciones frecuentes; tablas “events” sin límite ni particionado; y diseños que fuerzan muchas búsquedas aleatorias por solicitud.
En HDD, quieres localidad y menos toques de página por transacción.

Siguientes pasos prácticos

Si ejecutas MariaDB o PostgreSQL en HDD y la presión de disco ya es un problema, no empieces por los knobs.
Empieza por la medición, luego por la arquitectura, y después por los ajustes.

  1. Mide la latencia correctamente: iostat -x, eventos de espera y estadísticas de BD durante picos.
  2. Separa logs de datos: el aislamiento WAL/redo/binlog es el cambio con mayor ROI en discos giratorios.
  3. Haz que el conjunto caliente quepa: dimensiona cachés correctamente y evita swap. Si no puedes, reduce el conjunto de trabajo (particionar/archivar/eliminar índices no usados).
  4. Arregla las 5 consultas principales: usualmente no es “la base de datos”, son una o dos consultas que convierten al HDD en generador de seeks.
  5. Suaviza el trabajo en background: el ajuste de checkpoints (Postgres) y el comportamiento de flushing (InnoDB) debe buscar estado estable, no heroicas ráfagas.
  6. Documenta tu playbook: usa el orden de diagnóstico rápido y ensáyalo antes de que el próximo incidente te ensaye a ti.
← Anterior
Proxmox pveproxy.service falló: 7 causas comunes y el orden correcto de reparación
Siguiente →
El cloud gaming no matará a las GPU (No: he aquí por qué)

Deja un comentario