MariaDB vs PostgreSQL: picos de CPU — quién quema núcleos más rápido bajo carga máxima

¿Te fue útil?

No te fijas en la CPU hasta que empieza a chillar. El gráfico parece normal, luego llega un correo promocional, el QPS se triplica
y tus hosts de base de datos se convierten en pequeños calentadores. Los picos de latencia. Las timeouts se apilan. Alguien hace la peor
pregunta en operaciones: “¿Es la base de datos o la app?”

Esto no es un artículo de competición sobre cuál motor es “más rápido”. Bajo carga máxima, tanto MariaDB como PostgreSQL pueden
quemar núcleos rápido—solo que de maneras distintas y previsibles. El objetivo es identificar en qué modo de fallo estás,
probarlo con comandos y aplicar una solución que sobreviva al siguiente pico sin convertir tu rotación on-call en un hobby.

Qué significa realmente “quemar núcleos” bajo carga máxima

CPU alta no es un solo problema; es una reunión familiar de problemas. En la práctica, los “picos de CPU” en bases de datos provienen de:
(1) hacer trabajo útil real (ejecución de consultas), (2) hacer trabajo que no pretendías (planes malos, ordenamientos excesivos,
reparseo repetido), o (3) no hacer nada productivo pero aún así girar en vacío (contención, esperas de bloqueo con bucles activos,
cambios de contexto excesivos).

Bajo carga máxima, la misma consulta puede pasar de “bien” a “devoradora de núcleos” porque la forma del sistema cambia:
los conjuntos calientes crecen, los cachés fallan, se forman colas de bloqueo, o un plan cambia de búsqueda por índice a un escaneo
por estadísticas obsoletas o un parámetro distinto. Tu base de datos no se volvió tonta. Se volvió honesta.

Cuando la gente pregunta “quién quema núcleos más rápido”, suelen querer decir: “¿Cuál llega antes a la saturación de CPU y colapsa
más cuando sube la concurrencia?” La respuesta incómoda: depende de cómo la alimentes. MariaDB (InnoDB) suele sufrir cuando añades demasiados
escritores concurrentes o demasiadas conexiones sin pooling; PostgreSQL suele sufrir cuando permites que la deuda de vacuum se acumule o cuando
una regresión de plan convierte una petición estrecha en un escaneo amplio que multiplica el trabajo entre núcleos. Ambos pueden ajustarse.
Ambos pueden malconfigurarse hasta convertirse en una hoguera.

Un modelo mental útil: los problemas de CPU en picos rara vez se tratan del rendimiento bruto. Se trata del coste de coordinación.
Tu app pide trabajo; la base de datos coordina el acceso al estado compartido. Ahí es donde se pierden los núcleos.

Una frase útil para tener en un sticky, porque describe la mayoría de picos de CPU en producción:
“la latencia es un síntoma, no una causa raíz” (idea parafraseada a menudo atribuida a la metodología de rendimiento de Brendan Gregg).
Tu trabajo es encontrar la causa raíz, no discutir con el gráfico.

Cómo MariaDB gasta CPU cuando entra en pánico

MariaDB en producción suele ser MariaDB + InnoDB (o XtraDB históricamente). InnoDB es bueno en muchas cosas:
recuperación tras crash, bloqueo a nivel de fila y manejo de patrones OLTP comunes. Pero la carga máxima tiene una manera de encontrar las costuras.

Patrones comunes de picos de CPU en MariaDB

1) Coste de concurrencia: hilos, mutex y “demasiados clientes”

MariaDB usa un modelo thread-per-connection. Cuando escalas conexiones linealmente con el tráfico, no solo añades
trabajo de consulta: añades trabajo de planificación. Bajo alta concurrencia, la CPU se come por el cambio de contexto y la
coordinación de bloqueos dentro del motor. Si ves CPU alta con QPS modestos, sospecha thrash: muchos hilos ejecutables,
poco progreso útil.

Si estás ejecutando sin un thread pool (o con uno mal configurado), MariaDB puede parecer que “usa CPU” mientras logra
cada vez menos. Lo verás en las colas de ejecución a nivel SO y en los propios contadores de estado de la base de datos.

2) Contención en InnoDB: filas calientes, índices calientes y dolor por cinchas internas

Bajo carga intensa de escrituras aparecen puntos calientes de contención: una clave primaria auto-incremental con muchas inserciones,
una sola fila “última” golpeada por actualizaciones, o índices secundarios que deben mantenerse en cada escritura. Esto es trabajo real más
coordinación. Si la app crea una fila caliente o una hoja de índice caliente, puedes saturar la CPU mientras el rendimiento se estanca.

InnoDB ha mejorado con los años con locking más fino, pero todavía hay lugares donde la contención aparece como picos de CPU y paradas.
Muchos de esos problemas están definidos por la carga de trabajo, no por la versión.

3) Ejecución de consultas: planes malos y ordenamientos caros

