La mayoría de las “optimizaciónes de rendimiento” de bases de datos empiezan en el lugar equivocado: un archivo de configuración. Alguien recuerda un post del blog, gira un ajuste, reinicia producción y espera que los gráficos dejen de gritar. A veces funciona. A menudo solo traslada el dolor a otro subsistema y alarga el postmortem.
La forma más rápida de ganar debates sobre rendimiento MariaDB vs MySQL es aburrida: identifica el cuello de botella antes de cambiar nada. Esta lista de comprobación hace eso. Es el mismo enfoque que uso cuando una ruta de ingresos está agotándose y la única “reproducción” es “a veces va lento”.
La lista de comprobación: localizar el cuello de botella, luego ajustar
MariaDB y MySQL comparten mucho ADN, especialmente si vives principalmente en el mundo InnoDB. Por eso “cuál es más rápido” suele ser la pregunta equivocada. La pregunta correcta es: ¿qué está limitando actualmente el rendimiento o la latencia en este host? CPU. E/S. Bloqueos. Memoria. Red. Replicación. O el clásico: una consulta patológica que arrastra todo lo demás al barro.
Este es el modelo mental que te mantiene honesto:
- Cuello de botella de rendimiento: estás saturando algún recurso (CPU, ancho de banda de disco, mutexes). Síntomas: QPS se estabiliza mientras la carga aumenta; la latencia puede o no dispararse.
- Cuello de botella de latencia: algo bloquea ocasionalmente (picos de fsync, bloqueos, fallos del buffer pool, estancamiento en la aplicación de replicación). Síntomas: la cola de latencia sube; “a veces va lento”.
- Cuello de botella por encolamiento: una etapa single-threaded o serializada (flush del redo log, hilo SQL de replicación, metadata lock). Síntomas: una cola crece; todos esperan su turno.
Cuando estás bajo presión, los ajustes parecen productivos porque son concretos. Pero los ajustes son decisiones de downstream. Si no conoces el cuello de botella, estás cambiando variables al azar y declarando victoria cuando el próximo incidente ocurre otro día.
Una regla operativa: no ajustes hasta que puedas decir, en una frase, “el cuello de botella es X, probado por Y”. Si no puedes, no estás ajustando; estás apostando.
Y sí, seguirás ajustando. Pero lo harás con recibos.
Broma corta #1: “Ajustamos MySQL hasta que fue rápido.” Es adorable. El balanceador de carga lo recuerda.
Hechos e historia que siguen importando en producción
Algo de contexto ayuda porque explica por qué difieren los valores por defecto, por qué el comportamiento te sorprende y por qué el “tip de MySQL” de alguien rompe en MariaDB (o viceversa).
- MariaDB se bifurcó de MySQL en 2009 tras preocupaciones por la adquisición de MySQL por Oracle. Esto no es trivia: es por qué las suposiciones del ecosistema divergen con el tiempo.
- MariaDB temprana se comercializó agresivamente como “reemplazo drop-in”. Operacionalmente, es mayormente cierto a nivel SQL—pero el comportamiento de rendimiento puede divergir en los detalles (optimizador, thread pool, características de replicación).
- MySQL 5.6/5.7 convirtió a InnoDB en el centro de gravedad y mejoró performance_schema e instrumentación sustancialmente. Si todavía lo tratas como una caja negra, estás dejando diagnóstico gratis sobre la mesa.
- MySQL 8.0 eliminó el query cache (notorio por la contención de mutex). Si tu estate de MariaDB todavía usa query cache, eso es un artefacto histórico con consecuencias modernas.
- MariaDB divergió con características como Aria, ColumnStore e integración con Galera (en muchas distribuciones). No son solo características; cambian los modos de falla y los ajustes operativos.
- La revisión del diccionario de datos en MySQL 8.0 cambió metadata y algunas realidades de upgrade/rollback. “Funcionó en 5.7” no es un plan.
- Ambos dependen mucho del comportamiento de E/S de Linux (fsync, vaciado de páginas sucias, scheduler). Muchos incidentes “de base de datos” son solo el kernel haciendo lo que le pediste, no lo que querías.
- La replicación evolucionó de forma diferente: MySQL se enfocó en GTIDs, replicación multihilo, group replication; MariaDB tiene su propia implementación de GTID y a menudo ergonomías de replicación diferentes. Misma palabra, diferentes trampas.
Además: las conversaciones sobre rendimiento envejecen mal. Una guía de tuning escrita para MySQL 5.5 en discos giratorios puede ser activamente dañina en MySQL 8.0 sobre NVMe con cgroups y vecinos ruidosos.
Un requisito de cita, idea parafraseada: la esperanza no es una estrategia.
— a menudo atribuido en círculos de operaciones; úsalo como principio listo para el pager, no como eslogan.
Guía de diagnóstico rápido (primero/segundo/tercero)
Este es el orden para “entrar a una sala en llamas”. Está diseñado para velocidad, no elegancia. El objetivo es identificar si estás limitado por CPU, E/S, bloqueos o replicación en minutos.
Primero: prueba qué tipo de cuello de botella tienes
- ¿Está saturado el host? CPU, presión de memoria, IO wait, latencia de disco, errores de red.
- ¿Está esperando la base de datos? Esperas por bloqueos, fsync/redo, fallos del buffer pool, planificación de hilos, tormentas de conexiones.
- ¿Está la base de datos haciendo algo costoso? Escaneos completos, planes malos, ordenación en disco, tablas temporales, índices faltantes.
Segundo: identifica al delincuente principal, no el promedio
- Consultas top por tiempo total (no por cuenta).
- Principales esperas (bloqueos, E/S, flush, metadata locks).
- Tablas más calientes (filas calientes, índices faltantes, alto churn).
Tercero: decide mitigar, arreglar o escalar
- Mitigar ahora: matar consultas desbocadas, reducir carga, aumentar el pool con cuidado, reducir concurrencia, mover lecturas a réplicas.
- Arreglar después: cambios de índice, reescritura de consultas, cambios de esquema, particionamiento, corregir niveles de aislamiento.
- Escalar cuando corresponda: más CPU para cargas CPU-bound, almacenamiento más rápido para E/S-bound, réplicas para fan-out de lecturas, shardear solo si realmente lo necesitas.
Consejo de velocidad: no “muestres” mirando promedios. Busca colas y latencia en la cola. Los promedios son lo que le cuentas a management. Las colas son lo que le cuentas al on-call.
Tareas prácticas (comandos, salidas, decisiones)
Estas son tareas ejecutables que puedes hacer en un host Linux de BD y dentro de la base de datos. Cada una incluye (a) un comando, (b) qué significa la salida, y (c) la decisión que tomas. Esa última parte es la diferencia entre monitoreo y operaciones.
Task 1: confirma qué motor y versión estás ejecutando realmente
cr0x@server:~$ mysql -e "SELECT VERSION() AS version, @@version_comment AS comment, @@version_compile_machine AS arch;"
+------------------------+------------------------------+------+
| version | comment | arch |
+------------------------+------------------------------+------+
| 10.6.16-MariaDB-1:... | MariaDB Server | x86_64 |
+------------------------+------------------------------+------+
Significado: No depures “MySQL” cuando estás ejecutando MariaDB (o viceversa). Las diferencias de versión determinan la instrumentación disponible y el comportamiento del optimizador.
Decisión: Elige las herramientas correctas: los flujos intensivos de performance_schema están más maduros en MySQL 8.0; MariaDB puede requerir contadores de estado y equivalentes de sys schema diferentes.
Task 2: chequeo rápido de presión del host (CPU, IO wait, 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
3 1 0 81232 64312 987654 0 0 120 980 900 2100 25 10 45 20 0
8 2 0 79012 64320 987120 0 0 110 1600 1100 3200 35 12 33 20 0
Significado: Un r alto sugiere contención de CPU; un wa alto sugiere espera por E/S; un b sostenido indica tareas bloqueadas.
Decisión: Si wa es consistentemente alto y la latencia es inestable, ve directo a tareas de latencia de disco. Si r es alto con wa bajo, enfócate en CPU de consultas y contención.
Task 3: mide la latencia de disco, no solo el throughput
cr0x@server:~$ iostat -x 1 3
Device r/s w/s rkB/s wkB/s await svctm %util
nvme0n1 1200 9000 48000 320000 18.5 0.3 92.0
Significado: await es el tiempo que las solicitudes pasan esperando + siendo servidas. Un await alto con alta utilización significa que el dispositivo está saturado o que algo upstream está encolando.
Decisión: Si los picos de await coinciden con la latencia de DB, estás limitado por E/S. Mitiga reduciendo picos de escritura (checkpoint/flush), mejorando el buffer pool o moviendo a almacenamiento más rápido. Si await es bajo pero la BD va lenta, el cuello está en otro lado.
Task 4: encuentra los hilos MySQL/MariaDB más ocupados a nivel OS
cr0x@server:~$ top -H -p $(pidof mysqld)
top - 12:01:22 up 34 days, 2 users, load average: 9.10, 8.80, 7.90
Threads: 210 total, 8 running, 202 sleeping
%Cpu(s): 70.0 us, 10.0 sy, 0.0 ni, 20.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
12345 mysql 20 0 12.3g 8.1g 0.0g R 180.0 52.0 12:11.04 mysqld
Significado: Si ves unos pocos hilos al máximo, puede que tengas consultas CPU-bound o contención de mutex. Si la CPU está baja pero la latencia alta, céntrate en esperas/bloqueos/E/S.
Decisión: Si la CPU está saturada, ve a perfilado de consultas y planes. Si no, ve a esperas por bloqueos y E/S dentro de InnoDB.
Task 5: identificar consultas desbocadas inmediatamente
cr0x@server:~$ mysql -e "SHOW FULL PROCESSLIST\G" | sed -n '1,80p'
*************************** 1. row ***************************
Id: 123
User: app
Host: 10.0.2.14:51234
db: prod
Command: Query
Time: 88
State: Sending data
Info: SELECT ... FROM orders JOIN order_items ...
Significado: Time es cuánto tiempo lleva ejecutándose. El State insinúa lo que hace (no perfectamente). Un montón de “Sending data” de larga duración suele significar scans/joins grandes.
Decisión: Si una sola consulta domina y afecta al usuario, considera matarla y limitar al cliente. Luego captura la consulta para EXPLAIN y trabajo de índices.
Task 6: encontrar contención de bloqueos rápido (InnoDB)
cr0x@server:~$ mysql -e "SHOW ENGINE INNODB STATUS\G" | sed -n '1,140p'
TRANSACTIONS
------------
Trx id counter 987654321
Purge done for trx's n:o < 987654000 undo n:o < 0 state: running
History list length 12045
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 987654310, ACTIVE 92 sec
2 lock struct(s), heap size 1136, 1 row lock(s)
MySQL thread id 123, OS thread handle 1400, query id 7777 10.0.2.14 app updating
UPDATE accounts SET balance=balance-? WHERE id=?
Significado: Transacciones “ACTIVE” largas y un History list length grande suelen correlacionar con retraso de purge, crecimiento de undo y síntomas secundarios como “consultas aleatorias lentas”.
Decisión: Si ves una transacción de larga duración reteniendo locks, puede que necesites matarla, arreglar el alcance de transacciones en la aplicación o bajar el aislamiento donde sea seguro. También investiga por qué el purge no puede seguir el ritmo (a menudo por lectores largos).
Task 7: verifica la presión del buffer pool e indicadores de hit ratio
cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_reads'; SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read_requests';"
+-------------------------+-----------+
| Variable_name | Value |
+-------------------------+-----------+
| Innodb_buffer_pool_reads| 124009876 |
+-------------------------+-----------+
+----------------------------------+-------------+
| Variable_name | Value |
+----------------------------------+-------------+
| Innodb_buffer_pool_read_requests | 98012345678 |
+----------------------------------+-------------+
Significado: Innodb_buffer_pool_reads son lecturas físicas. Si esto sube rápidamente respecto a las read requests, estás fallando el caché y pagando latencia de disco.
Decisión: Si las lecturas físicas son altas y la latencia de disco es significativa, aumenta el buffer pool (si la RAM lo permite) o reduce el working set (índices, patrones de consulta). Si el buffer pool está bien, no lo toques.
Task 8: detectar volcados a tablas temporales (ordenaciones/joins que usan disco)
cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Created_tmp%tables';"
+-------------------------+----------+
| Variable_name | Value |
+-------------------------+----------+
| Created_tmp_disk_tables | 9123456 |
| Created_tmp_tables | 12345678 |
+-------------------------+----------+
Significado: Una alta proporción de tablas temporales en disco sugiere límites de memoria demasiado bajos para sorts/joins o consultas inherentemente propensas a volcar (índices faltantes, GROUP BY enormes).
Decisión: Si las temporales en disco correlacionan con endpoints lentos, arregla los planes de consulta primero (índices), luego ajusta los límites de memoria para tablas temporales con cuidado. Límites más grandes también pueden causar explosiones de memoria bajo concurrencia.
Task 9: comprobar presión de redo log y flushing (dolor de fsync)
cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Innodb_os_log_fsyncs'; SHOW GLOBAL STATUS LIKE 'Innodb_log_waits';"
+----------------------+----------+
| Variable_name | Value |
+----------------------+----------+
| Innodb_os_log_fsyncs | 77881234 |
+----------------------+----------+
+------------------+-------+
| Variable_name | Value |
+------------------+-------+
| Innodb_log_waits | 12345 |
+------------------+-------+
Significado: Innodb_log_waits indica esperas por espacio en el log buffer / flush. Combinado con altas tasas de fsync y latencia de E/S, esto apunta a cuellos de botella de escritura.
Decisión: Si los log waits suben, estás golpeando un techo de commit/flush. Mitiga con almacenamiento más rápido, reduciendo la tasa de transacciones, agrupando escrituras o ajustando durabilidad solo con aceptación explícita del negocio.
Task 10: comprobar retraso de replicación y si es E/S, aplicación SQL o bloqueos
cr0x@server:~$ mysql -e "SHOW SLAVE STATUS\G" | egrep -i "Seconds_Behind_Master|Slave_IO_Running|Slave_SQL_Running|Relay_Log_Space"
Seconds_Behind_Master: 420
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
Relay_Log_Space: 987654321
Significado: El hilo IO está bien, el hilo SQL está en ejecución, pero el lag es alto y los relay logs se acumulan. La réplica puede traer los logs pero no aplicarlos lo suficientemente rápido.
Decisión: Investiga cuellos de botella en la aplicación: aplicación single-threaded, filas calientes, DDL, transacciones largas o almacenamiento lento. Considera replicación multihilo donde esté soportada y sea segura, y corrige patrones de consulta/transacción que serializan la aplicación.
Task 11: verificar tormentas de conexiones y planificación de hilos
cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Threads%'; SHOW GLOBAL STATUS LIKE 'Connections';"
+-------------------+-------+
| Variable_name | Value |
+-------------------+-------+
| Threads_connected | 980 |
| Threads_running | 64 |
| Threads_cached | 0 |
+-------------------+-------+
+---------------+--------+
| Variable_name | Value |
+---------------+--------+
| Connections | 987654 |
+---------------+--------+
Significado: Muchas conexiones con cache bajo pueden significar churn de conexiones (apps sin pooling) o cache de hilos demasiado bajo.
Decisión: Arregla la aplicación primero: activa pooling, reutiliza conexiones, limita la concurrencia. Solo entonces ajusta el thread cache/pools; de lo contrario solo permitirás que la app te dañe más rápido.
Task 12: comprobar tamaño de tablas/índices respecto a la memoria para predecir fallos de caché
cr0x@server:~$ mysql -e "SELECT table_schema, SUM(data_length+index_length)/1024/1024/1024 AS gb FROM information_schema.tables GROUP BY table_schema ORDER BY gb DESC;"
+--------------+--------+
| table_schema | gb |
+--------------+--------+
| prod | 820.12 |
| mysql | 0.05 |
+--------------+--------+
Significado: Si tu dataset activo son cientos de GB y tu buffer pool es 32GB, no vas a cachearte. Las lecturas serán de disco. Los planes deben ser selectivos.
Decisión: O provees RAM para el working set o diseñas consultas/índices para evitar escaneos. Si ninguna es posible, deriva lecturas a réplicas o acepta que la latencia en la cola está determinada por el almacenamiento.
Task 13: detectar acumulaciones de metadata locks (DDL y ALTER “inofensivos”)
cr0x@server:~$ mysql -e "SHOW FULL PROCESSLIST;" | awk '$6 ~ /Waiting/ || $7 ~ /metadata/ {print}'
234 app 10.0.2.21:49910 prod Query 35 Waiting for table metadata lock ALTER TABLE users ADD COLUMN ...
Significado: Los metadata locks bloquean consultas que tocan la tabla. Un DDL puede congelar una ruta de tabla caliente si espera detrás de una transacción larga.
Decisión: Pausa el DDL, encuentra y termina la transacción larga, o usa técnicas de cambio de esquema en línea. Además: programa cambios de esquema como programas riesgo, no como almuerzos.
Task 14: inspeccionar el slow query log rápidamente (si está habilitado)
cr0x@server:~$ sudo tail -n 20 /var/log/mysql/slow.log
# Time: 2025-12-31T11:55:22.123456Z
# Query_time: 12.400 Lock_time: 0.000 Rows_sent: 50 Rows_examined: 50000000
SELECT ... FROM events WHERE customer_id=... ORDER BY created_at DESC LIMIT 50;
Significado: Rows examined es el indicador. 50M examinadas para devolver 50 filas es un problema de índice o plan, no de “más buffer pool”.
Decisión: Añade/arregla el índice (probablemente (customer_id, created_at)), valida con EXPLAIN y considera reescribir la consulta. No “tunes” alrededor de un índice faltante.
Task 15: validar el plan con EXPLAIN y hacerlo legible
cr0x@server:~$ mysql -e "EXPLAIN SELECT id FROM events WHERE customer_id=42 ORDER BY created_at DESC LIMIT 50\G"
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: events
type: ALL
possible_keys: idx_customer_created
key: NULL
rows: 50000000
Extra: Using where; Using filesort
Significado: type: ALL y filesort en una tabla grande indica un escaneo completo + ordenación. El índice existe pero no se usa (orden incorrecto, baja selectividad, collation distinta, función en la columna o estadísticas desactualizadas).
Decisión: Arregla la consulta o el índice. Si las estadísticas están obsoletas, ejecuta analyze. Si el optimizador se equivoca, considera hints solo como último recurso y con tests de regresión.
Task 16: comprobar deadlocks (no son raros; solo están poco reportados)
cr0x@server:~$ mysql -e "SHOW ENGINE INNODB STATUS\G" | sed -n '/LATEST DETECTED DEADLOCK/,+60p'
LATEST DETECTED DEADLOCK
------------------------
*** (1) TRANSACTION:
TRANSACTION 987654400, ACTIVE 2 sec updating or deleting
...
WE ROLL BACK TRANSACTION (1)
Significado: Los deadlocks son errores de concurrencia, no “fallos de la base de datos”. Suelen ser causados por orden de actualización inconsistente o transacciones demasiado grandes.
Decisión: Arregla el orden de las operaciones en la aplicación y el alcance de transacciones. Luego asegúrate de que la lógica de reintento sea correcta y limitada. Reintentar indefinidamente es solo cosplay de deadlock.
Task 17: medir el sistema de ficheros y opciones de montaje que afectan a fsync
cr0x@server:~$ mount | egrep ' /var/lib/mysql | /data '
/dev/nvme0n1p2 on /var/lib/mysql type ext4 (rw,noatime,data=ordered)
Significado: El sistema de ficheros y las opciones de montaje afectan actualizaciones de metadata y barreras de escritura. No necesitas ajustes exóticos; necesitas opciones consistentes y probadas.
Decisión: Si ves parones por fsync, valida firmware de almacenamiento, salud del filesystem y logs del kernel antes de tocar los ajustes de durabilidad de la base de datos.
Task 18: verificar que la base de datos no esté intercambiando (swap)
cr0x@server:~$ free -h
total used free shared buff/cache available
Mem: 31Gi 28Gi 0.8Gi 0.2Gi 2.2Gi 1.1Gi
Swap: 4Gi 1.9Gi 2.1Gi
Significado: Si hay swap en uso en un host BD, la latencia se vuelve impredecible. Incluso “un poco de swap” puede significar que páginas importantes se expulsan.
Decisión: Reduce el footprint de memoria (buffer pool demasiado grande, buffers por conexión demasiado altos), añade RAM o limita la concurrencia. No normalices swap en bases de datos a menos que disfrutes novelas de misterio.
Listas de comprobación / plan paso a paso
Esta es la lista operativa que quiero en la pared (o en el runbook). Es el conjunto mínimo de pasos que te lleva de forma fiable a una respuesta sin tuning por folklore.
Checklist A: triage de 10 minutos (producción en llamas)
- Confirma el alcance: ¿un endpoint, un tenant, o todo? Toma una ventana de tiempo y tasas de error de tu monitorización de aplicación.
- Saturación del host: ejecuta
vmstatyiostat -x. Decide: CPU-bound o IO-bound. - Cola de la base de datos:
SHOW FULL PROCESSLISTpara ver si hay muchos estados “Waiting for …” o pocos procesos de larga duración. - Bloqueos:
SHOW ENGINE INNODB STATUSy busca transacciones largas, esperas por locks, deadlocks. - Replicación (si aplica): comprueba lag. Si las lecturas las sirven réplicas, el lag es un bug visible por el usuario.
- Mitigación inmediata: mata la peor consulta, reduce la carga, pausa el DDL o redirige tráfico de lectura temporalmente.
- Captura evidencia: fragmento del slow log, EXPLAIN, contadores de estado relevantes antes de reiniciar nada.
Checklist B: diagnóstico de 1 hora (encontrar el verdadero cuello de botella)
- Consultas top por tiempo total: usa agregación de slow log o vistas de performance_schema (si están disponibles). Enfócate en tiempo total, no solo en la consulta más larga.
- Validación de planes: EXPLAIN de los principales ofensores. Busca escaneos completos, filesorts, temporales, mal orden de joins.
- Clasificación de esperas: identifica si las esperas son mayoritariamente E/S, bloqueos o CPU. Si es MySQL 8.0, performance_schema puede mostrar esto claramente; en MariaDB, confía más en InnoDB status + contadores de estado.
- Chequeo de forma de datos: tamaños de tablas, tamaños de índices, distribuciones sesgadas, filas calientes.
- Chequeo de concurrencia: hilos en ejecución, conexiones, alcance de transacciones, tamaños de lotes.
- Chequeo de almacenamiento: logs del kernel, latencia de disco, comportamiento del filesystem, picos de writeback.
- Decidir la clase de solución: consulta/índice, comportamiento de la app, almacenamiento o capacidad.
Checklist C: secuencia segura de tuning (solo después de conocer el cuello de botella)
- Primero arreglos de consulta/índice (mayor ROI, menor riesgo sistémico). Hazlos testeables y reversibles.
- Segundo controles de concurrencia (pooling, colas, límites). Prevén hordas simultáneas.
- Tercero dimensionamiento de memoria (buffer pool, buffers por conexión). Observa swap y OOM.
- Por último ajustes de E/S/durabilidad y solo con aceptación explícita del riesgo. Puedes comprar rendimiento con durabilidad; no puedes recuperar la durabilidad tras una pérdida de datos.
Broma corta #2: Apagar la durabilidad es como quitar el detector de humo porque hace ruido. Dormirás genial justo hasta que no.
Tres mini-historias del mundo corporativo (realistas, anonimizadas)
Mini-historia 1: el incidente causado por una suposición equivocada
Una empresa SaaS mediana migró un servicio central de MySQL a MariaDB porque el paquete del proveedor lo hacía fácil y la app “solo usa SQL estándar”. El equipo hizo una migración de esquema cuidadosa, ejecutó tests de integración y declaró el éxito. En staging la latencia parecía bien. Producción fue otro animal.
La suposición equivocada fue sutil: asumieron que su carga estaba limitada por consultas. No lo estaba. En producción corrían una carga de escritura de alta concurrencia con picos periódicos—eventos de facturación al final de la hora—cada transacción tocando unas pocas filas calientes. La app usaba transacciones de larga duración con lecturas extra “por seguridad”. Esas lecturas no eran seguras. Retenían locks y aumentaban la presión de purge.
En un día empezaron a ver consultas “aleatorias” lentas. Luego el lag de replicación creció. Luego una réplica de lectura fue promovida durante una ventana de mantenimiento separada y el sistema se vino abajo: el nuevo primario tenía un history list length grande, el almacenamiento tenía picos periódicos de fsync, y la app lo amplificó abriendo nuevas conexiones durante los reintentos.
Inicialmente culparon la elección del motor. Era una conclusión conveniente y equivocada. El verdadero culpable fue el alcance de las transacciones y la contención por filas calientes. Cuando acortaron transacciones, añadieron un índice compuesto faltante e introdujeron reintentos acotados, tanto MariaDB como MySQL se comportaron bien. El motor importaba menos que la disciplina de la carga de trabajo.
La conclusión del postmortem fue directa: “Drop-in replacement” trata de corrección, no de predictibilidad de rendimiento. Añadieron una puerta de migración: demostrar la clase de cuello de botella bajo carga similar a producción antes de cambiar de motor.
Mini-historia 2: la optimización que salió mal
Una plataforma de retail tenía un dolor clásico: una consulta de reporte lenta durante horario comercial. Alguien propuso una ganancia fácil: aumentar buffers por conexión y tamaños de tabla temporal para que las ordenaciones no volcaran a disco. En papel era razonable. En la práctica desencadenó una cascada.
El cambio se aplicó un viernes (no malicioso, solo optimista). En el pico del tráfico, el conteo de conexiones subió por un fallo en el pooler upstream. Cada conexión ahora podía asignar más memoria durante sorts y joins. La presión de memoria subió, el kernel empezó a hacer swapping y de repente cada consulta—rápida y lenta—se volvió más lenta. La latencia se volvió inestable y luego se aplanó.
Revirtieron rápido, pero la lección quedó: los ajustes de memoria se multiplican por la concurrencia. “Buffers más grandes” no son un almuerzo gratis; es un buffet donde cada hilo coge una bandeja. Si tu app invita a 1.000 invitados, necesitas un portero, no una cocina más grande.
Lo arreglaron de la forma aburrida. Primero repararon el pooling de conexiones y pusieron límites sensatos de concurrencia. Luego añadieron un índice dirigido y reescribieron la consulta del reporte para preagregar. Después de eso, un ajuste moderado de tablas temporales ayudó porque ya no competía con swap.
Mini-historia 3: la práctica aburrida pero correcta que salvó el día
Un equipo de servicios financieros corría MySQL y MariaDB en distintas unidades de negocio. La infra era desordenada, pero su higiene operativa era excepcional. Cada host de base de datos tenía un script estándar de “captura de evidencia”: recogía processlist, InnoDB status, contadores clave, estadísticas de I/O y un trozo de slow logs, y lo almacenaba con timestamps.
Una tarde recibieron una ráfaga de timeouts. El on-call ejecutó el script dos veces, con dos minutos de diferencia. La segunda ejecución mostró un aumento en Innodb_log_waits y un salto repentino en await de disco. El processlist mostraba muchas escrituras cortas esperando el commit. Esto no era un problema de plan de consulta; era un problema de latencia de escritura.
Porque tenían los datos, no perdieron tiempo debatiendo diferencias entre motores. Revisaron logs del kernel y encontraron reinicios a nivel de dispositivo NVMe. La capa RAID/controladora lo enmascaraba lo justo para parecer “lentitud de base de datos”. Hicieron failover a un standby y reemplazaron el dispositivo el mismo día.
La solución no fue ingeniosa. Fue evidencia repetible. Su “script aburrido” evitó una semana de tuning basado en supersticiones y una discusión sobre qué fork de MySQL era “más estable”.
Errores comunes: síntoma → causa raíz → solución
Esta es la parte donde los sistemas en producción confiesan sus pecados. Cada ítem es específico porque el consejo vago es cómo se reproducen los incidentes.
1) Síntoma: “CPU alta, pero las consultas son simples”
Causa raíz: concurrencia y contención. Muchas consultas “simples” todavía pueden pelear por filas calientes, índices o mutexes internos. Otro culpable común: comparaciones con collation ineficiente o funciones sobre columnas indexadas que impiden el uso del índice.
Solución: Identifica las consultas top por tiempo total, confirma el uso de índices con EXPLAIN y reduce la concurrencia desde la app. Considera añadir o remodelar índices para evitar puntos calientes.
2) Síntoma: “Picos de latencia cada pocos minutos, luego se recupera”
Causa raíz: tormentas de flush de E/S, comportamiento de checkpointing o fallos del almacenamiento. También: jobs en background o DDL que arrancan por horario.
Solución: Correlaciona los picos con iostat await, Innodb_os_log_fsyncs y comportamiento de páginas sucias. Mueve ráfagas fuera de pico, agrupa escrituras y verifica la salud del almacenamiento.
3) Síntoma: “El lag de la réplica crece, pero la CPU es baja”
Causa raíz: la aplicación del SQL en la réplica está serializada por filas calientes, transacciones grandes, DDL o configuración single-threaded. CPU baja no significa saludable; puede significar espera por E/S o bloqueos.
Solución: Inspecciona el crecimiento de relay logs, busca transacciones largas, habilita/ajusta replicación multihilo donde aplique y reduce el tamaño de transacciones en el primario.
4) Síntoma: “Muchas conexiones, pero Threads_running no es tan alto”
Causa raíz: churn de conexiones y overhead de recursos; la app no está usando pooling, o los health checks son demasiado agresivos. También: max_connections puesto alto para “arreglar” errores, creando un radio de impacto mayor.
Solución: Arregla el pooling, limita la concurrencia, usa un proxy/pooler si hace falta y mantén max_connections realista para que la sobrecarga falle rápido en lugar de lentamente.
5) Síntoma: “Los deadlocks empezaron después de añadir un índice”
Causa raíz: rutas de acceso cambiadas alteraron el orden de adquisición de locks. El índice no creó deadlocks; reveló bugs de ordenamiento bajo concurrencia.
Solución: Estandariza el orden de actualizaciones en el código, reduce el alcance de transacciones y asegúrate de que los reintentos por deadlock estén acotados. Usa cláusulas WHERE consistentes y evita updates por rango en tablas calientes.
6) Síntoma: “ALTER TABLE cuelga para siempre”
Causa raíz: metadata lock esperando detrás de una transacción larga, a menudo una lectura “inofensiva” dejada abierta por un pool de conexiones.
Solución: Encuentra el bloqueador vía processlist/InnoDB status, mata o termina la transacción larga y usa estrategias de cambio de esquema online con guardarraíles operativos.
7) Síntoma: “La utilización de IO es baja, pero await es alto”
Causa raíz: encolamiento en la pila de almacenamiento, throttling, vecino ruidoso o latencia inyectada por firmware/controladora. %util puede mentir en dispositivos modernos y entornos virtualizados.
Solución: Confía en métricas de latencia, revisa logs del kernel, valida salud del dispositivo y considera aislar E/S (volúmenes dedicados, mejor QoS) antes de tunear InnoDB.
8) Síntoma: “Las consultas se pusieron más lentas después de actualizar”
Causa raíz: regresiones del optimizador, valores por defecto cambiados, comportamiento de estadísticas distinto o modos SQL diferentes. MariaDB y MySQL divergen aquí más de lo que se admite.
Solución: Captura EXPLAIN antes/después, refresca estadísticas, fija consultas críticas con hints probados si es necesario y ejecuta tests de regresión basados en carga — no solo unitarios.
MariaDB vs MySQL: dónde realmente importan las diferencias
Si estás diagnosticando cuellos de botella, la elección del motor importa principalmente donde cambia la instrumentación, el comportamiento del optimizador, el control de concurrencia y la semántica de replicación. Aquí cómo pensarlo sin convertirlo en una guerra religiosa.
Instrumentación: ¿puedes ver lo que sucede?
El performance schema de MySQL 8.0 es un punto fuerte para diagnóstico en producción: esperas, etapas, digests de sentencias y contención se pueden visibilizar. MariaDB también tiene instrumentación, pero la madurez y los valores por defecto difieren entre versiones y distribuciones.
Consejo operativo: elige un “camino dorado” para captura de evidencia por motor. Para MySQL 8.0, apóyate en performance_schema y resúmenes de digest. Para MariaDB, valida qué está habilitado y confía en una mezcla de contadores de estado, InnoDB status, slow logs y muestreo dirigido.
Deriva del optimizador: misma consulta, plan distinto
Aquí el “drop-in” se pone picante. Una consulta puede ser correcta en ambos motores pero elegir órdenes de join o rutas de índice diferentes. Bajo carga, eso no es académico; es la diferencia entre “funciona” y “se derrite”.
Consejo operativo: trata los cambios de motor y las actualizaciones mayores como eventos de cambio de plan. Baselinea planes de consultas críticas y pruébalos contra regresiones con datos representativos.
Comportamiento de concurrencia: manejo de hilos y contención interna
MariaDB a menudo se discute junto con el comportamiento del thread pool en ciertas distribuciones. MySQL tiene su propio comportamiento de hilos y mejoras entre versiones. En ambos, el tema es el mismo: la concurrencia descontrolada convierte la latencia en una lotería.
Consejo operativo: limita la concurrencia en la app y en el límite de la base de datos. No “arregles” la sobrecarga subiendo max_connections. Es como ensanchar el embudo mientras la tubería está obstruida.
Replicación y clustering: herramientas distintas, modos de falla distintos
El ecosistema de MySQL se inclina hacia GTIDs, semi-sync, group replication y patrones de servicio gestionado. MariaDB suele aparecer en campo con Galera. Ambos pueden ser fiables. Ambos también pueden fallar de formas que parecen “lentitud aleatoria” cuando el problema real es control de flujo, conflictos de certificación o serialización de aplicación.
Consejo operativo: decide lo que necesitas: escalado de lecturas, failover, multi-writer o distribución geográfica. Luego diseña para eso. No “habilites clustering” como característica de rendimiento; es una característica de disponibilidad con costos de latencia.
Realidad del motor de almacenamiento: la mayoría corre InnoDB
Si tu carga es InnoDB, muchos de tus cuellos de botella serán los mismos en ambos: fallos del buffer pool, flush del redo log, comportamiento de doublewrite, page flushing y contención de locks. Por eso la lista de comprobación funciona independientemente de la marca.
Consejo operativo: si alguien afirma “MariaDB es más lento” o “MySQL es más lento”, pregunta: ¿más lento en qué clase de cuello de botella? ¿y dónde está la evidencia?
Preguntas frecuentes
1) ¿Cuál es la forma más rápida de encontrar el cuello de botella?
Clasifícalo: CPU-bound vs IO-bound vs lock-bound vs replication-bound. Usa vmstat, iostat -x y SHOW FULL PROCESSLIST antes de tocar la configuración.
2) ¿Debo empezar con el slow query log o con performance schema?
Empieza con lo que ya esté habilitado y sea confiable. El slow query log tiene poca fricción y funciona en ambos motores. Performance schema es más rico en MySQL 8.0, pero necesitas saber qué está habilitado y la sobrecarga que aceptas.
3) Si la latencia de disco es alta, ¿debería simplemente aumentar el buffer pool?
Sólo si tienes margen de RAM y tu working set puede beneficiarse. Si tu dataset activo es mucho mayor que la memoria, aumentar buffer pool tiene retornos decrecientes. Arregla selectividad e índices primero.
4) ¿Por qué “Threads_running” es más importante que “Threads_connected”?
Connected te dice cuántos clientes están aparcados. Running te dice cuántos consumen CPU activamente o esperan dentro del motor. Contadores altos de running suelen correlacionar con contención y encolamiento.
5) ¿El lag de replicación es problema de la base de datos o de la aplicación?
Ambos. El lag a menudo proviene de la forma de las transacciones de la aplicación: transacciones grandes, filas calientes o DDL. La configuración de la base de datos (paralelismo de aplicación, capacidad de I/O) puede ayudar, pero no puede hacer paralelo lo que es fundamentalmente serializado.
6) ¿Cuándo está bien cambiar ajustes de durabilidad por rendimiento?
Cuando el negocio acepta explícitamente el riesgo y puedes cuantificar el radio de impacto. Si no, trata la durabilidad como no negociable y arregla el cuello de botella real (almacenamiento, tasa de transacciones, batching).
7) Mi consulta usa un índice pero sigue lenta—¿por qué?
Porque “usa un índice” puede significar aún escanear un rango grande, hacer I/O aleatorio o ordenar después. Observa rows examined, “Using filesort”, “Using temporary” y si el índice coincide con filtro + orden.
8) ¿Cómo evito regresiones de plan durante upgrades de MariaDB/MySQL?
Basea las consultas críticas (EXPLAIN y tiempo de ejecución con datos representativos), ejecuta replay de workload o tests de carga y compara los digests de consultas top antes y después. Trata los cambios del optimizador como característica y riesgo.
9) ¿Cuál es la causa más común de “a veces va lento”?
Encolamiento: parones de fsync, esperas por locks o ráfagas de flush en background. La latencia intermitente suele significar que algo está bloqueando, no que la CPU olvidó cómo computar.
10) ¿Deberíamos movernos de MySQL a MariaDB (o viceversa) para arreglar rendimiento?
No como primer movimiento. Cambiar de motor puede ayudar en casos específicos (características, instrumentación, modelo de replicación), pero la mayoría de las mejoras de rendimiento vienen de arreglos de consulta/índice, disciplina de transacciones y corrección del almacenamiento.
Próximos pasos que no arruinarán tu fin de semana
Este es el plan práctico que daría a una rotación on-call que quiere menos misterios a las 3 a.m. y sistemas más predecibles:
- Estandariza la captura de evidencia: un script/runbook que tome processlist, InnoDB status, contadores clave y estadísticas de I/O del host con timestamps.
- Habilita al menos una fuente de trazado de consultas: slow query log con umbrales sensatos, además de captura de plan para los principales ofensores.
- Establece tu taxonomía de cuellos de botella: CPU, E/S, bloqueos, replicación, memoria. Etiqueta los incidentes según esto. Los patrones emergen rápido.
- Arregla la forma de la carga: acorta transacciones, añade índices compuestos faltantes, reduce joins en abanico y limita la concurrencia upstream.
- Solo entonces afina: ajusta tamaños de memoria y parámetros relacionados con E/S basándote en evidencia probada, y hazlo con pasos de rollback.
La verdadera ventaja de rendimiento entre MariaDB y MySQL no es un parámetro secreto. Es la claridad operativa. Ganas encontrando el cuello de botella rápido, arreglando la causa real y negándote a confundir actividad con progreso.