MariaDB puede hacer un gran trabajo rápidamente, pero también puede cometer errores clásicos bajo presión:
ordenar grandes conjuntos intermedios, escanear por índices faltantes, o repetir trabajo idéntico porque la app envía consultas en texto
ligeramente distinto que derrotan la query cache (y de todas formas no deberías confiar en la query cache).

4) Replicación y coste del binlog

La carga máxima a menudo coincide con lag de replicación. Si tu primary está saturado de CPU, no puede volcar binlogs, no puede mantener
réplicas alimentadas y no puede commitear rápido. Las mejoras de group commit ayudan, pero aun así pagas CPU por el logging row-based
y por la maquinaria transaccional asociada. “Solo está escribiendo un log” son las famosas últimas palabras.

Modo de fallo característico de MariaDB bajo carga máxima

Cuando MariaDB colapsa por CPU bajo pico, a menudo parece “todos están ocupados, nadie está feliz”:
montones de conexiones, muchos hilos, alta CPU del sistema y una curva de throughput que deja de escalar al añadir núcleos.
Tu solución suele ser reducir la concurrencia y eliminar puntos calientes de contención, no añadir más núcleos y rezar.

Primera broma (permitida, y sí, es relevante): Una base de datos sin connection pooling es como un bar abierto sin camareros—todos técnicamente atendidos,
pero nada bueno ocurre rápido.

Cómo PostgreSQL gasta CPU cuando entra en pánico

La arquitectura de PostgreSQL es famosa por ser conservadora y muy ingenierizada: process-per-connection, shared buffers,
MVCC, un planner robusto y workers en background que hacen mantenimiento. Bajo carga máxima, suele fallar de forma más “lógica”
que MariaDB: gastará CPU haciendo exactamente lo que cree que le pediste, lo cual a veces es el verdadero problema.

Patrones comunes de picos de CPU en PostgreSQL

1) Regresión de plan: misma consulta, plan distinto, de repente caro

El planner de PostgreSQL es potente y a veces demasiado confiado. Si las estadísticas derivan, la distribución de datos cambia o
los parámetros de bind varían mucho, un plan puede pasar de nested loop por índice a hash join o a un sequential scan. Bajo pico,
ese cambio puede multiplicar el trabajo por órdenes de magnitud, y la CPU seguirá ese patrón.

PostgreSQL te da excelentes herramientas para ver el plan y los tiempos. La trampa es no mirar hasta el outage y luego descubrir que
nunca capturaste una línea base.

2) Deuda de autovacuum: si no la pagas, se cobra interés

MVCC significa que los tuples muertos se acumulan hasta que vacuum los limpia. Si autovacuum no puede seguir el ritmo—por límites de IO,
configuraciones pobres o simplemente porque golpeaste un pico de escrituras—los escaneos tocan más páginas, los índices se inflan y la CPU
sube porque cada consulta hace más trabajo. Verás CPU mayor incluso en “lecturas”, porque las lecturas ahora hacen más trabajo por fila devuelta.

3) Coste de concurrencia: muchas conexiones significan muchos procesos

El modelo process-per-connection de PostgreSQL es estable y fácil de depurar, pero cada backend es real. Demasiadas conexiones significan más
cambios de contexto, huella de memoria mayor y CPU perdida en coordinación, incluso antes de ejecutar SQL. Añade tráfico pico y puedes chocar
con un muro donde la CPU se quema en programación de procesos y gestión de bloqueos.

4) Ordenamientos, hashing y presión de memoria (las minas de work_mem)

PostgreSQL puede quemar CPU haciendo ordenamientos y operaciones de hash. Si work_mem es muy bajo, las operaciones derraman a disco
y obtienes una mezcla de IO wait y sobrecarga de CPU. Si work_mem es demasiado alto y tienes muchas consultas concurrentes,
obtienes presión de memoria, entonces el kernel empieza a hacer sus propias “optimizaciónes”, y el uso de CPU se convierte en una instalación
artística caótica.

Modo de fallo característico de PostgreSQL bajo carga máxima

Cuando PostgreSQL colapsa por CPU bajo pico, a menudo parece: unas pocas formas de consulta dominan, se ejecutan más tiempo de lo habitual,
autovacuum se queda atrás, aumenta el bloat y entonces todo se ralentiza más. Es un bucle de retroalimentación:
consultas más largas mantienen recursos más tiempo, lo que incrementa la contención y la deuda de vacuum, y eso hace las consultas más largas.
Tu solución suele ser arreglar las peores consultas y mantener el vacuum saludable, no “tocar perillas al azar”.

Guía rápida de diagnóstico (primeras/segundas/terceras comprobaciones)

Primero: ¿la CPU es “trabajo real” o “thrash”?

  • Cola de ejecución del SO alta (load average muy por encima del número de CPUs, muchas tareas ejecutables): probablemente thrash o demasiada concurrencia.
  • Uno o pocos hilos/procesos calientes: probablemente consultas específicas, vacuum, replicación o un bloqueo hotspot.
  • CPU de sistema alta: cambio de contexto, overhead del kernel (red, fs), spinlocks.
  • CPU de usuario alta: ejecución de consultas, ordenamientos, hash, evaluación de expresiones.

Segundo: identifica las formas de consulta principales y las causas de espera

  • MariaDB: mira processlist, InnoDB status, métricas handler/read y si estás limitado por la planificación de hilos.
  • PostgreSQL: mira pg_stat_activity, SQL principales en pg_stat_statements e indicadores de vacuum/bloat.

Tercero: decide si el cuello de botella es solo CPU o “CPU como síntoma”

  • Si el IO está saturado, la CPU puede subir por reintentos, churn de buffers y overhead del kernel. Arregla IO primero.
  • Si los bloqueos dominan, los picos de CPU son solo la parte visible. Arregla orden de bloqueos, filas calientes y duración de transacciones.
  • Si ocurrió una regresión de plan, nada más importa hasta que el plan esté arreglado.

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

Estas son las tareas que realmente ejecuto durante un incidente o justo después. Cada una tiene: comando, salida de ejemplo, qué significa
y la decisión que tomas. Son intencionalmente aburridas. Aburrido es cómo te llaman menos a la guardia.

Task 1: Confirma si estás saturado de CPU o solo ruidoso

cr0x@server:~$ uptime
 14:22:01 up 31 days,  3:07,  2 users,  load average: 42.18, 39.77, 28.54

Qué significa: Load average ~42 en una máquina de 16 núcleos significa que tienes muchas más tareas ejecutables o no interrumpibles que CPUs.

Decisión: Deja de adivinar. Inspecciona inmediatamente la cola de ejecución y los procesos top. Si las conexiones son enormes, empieza a limitar la concurrencia (pooling, max connections) mientras diagnosticas.

Task 2: Ver desglose de CPU (user vs system) y cola de ejecución

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
32  1      0 812344  92324 6231880   0    0   120   410 9800 42000 78 18  2  2  0
29  0      0 805112  92328 6239024   0    0    40   210 9600 39000 81 16  1  2  0

Qué significa: r cerca de 30+ indica presión en la cola de ejecutables; alto cs significa cambio de contexto intenso; alto sy sugiere overhead del kernel.

Decisión: Si cs es enorme y la BD tiene miles de conexiones, prioriza pooling de conexiones y límites. Si wa es alto, revisa storage/IO.

Task 3: Identifica los mayores consumidores de CPU y si son muchos o pocos

cr0x@server:~$ ps -eo pid,comm,%cpu,%mem,stat --sort=-%cpu | head
 2314 mariadbd  690.3 18.2 Sl
 4481 postgres  189.4  1.7 R
 4520 postgres  176.0  1.6 R
  911 node      42.1  0.8 R

Qué significa: Un gran proceso mariadbd quemando muchos núcleos sugiere contención interna o carga de consultas intensa en un solo proceso; muchos backends postgres quemando CPU sugiere unas pocas consultas costosas ejecutándose concurrentemente.

Decisión: Si ves un proceso DB dominando: céntrate en contención del motor y planificación de hilos (MariaDB) o en un worker background específico (Postgres). Si muchos procesos backend dominan: encuentra las formas de SQL top.

Task 4: Mide cambio de contexto y migraciones (indicador de thrash)

cr0x@server:~$ pidstat -w -p $(pgrep -o mariadbd) 1 3
Linux 6.1.0 (server)  12/30/2025  _x86_64_  (16 CPU)

01:22:10      PID   cswch/s nvcswch/s  Command
01:22:11     2314    1200.0   38000.0  mariadbd
01:22:12     2314    1105.0   40120.0  mariadbd

Qué significa: Cambios de contexto involuntarios masivos (nvcswch/s) usualmente significan que el scheduler está malabareando hilos/procesos ejecutables. Eso quema CPU sin completar trabajo proporcional.

Decisión: Reduce la concurrencia: aplica pooling, baja max connections, habilita/configura thread pool (MariaDB), ajusta el tamaño del pool (Postgres vía pgbouncer) y busca contención de bloqueos.

Task 5 (MariaDB): ¿Quién está ejecutando qué ahora mismo?

cr0x@server:~$ mysql -e "SHOW FULL PROCESSLIST\G" | head -n 30
*************************** 1. row ***************************
     Id: 81423
   User: app
   Host: 10.2.4.18:51214
     db: prod
Command: Query
   Time: 12
  State: Sending data
   Info: SELECT ... FROM orders WHERE customer_id=? ORDER BY created_at DESC LIMIT 50
*************************** 2. row ***************************
     Id: 81451
   User: app
   Host: 10.2.4.19:52108
     db: prod
Command: Query
   Time: 12
  State: Sorting result
   Info: SELECT ... FROM orders WHERE status='open' ORDER BY priority DESC

Qué significa: Estados como Sorting result y Time largo bajo pico suelen indicar índices faltantes o ineficaces; Sending data puede significar conjuntos de resultados grandes o lecturas lentas por parte del cliente.

Decisión: Captura las 2–3 consultas principales. Ejecuta EXPLAIN, valida índices y considera límites/timeout temporales para detener la hemorragia.

Task 6 (MariaDB): Comprueba contención InnoDB y presión de purge

cr0x@server:~$ mysql -e "SHOW ENGINE INNODB STATUS\G" | sed -n '1,120p'
=====================================
2025-12-30 14:24:51 0x7f1f6c0a9700 INNODB MONITOR OUTPUT
=====================================
Mutex spin waits 1203328, rounds 3821140, OS waits 44211
RW-shared spins 922110, rounds 1102100, OS waits 21011
History list length 884321
...

Qué significa: Altos spin waits y OS waits pueden indicar contención; una history list length muy grande sugiere que el purge está atrasado (transacciones largas o progreso de purge insuficiente), lo que puede inflar el trabajo para lecturas e índices.

Decisión: Encuentra transacciones de larga duración, reduce el tiempo de las transacciones y evalúa si la carga de escrituras o filas calientes están causando contención. Si la history list explota, busca la sesión que tiene un snapshot antiguo.

Task 7 (MariaDB): Verifica presión de hilos/conexiones rápidamente

cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Threads%'; SHOW VARIABLES LIKE 'max_connections';"
+-------------------+--------+
| Variable_name     | Value  |
+-------------------+--------+
| Threads_connected | 1850   |
| Threads_running   | 280    |
| Threads_created   | 992134 |
+-------------------+--------+
+-----------------+-------+
| Variable_name   | Value |
+-----------------+-------+
| max_connections | 4000  |
+-----------------+-------+

Qué significa: Miles de hilos conectados y un alto Threads_created a menudo correlacionan con CPU gastada en manejo de conexiones y planificación.

Decisión: Limita conexiones a lo que el host puede realmente programar. Implementa pooling. Si debes aceptar muchos clientes, usa un thread pool y mantiene Threads_running cerca del recuento de núcleos, no 10x.

Task 8 (PostgreSQL): Ver consultas activas y sus eventos de espera

cr0x@server:~$ psql -XAt -c "SELECT pid, state, wait_event_type, wait_event, now()-query_start AS age, left(query,120) FROM pg_stat_activity WHERE state<>'idle' ORDER BY age DESC LIMIT 10;"
4481|active|||00:00:17.182913|SELECT ... FROM orders WHERE customer_id=$1 ORDER BY created_at DESC LIMIT 50
4520|active|LWLock|buffer_mapping|00:00:12.504991|SELECT ... FROM events WHERE tenant_id=$1 AND ts>$2 ORDER BY ts DESC LIMIT 200
4602|active|Lock|transactionid|00:00:09.991221|UPDATE accounts SET balance=balance-$1 WHERE id=$2

Qué significa: Sin evento de espera y activo significa trabajo puro de CPU. LWLock waits pueden indicar contención interna (buffer mapping, WAL, etc.). Lock waits significan que estás atascado detrás de concurrencia, no de cómputo.

Decisión: Si la mayoría están activos por CPU: encuentra el SQL top y los planes. Si dominan las esperas por lock: reduce la duración de transacciones y arregla orden de bloqueos/filas calientes. Si hay hotspots de LWLock: revisa churn excesivo de shared buffers, alta concurrencia o patrones que golpean estructuras compartidas.

Task 9 (PostgreSQL): Identifica consultas costosas por CPU vía pg_stat_statements

cr0x@server:~$ psql -X -c "SELECT calls, round(total_exec_time::numeric,1) AS total_ms, round(mean_exec_time::numeric,2) AS mean_ms, rows, left(query,120) FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 8;"
 calls | total_ms | mean_ms |  rows  | left
-------+----------+---------+--------+------------------------------------------------------------
 93210 | 982344.6 |   10.54 | 186420 | SELECT ... FROM orders WHERE customer_id=$1 ORDER BY created_at DESC LIMIT 50
 11234 | 621990.2 |   55.36 |  11234 | SELECT ... FROM events WHERE tenant_id=$1 AND ts>$2 ORDER BY ts DESC LIMIT 200
  2011 | 318221.0 |  158.25 |   2011 | SELECT ... FROM line_items JOIN products ON ...

Qué significa: El tiempo total destaca lo que consume la caja, no lo que es “más lento una sola vez”. Bajo pico, los top por total son los que matan tu presupuesto de CPU.

Decisión: Toma las dos consultas principales, ejecuta EXPLAIN (ANALYZE, BUFFERS) fuera de pico o en una réplica, y arregla planes/índices primero.

Task 10 (PostgreSQL): Comprueba señales de autovacuum y presión de bloat

cr0x@server:~$ psql -X -c "SELECT relname, n_live_tup, n_dead_tup, last_autovacuum, last_autoanalyze FROM pg_stat_user_tables ORDER BY n_dead_tup DESC LIMIT 10;"
  relname  | n_live_tup | n_dead_tup |      last_autovacuum       |      last_autoanalyze
-----------+------------+------------+----------------------------+----------------------------
 orders    |   81203421 |   22190311 | 2025-12-29 03:12:44+00     | 2025-12-29 03:22:01+00
 events    |  190233442 |   80311220 |                            | 2025-12-28 22:10:08+00

Qué significa: Una tabla con muchos tuples muertos y sin autovacuum reciente es una trampa de CPU: escaneos y recorridos de índice hacen más trabajo por fila útil.

Decisión: Arregla el rendimiento de autovacuum (límites de cost, workers, IO), reduce transacciones largas y considera vacuum dirigido en ventanas controladas. Si es extremo, planifica una acción de mantenimiento (VACUUM FULL o reindex) con cuidado.

Task 11: Confirma si IO es el verdadero cuello de botella (CPU como síntoma)

cr0x@server:~$ iostat -x 1 3
Device            r/s     w/s   rKB/s   wKB/s  await  svctm  %util
nvme0n1         820.0  610.0  98240.0 110820.0  7.10  0.32  92.40

Qué significa: Alto %util y await creciente indica saturación de IO. Los picos de CPU pueden acompañar esto debido a churn de buffers y aumento de trabajo de consultas por fallos de caché.

Decisión: Si el almacenamiento está saturado, deja de “tocar la CPU”. Reduce IO: añade índices para evitar escaneos, aumenta la tasa de hit del caché, arregla bloat o escala almacenamiento/replicas.

Task 12: Valida presión de memoria e intercambio (killer silencioso de CPU)

cr0x@server:~$ free -h
               total        used        free      shared  buff/cache   available
Mem:           125Gi        96Gi       1.2Gi       2.1Gi        27Gi        18Gi
Swap:           16Gi       3.8Gi        12Gi

Qué significa: Uso de swap durante el pico puede convertir los gráficos de CPU en mentiras: la CPU está “ocupada” pero espera páginas de memoria que vienen del swap.

Decisión: Reduce la huella de memoria (menos conexiones, dimensiona buffers correctamente), detén work_mem desbocado (Postgres) y arregla patrones de consulta que causan grandes ordenamientos/hashes.

Task 13: Captura hotspots a nivel kernel con perf (cuando necesitas pruebas)

cr0x@server:~$ sudo perf top -p $(pgrep -o mariadbd)
Samples:  915 of event 'cycles', 4000 Hz, Event count (approx.): 145392102
  18.40%  mariadbd           [.] btr_cur_search_to_nth_level
  12.15%  mariadbd           [.] row_search_mvcc
   7.92%  libpthread-2.31.so [.] pthread_mutex_lock
   6.71%  mariadbd           [.] lock_rec_lock

Qué significa: Ver mutex locks alto en la pila sugiere contención; ver búsqueda B-tree dominando sugiere recorridos de índices intensos (posiblemente por fallos de caché o patrones de acceso ineficientes).

Decisión: Si mutex domina: reduce concurrencia y hotspots. Si B-tree domina: mejora índices, reduce búsquedas aleatorias e incrementa el hit rate de caché (buffer pool/shared buffers y working set).

Task 14 (PostgreSQL): Prueba un problema de plan con EXPLAIN (ANALYZE, BUFFERS)

cr0x@server:~$ psql -X -c "EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM orders WHERE customer_id=123 ORDER BY created_at DESC LIMIT 50;"
                                                         QUERY PLAN
-------------------------------------------------------------------------------------------------------------------
 Limit  (cost=0.56..42.89 rows=50 width=312) (actual time=0.212..1.903 rows=50 loops=1)
   Buffers: shared hit=108 read=24
   ->  Index Scan using orders_customer_created_idx on orders  (cost=0.56..71234.12 rows=84123 width=312)
         Index Cond: (customer_id = 123)
 Planning Time: 0.295 ms
 Execution Time: 1.982 ms

Qué significa: Lecturas vs hits te dicen el comportamiento del caché. Si ves un sequential scan con lecturas enormes donde esperabas un index scan, ese es tu pico de CPU con disfraz.

Decisión: Arregla índices, estadísticas (ANALYZE) y la forma de la consulta. Si esta consulta está en la ruta crítica, fórrala: índices estables, evita sensibilidad por parámetros si es necesario y añade guardarraíles.

Tres microhistorias corporativas desde el frente

Incidente 1: El outage causado por una suposición equivocada

Una compañía SaaS mediana migró un subsistema de facturación de MariaDB a PostgreSQL. La migración fue fluida en staging.
En producción todo fue bien—hasta la primera corrida de facturas de fin de trimestre. CPU al tope, timeouts de API, tickets de soporte.
El postmortem olía familiar: “Asumimos que la base de datos escalaría como la anterior.”

La suposición era que más conexiones significaban más throughput. En MariaDB habían aprendido, dolorosamente, a limitar conexiones y
usar pooling. Durante la migración, un nuevo servicio llegó con “solo abre una conexión por petición, está bien”, porque el equipo no quiso
lidiar con pooling por un plazo. Los backends de Postgres se multiplicaron. El cambio de contexto se volvió salvaje. La CPU ardió
y la latencia de consultas creció.

Lo más irritante: las consultas no eran lentas aisladamente. Eran lentas juntas. En pico, el overhead de planificación y las colas de bloqueo
amplificaron todo. Autovacuum también quedó atrás durante la tormenta de escrituras, añadiendo bloat y empeorando las lecturas,
justo cuando los portales de cliente estaban más ocupados.

La solución no fue exótica. Introdujeron un pooler de conexiones, pusieron un límite duro en sesiones activas por servicio y
convirtieron la retropresión de concurrencia en una característica de primera clase en lugar de un secreto vergonzoso. La CPU bajó lo suficiente
como para que los verdaderos cuellos de botella de consultas por fin se hicieran visibles, y esos sí se pudieron resolver.

Incidente 2: La “optimización” que salió mal

Una plataforma de e-commerce en MariaDB tenía un servicio de carrito con muchas escrituras. Alguien “optimizó” añadiendo tres índices
compuestos secundarios para soportar nuevas consultas de reporting. Los índices hicieron más rápidas las endpoints de reporting en pruebas.
Se celebró en Slack y luego se fusionó en silencio.

Llegó el día pico. Los picos de CPU aparecieron justo después de la ola matutina de tráfico. Las escrituras se ralentizaron primero, luego los checkouts
tuvieron timeouts. La BD no estaba limitada por IO. Estaba limitada por CPU en el motor de almacenamiento, haciendo mantenimiento de índices y luchando
contra la contención en las mismas hojas calientes que cada actualización del carrito tocaba. La plataforma había convertido accidentalmente una tabla
optimizada para escrituras en un impuesto de múltiples índices por escritura.

No tenían un plan de rollback. No podían soltar índices al instante sin arriesgar locks prolongados y más dolor. La mitigación de emergencia fue desviar
reporting a una réplica y cortar temporalmente funciones de reporting. Luego, en una ventana controlada, quitaron el peor índice y lo reemplazaron por otra
forma que soportaba la consulta necesaria pero reducía la amplificación de escrituras.

La lección no fue “no indexar”. Fue “indexar es una característica del camino de escritura”. Si no puedes explicar el coste de escritura de un índice bajo
concurrencia pico, no terminaste.

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

Una fintech con PostgreSQL tenía una regla: cada cambio de esquema debía incluir (a) el cambio esperado en el plan de consultas,
(b) una ruta de rollback, y (c) una consulta canaria de verificación. A nadie le encantaba la regla. Era papeleo con pager.

Un viernes, se desplegó un cambio aparentemente inofensivo: un nuevo predicado en una tabla grande. La consulta canaria se ejecutó automáticamente tras el deploy
y comparó la forma del plan con la línea base. Marcó un sequential scan sorpresa. Aún no en tráfico real—solo en la canaria.
El despliegue se pausó por sí mismo.

En el portátil del desarrollador se veía el índice siendo usado. Las estadísticas de producción estaban obsoletas; la distribución difería; el planner hizo
otra elección. El equipo ejecutó un ANALYZE en la tabla afectada en forma controlada, ajustó un único índice para coincidir con el nuevo predicado,
y volvió a ejecutar la canaria. El plan volvió a ser predecible.

No pasó nada dramático. Ese es el punto. El incidente que no sucedió es el mejor tipo—silencioso, ligeramente tedioso e invisible financieramente.

Datos interesantes y contexto histórico (8 puntos)

  1. La historia del fork de MySQL importa: MariaDB fue creada por los fundadores originales de MySQL tras la adquisición de Sun por Oracle, en parte para mantener un fork comunitario viable.
  2. InnoDB no siempre fue “el por defecto” en todas partes: Despliegues tempranos de MySQL usaban MyISAM; InnoDB se volvió dominante a medida que crecieron las cargas transaccionales y las expectativas de fiabilidad.
  3. El linaje de PostgreSQL es académico: Desciende del proyecto POSTGRES en UC Berkeley, con una larga tradición de ingeniería centrada en la corrección.
  4. MVCC es regalo y deuda: El diseño MVCC de PostgreSQL evita muchos bloqueos de lectura pero requiere vacuum continuo; si omites la limpieza, los costes de CPU aparecen después.
  5. Modelos hilo vs proceso moldean el comportamiento en picos: Los hilos de MariaDB hacen que el overhead por conexión sea más ligero que procesos completos, pero siguen siendo vulnerables a scheduling/contención a alta concurrencia.
  6. Postgres mejoró mucho en paralelismo con el tiempo: PostgreSQL moderno puede usar ejecución paralela en más casos, lo que puede mejorar throughput y también quemar más núcleos rápidamente si un mal plan se vuelve paralelo.
  7. MariaDB y MySQL divergieron en funcionalidades: Dependiendo de la versión, MariaDB puede ofrecer comportamientos del optimizador, opciones de thread pool e instrumentación diferentes a MySQL upstream, lo que cambia cómo se manifiestan los picos de CPU.
  8. Ambos ecosistemas aprendieron por las malas sobre “demasiadas conexiones”: La práctica operativa de pooling y back-pressure es ahora folklore compartido, porque la física convence.

Segunda broma (y la última, por las reglas): Afinar una base de datos durante un outage es como cambiar las ruedas de un avión en pleno vuelo.
Es posible, pero todos recordarán tu expresión facial.

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

1) Síntoma: CPU 90–100%, QPS plano, latencia en aumento

Causa raíz: Thrash por concurrencia (demasiadas conexiones/sesiones), overhead del scheduler, contención interna.

Solución: Limita conexiones; aplica pooling; reduce concurrencia en la app; añade back-pressure. En MariaDB, considera thread pool; en Postgres, usa un pooler y fija max_connections sensatos.

2) Síntoma: picos de CPU se alinean con un endpoint o job

Causa raíz: Una forma de consulta se volvió cara (regresión de plan, índice faltante, orden de joins malo).

Solución: Captura la consulta, ejecuta EXPLAIN/ANALYZE, añade/ajusta índice, arregla predicados sargables, refresca estadísticas. Pon un guardarraíl: statement timeout, límite de consulta o feature flag.

3) Síntoma: CPU alta, muchas esperas por locks, throughput colapsa

Causa raíz: Transacciones largas; filas calientes; inversión de orden de locks; patrones de “actualizar la misma fila”.

Solución: Acorta transacciones; arregla hotspots de contención (shard de contadores calientes, evita patrones de fila única “última”); asegura orden consistente de locks; agrupa escrituras razonablemente.

4) Síntoma (Postgres): CPU empeora gradualmente en días/semanas, no minutos

Causa raíz: Deuda de autovacuum y bloat. Las consultas hacen más trabajo por resultado a medida que se acumulan tuples muertos.

Solución: Ajusta autovacuum para las tablas pesadas; evita transacciones largas; monitoriza tuples muertos; programa mantenimiento; considera fillfactor o particionado para tablas con churn alto.

5) Síntoma (MariaDB): CPU alta durante escrituras, lag de replicación crece

Causa raíz: Amplificación de escrituras por índices/binlog; coste del commit; presión de fsync; páginas de índice calientes.

Solución: Quita índices secundarios innecesarios; optimiza batching de transacciones; valida settings de durabilidad; asegura storage rápido; ajusta group commit; mueve lecturas/reporting fuera del primary.

6) Síntoma: CPU alta, pero clientes ven “Sending data” / lecturas lentas

Causa raíz: Conjuntos de resultados grandes; paginación ineficiente; lentitud en el cliente que obliga al servidor a mantener recursos.

Solución: Limita tamaño de resultados; paginación por keyset; selecciona solo columnas necesarias; arregla patrones N+1; añade timeouts y max rows.

7) Síntoma: picos de CPU tras desplegar un cambio “menor”

Causa raíz: Cambio de plan debido a predicado nuevo o estadísticas obsoletas; nuevo índice que altera decisiones del planner; distribución de parámetros distinta.

Solución: Consultas canarias de planes; compara líneas base; analiza tablas afectadas; ajusta índices; si hace falta, reescribe la consulta para estabilidad de plan.

Listas de verificación / plan paso a paso para mantener la carga máxima

A. Durante el incidente (estabilizar primero)

  1. Detén la estampida: habilita back-pressure en la app; reduce tráfico no crítico; aplica rate-limit en endpoints pesados.
  2. Limita la concurrencia en el límite DB: pon pool de conexiones; baja temporalmente sesiones activas máximas; prioriza servicios críticos.
  3. Encuentra las dos formas de consulta principales: processlist/pg_stat_activity + estadísticas SQL top. No persigas la consulta número 20.
  4. Revisa acumulación de locks: si las esperas por locks dominan, matar consultas al azar no es estrategia—arregla el bloqueador.
  5. Valida IO: si el almacenamiento está pegado, las soluciones de CPU no sostendrán; reduce presión de IO primero.

B. En 48 horas (que no vuelva a ocurrir la próxima semana)

  1. Baselines de planes: captura EXPLAIN para consultas críticas y guárdalas con el servicio.
  2. Arregla salud de vacuum/purge: tuning de autovacuum en Postgres; control de transacciones largas y visibilidad de purge en MariaDB.
  3. Dimensiona índices correctamente: conserva lo que ayuda la ruta de lectura, elimina lo que grava la ruta de escritura en picos.
  4. Establece timeouts sensatos: statement timeouts, lock timeouts y deadlines en la app. Una consulta atascada es contagiosa.
  5. Prueba concurrencia pico: los load tests deben incluir conteos de conexión similares a producción y distribución de datos realista.

C. Decisiones de arquitectura que afectan “quién quema núcleos más rápido”

  • Si puedes poolar agresivamente: PostgreSQL se calma bajo pico porque limitas backends y proteges al scheduler.
  • Si no puedes controlar clientes: MariaDB con thread pool puede perdonar, pero aún necesitas límites; la concurrencia descontrolada eventualmente gana.
  • Si tu carga tiene mucho churn de escrituras: Postgres necesita disciplina en vacuum; MariaDB necesita disciplina en índices y diseño consciente de contención.
  • Si tienes unas pocas consultas analíticas complejas en OLTP: ambos quemarán CPU; aisla las cargas (réplicas, sistemas separados) en lugar de rezar.

Preguntas frecuentes

1) Entonces, ¿quién quema núcleos más rápido: MariaDB o PostgreSQL?

Bajo concurrencia descontrolada, ambos pueden incinerar CPU. MariaDB suele chocar con contención y límites de planificación de hilos cuando
le lanzas miles de conexiones activas. PostgreSQL suele quemar CPU más rápido cuando una consulta mala o la deuda de vacuum multiplican el trabajo
por consulta, y los planes paralelos pueden amplificar eso. El “ganador” es el que operas con más descuido.

2) ¿Por qué sube la CPU cuando el QPS no ha aumentado mucho?

Porque el trabajo por petición aumentó. Causas típicas: regresión de plan, aumento de fallos de caché, bloat/tuples muertos, o contención de locks
que alarga la duración de la consulta. Mismo QPS, más segundos-CPU por consulta, mayor utilización.

3) ¿Es malo siempre tener CPU alta?

No. CPU alta con latencia estable y throughput predecible puede estar bien; estás usando lo que pagaste. CPU alta con latencia en aumento y throughput
decreciente es el tipo malo: overhead de coordinación o trabajo amplificado.

4) ¿Añadir núcleos puede resolver picos de CPU?

A veces, pero es la solución menos fiable. Si estás limitado por ejecución pura de consultas y escalas linealmente, más núcleos dan margen.
Si estás limitado por contención, locks o cambio de contexto, más núcleos normalmente solo generan una factura mayor y el mismo incidente.

5) ¿Cuál es la mejor práctica única para prevenir colapso de CPU?

Pooling de conexiones con límites estrictos y back-pressure. Obliga al sistema a comportarse bajo pico. Sin eso, estás jugando a la lotería de la concurrencia.

6) Para PostgreSQL, ¿cómo sé que es deuda de autovacuum?

Busca aumento en n_dead_tup, escaneos lentos, incremento de lecturas de buffer y vacuum quedándose atrás (o bloqueado por transacciones largas).
La CPU irá subiendo a medida que las consultas tocan más páginas y hacen más checks de visibilidad.

7) Para MariaDB, ¿cuál es el indicador más rápido de dolor por conexiones/hilos?

Alto Threads_connected, alto cambio de contexto a nivel SO y Threads_running muy por encima del recuento de núcleos. Combínalo con
InnoDB status mostrando contención o una history list length larga para confirmación extra.

8) ¿Ayudan las réplicas a los picos de CPU?

Las réplicas de lectura ayudan cuando las lecturas dominan y puedes enrutarlas limpiamente. No arreglan la saturación de CPU en el path de escritura del primario.
Además, si tu primario está falto de CPU, la replicación puede atrasarse y las réplicas volverse obsoletas justo cuando las necesitas.

9) ¿Debería tunear parámetros del kernel primero?

Solo después de haber probado que el cuello de botella es de base de datos. El tuning del kernel puede ayudar (scheduler, red, colas de IO), pero no es sustituto
de arreglar concurrencia, planes de consulta y salud de vacuum/purge.

10) ¿Qué pasa si la CPU es alta pero perf muestra mayoritariamente funciones de mutex/lock?

Eso es contención. Estás pagando por coordinación, no por cómputo. La solución es reducir concurrencia y quitar hotspots:
acorta transacciones, shardea contadores calientes, rediseña patrones de “fila única” y limita sesiones activas.

Conclusión: pasos prácticos siguientes

Bajo carga máxima, MariaDB y PostgreSQL no “disparan” picos de CPU al azar. Siguen patrones. MariaDB tiende a castigar la concurrencia
descontrolada y la amplificación en el camino de escritura; PostgreSQL tiende a castigar la negligencia del vacuum y las sorpresas de plan.
Si quieres menos picos, deja de tratar la CPU como el problema y trátala como el recibo.

Pasos siguientes que rinden inmediatamente:

  1. Impón límites duros a la concurrencia DB (pooling + caps) y haz que la app los respete.
  2. Instrumenta el SQL top por tiempo total y alerta en cambios súbitos en la forma del plan o la distribución de latencia.
  3. Mantén la limpieza: disciplina de autovacuum en Postgres; control de transacciones largas y visibilidad de purge en MariaDB.
  4. Convierte la indexación en una decisión de producción: cada índice nuevo debe justificar su coste de escritura en picos.
  5. Practica la guía en un test de carga en staging. La primera vez que ejecutes estos comandos no debería ser durante un outage.
← Anterior
Firewall de Proxmox te bloqueó: restaurar SSH/Web UI desde la consola sin pánico
Siguiente →
Debian/Ubuntu: «Funciona en LAN, falla en WAN» — comprobaciones de enrutamiento/NAT que revelan la causa (caso #85)

Deja un comentario