No eliges una base de datos en calma. La eliges el día en que tu equipo de marketing “solo manda un email”, tu API recibe una tormenta de conexiones y la latencia p95 se convierte en una coreografía interpretativa.
MySQL y Percona Server son parientes cercanos. Bajo carga constante, puedes medir hasta el empate. En picos, las diferencias aparecen en las partes feas: control de admisión, comportamiento de flushing, presión de mutex, sobrecarga de instrumentación y qué tan rápido puedes demostrar qué está realmente roto.
Qué cambia realmente bajo picos (no el marketing)
Percona Server comenzó como “MySQL, pero con las perillas que necesitamos y la visibilidad que desearíamos que MySQL trajera”. MySQL moderno ha alcanzado varias mejoras, y Percona también ha evolucionado según cambia upstream. La pregunta práctica hoy no es “cuál es más rápido”, sino “cuál se comporta de forma más predecible cuando el mundo arde”. La predictibilidad gana incidentes.
Las diferencias principales que aparecen durante picos
- Control de admisión y programación de hilos: Percona Server suele incluir la opción de thread pool que puede domar tormentas de conexiones y ráfagas OLTP de alta concurrencia. MySQL Enterprise trae thread pool; Community normalmente no. Si tu pico es “demasiadas conexiones concurrentes”, el pool de hilos puede marcar la diferencia entre un sistema degradado y uno muerto.
- Comportamiento de InnoDB/XtraDB y variables adicionales: Históricamente, Percona distribuyó XtraDB (un fork/mejorado InnoDB) con más instrumentación y opciones de ajuste. Hoy muchas mejoras están upstream, pero Percona suele exponer más introspección y palancas operativas que importan en medio de un incidente.
- Postura por defecto de la instrumentación: Percona Server facilita obtener información profunda rápidamente (contadores de estado extra, opciones mejoradas de slow query, a veces menos fricción para habilitar plugins útiles). MySQL puede hacer la mayoría, pero quizá tengas que planear con antelación: activar instrumentación pesada durante un pico es como decidir hacer una resonancia mientras corres una carrera.
- Ecosistema de herramientas operativas: Percona Toolkit (pt-query-digest, pt-online-schema-change, pt-kill) no es “Percona Server”, pero en la práctica los equipos adoptan el conjunto. En picos esto importa: quieres comandos de triaje repetibles, no SQL artesanal tecleado con manos temblorosas.
- Decisiones de compilación y empaquetado: Percona tiende a distribuir builds pensadas para necesidades de ops (plugins extra, performance schema con valores por defecto razonables, opciones de telemetría sensatas). El empaquetado del proveedor afecta tu cadencia de upgrade y qué está habilitado por defecto—y los valores por defecto deciden el comportamiento bajo carga sorpresa.
La trampa: asumir que Percona Server es inherentemente “más rápido” en todo. No lo es. A menudo es más controlable y más observable. En picos, control y observabilidad vencen a lo que teóricamente es más rápido en laboratorio.
Un chiste corto, como prometí: Una base de datos en un pico es como una invitación a una reunión: añadir más gente nunca la hace terminar antes.
Algunos datos e historia útiles (porque los valores por defecto tienen una historia)
Estos no son datos para el trivial. Cada uno explica por qué existe cierto comportamiento y por qué lo verás en producción.
- El valor inicial de Percona Server fue XtraDB: durante años distribuyó un fork mejorado de InnoDB con contadores extras y ajustes que upstream no tenía. Ese ADN aún se refleja en características “ops-first”.
- El thread pool no siempre fue accesible: el thread pool de MySQL históricamente fue una característica de Enterprise; Percona hizo capacidades similares disponibles para más usuarios vía su distribución y plugins.
- MySQL 5.6→5.7→8.0 cambió las reglas: mejoras en Performance Schema, mejor comportamiento de metadata locking y avances en replicación redujeron la presión de “necesito un fork” para muchas cargas de trabajo.
- Performance Schema pasó de “evitar” a “usar”: despliegues tempranos temían la sobrecarga; versiones modernas pueden configurarse para producción y son esenciales para la forense de picos.
- La replicación se hizo multihilo en etapas: desde un hilo SQL único a appliers multihilo con modos de planificación distintos; el comportamiento en picos de réplicas depende fuertemente de estas configuraciones.
- Las heurísticas de flushing de InnoDB evolucionaron: flushing adaptativo y checkpoints más estables mejoraron la situación, pero aún puedes crear “flush storms” autoinfligidos con configuraciones malas o supuestos de almacenamiento irreales.
- El comportamiento por defecto de autenticación y SSL de MySQL cambió con el tiempo: en picos, el coste del handshake y el churn de conexiones pueden dominar; la versión y el plugin de auth importan.
- El ecosistema de Percona modeló hábitos operativos: pt-query-digest y similares enseñaron a una generación de SREs a tratar el análisis de consultas como una habilidad de respuesta a incidentes, no como un proyecto trimestral.
Los picos no son “alta carga”: la mecánica del fallo
La carga en estado estable prueba mayormente capacidad. Los picos prueban bucles de control: encolamiento, backpressure y qué tan rápido los subsistemas internos se recuperan de un exceso repentino de trabajo.
Qué hace un pico a un sistema tipo MySQL
Cuando las peticiones se multiplican 5–20x por un periodo corto, normalmente obtienes alguna combinación de:
- Tormentas de conexiones: nuevos handshakes TCP + TLS + autenticación y overhead por hilo de conexión. Aunque las consultas sean baratas, la planificación de hilos y la asignación de memoria se vuelven costosas.
- Contención de mutex/bloqueos: hotspots internos (bloqueos de instancias del buffer pool, dict locks, caché de tablas, MDL, contención del adaptive hash index, o incluso contención del scheduler del SO).
- Acumulación de páginas sucias: escrituras que superan al flushing hacen crecer la edad del checkpoint. Eventualmente, el motor entra en pánico y hace flushing agresivo, robando I/O a las lecturas e incrementando la latencia.
- Presión en el redo log: si el redo se llena más rápido de lo que puede checkpointearse, se producen stalls. Este es uno de los patrones más comunes de “todo iba bien hasta que dejó de estarlo”.
- Retraso en replicación: ráfagas de escrituras crean backlog; las réplicas se quedan atrás y tu escalado de lecturas se convierte en amplificación de lecturas contra el primario porque la app recurre a él.
- Desplazamiento de caché: el buffer pool se contamina con patrones de acceso de una sola vez (piensa: una página de promoción que recorre una categoría grande), expulsando páginas calientes y causando dolor prolongado incluso después de que el pico termine.
En picos, la pregunta correcta rara vez es “cómo hago más rápidas las consultas”, porque la mayoría de los picos no son causados por una consulta lenta. Son causados por demasiadas consultas razonables al mismo tiempo, más unas pocas patológicas que se vuelven letales cuando los recursos están ajustados.
Control de concurrencia: un hilo de más sigue siendo demasiado
En MySQL Community, un modo común es un hilo por conexión. En un pico, tienes un ejército de hilos compitiendo por CPU, bloqueos y líneas de caché. Tu máquina no está “ocupada”, está thrashing.
Por qué el thread pool de Percona importa durante picos
Un thread pool cambia la forma del fallo. En lugar de 2.000 hilos ejecutables intentando ejecutar 2.000 consultas concurrentes, puedes tener un pool de trabajadores más pequeño que limita la ejecución activa y encola el resto. El sistema se degrada con más gracia: la latencia sube, pero sigue respondiendo.
Las pools de hilos no son magia. Pueden ocultar consultas lentas (la cola enmascara el incendio) y pueden introducir problemas de equidad (algunos clientes se quedan sin servicio si el pool no está afinado). Pero para el modo de fallo específico “tormenta de conexiones + colapso del scheduler de CPU”, son una herramienta práctica de control de admisión.
Qué hacer si no tienes thread pool
- Haz obligatorio el pooling de conexiones: en la capa de aplicación. Si tu app abre nuevas conexiones durante picos, eliges dolor.
- Usa max_connections como fusible: no como trofeo. Fíjalo a lo que el host puede manejar y falla rápido aguas arriba con retry/backoff adecuados.
- Protege el primario con un proxy: HAProxy/ProxySQL pueden limitar la tasa de conexiones y ofrecer multiplexación. Incluso un límite bruto de conexiones en el borde puede evitar que la base de datos sea el limitador de tasa.
Flushing y checkpoints de InnoDB: donde los picos van a morir
Si los picos son tu problema principal, deja de pensar en InnoDB como “un motor de almacenamiento”. En un pico es un conjunto de colas competidoras: hilos limpiadores de páginas, escritor de redo log, doublewrite buffer, cadencia de fsync y la capacidad real de tu subsistema de almacenamiento para hacer escrituras pequeñas aleatorias.
El patrón clásico de pico: las páginas sucias se acumulan y luego llega la tormenta de flush
Durante una ráfaga de escrituras, las páginas sucias se acumulan en el buffer pool. Si el flushing no puede seguir el ritmo, la edad del checkpoint crece. Eventualmente InnoDB debe hacer flush agresivo para avanzar el checkpoint y evitar quedarse sin espacio de redo. Ese flushing agresivo compite con I/O de lectura y CPU, así que la latencia se dispara. El remate: la peor latencia a menudo aparece después de la ráfaga.
Configuraciones que cambian el perfil del pico
- innodb_log_file_size / innodb_redo_log_capacity: Una mayor capacidad de redo puede absorber ráfagas, pero también aumenta el tiempo de recuperación tras un crash y puede retrasar la señal de “estás en problemas”.
- innodb_flush_log_at_trx_commit: 1 es lo más seguro; 2 sacrifica durabilidad por rendimiento. En picos, la frecuencia de fsync suele ser un limitador. Si cambias esto, hazlo como decisión de negocio, no por pánico.
- innodb_io_capacity / innodb_io_capacity_max: Son hints para el flushing en background. Valores incorrectos causan flushing perezoso (pánico de checkpoint más tarde) o flushing agresivo (presión I/O constante).
- innodb_flush_neighbors: En SSD/NVMe, flush de vecinos suele ser trabajo desperdiciado. En discos giratorios puede ayudar. Si estás en almacenamiento moderno y esto está activado, quizá pagues por nostalgia.
- innodb_lru_scan_depth y page_cleaners: Afectan qué tan rápido InnoDB encuentra páginas sucias para limpiar. Un mal ajuste puede crear consumo de CPU o limpieza insuficiente.
Segundo chiste (y ya terminamos): Afinar el flushing durante un incidente es como cambiar neumáticos en la autopista—es posible, pero tu métrica de éxito es “nadie murió”.
Replicación bajo ráfagas: la latencia que te ganas
La latencia de replicación en picos rara vez es “la replicación es lenta”. Suele ser que generaste escrituras más rápido de lo que la réplica puede aplicar, o forzaste que la replicación se serialice por la forma de las transacciones o el esquema.
Dónde aparecen las diferencias MySQL/Percona
La mayor parte del comportamiento de replicación es upstream MySQL, pero las distribuciones de Percona suelen facilitar su observación y ajuste. Las realidades clave en época de picos:
- La replicación basada en filas suele ser mejor en picos: la replicación basada en sentencias puede comportarse raro con no-determinismo y ser más difícil de paralelizar de forma segura.
- Las transacciones grandes son fábricas de lag: una transacción gigante se aplica como una unidad. La réplica se queda ahí haciendo una cosa mientras la app suplica lecturas frescas.
- Los aplicadores multihilo necesitan el modo correcto: “LOGICAL_CLOCK” (en términos de MySQL) y un seguimiento de dependencias adecuado ayudan. Pero si tu carga concentra accesos en pocas filas, el paralelismo no te salvará.
- Configuraciones de durabilidad en réplicas: A menudo puedes relajar la durabilidad en réplicas (con los ojos abiertos) para mantenerlas lo bastante cerca y servir lecturas durante ráfagas.
Instrumentación y observabilidad: ver el problema sin volverte el problema
Durante un pico necesitas respuestas rápidas: ¿es CPU, I/O, bloqueos o la app? La diferencia entre “suficiente” y “bueno” en observabilidad son minutos de downtime.
MySQL te da Performance Schema, sys schema, EXPLAIN ANALYZE y muchos contadores de estado. Percona Server suele añadir o exponer más detalle y distribuirse con una postura amigable para ops. La clave no es la marca. La clave es si puedes recopilar la evidencia sin causar carga adicional.
Una cita operativa, usada con cuidado: La esperanza no es una estrategia.
— James Cameron. Aplica a la respuesta a incidentes más de lo que debería.
Qué deberías prehabilitar antes de que ocurran picos
- Slow query log con umbrales sensatos (y muestreo si está disponible). Los picos suelen involucrar “consultas moderadamente lentas a alta frecuencia”.
- Performance Schema con consumidores objetivos (no todo). Quieres waits, stages y digests de sentencias—no necesariamente tablas históricas completas en un primario caliente.
- Canal de digestión de consultas que pueda ejecutarse bajo carga sin bloquear la base de datos (parsear logs off-host, no ejecutar análisis pesado en el primario).
- Telemetría a nivel OS (iostat, vmstat, métricas de CPU listas para perf) porque las bases de datos aman mentir por omisión.
Tareas prácticas: comandos, salidas y la decisión que tomas
Estos son los movimientos que realmente haces a las 02:14. Cada uno incluye un comando ejecutable, un fragmento de salida realista, qué significa y qué decisión sigue. Supuestos: host Linux, systemd, cliente MySQL disponible, y credenciales configuradas vía ~/.my.cnf o env.
Tarea 1: Comprobar si estás saturado de CPU o solo saturado de hilos
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
6 1 0 31284 81264 923456 0 0 120 980 1800 4200 55 18 12 15 0
18 2 0 29876 79912 918332 0 0 110 1120 2600 9800 63 22 5 10 0
21 3 0 28740 79020 914120 0 0 90 1400 2900 12000 68 25 2 5 0
Significado: Alto r de hilos ejecutables y bajo id indica presión de CPU; cs elevado sugiere tormenta de cambio de contexto.
Decisión: Si cs es enorme y la latencia es errática, sospecha demasiados hilos activos. Considera thread pool (Percona) o limitar concurrencia vía proxy/app. Si domina wa, pivota a I/O.
Tarea 2: Detectar saturación y latencia de I/O
cr0x@server:~$ iostat -x 1 3
Linux 6.1.0 (db01) 12/30/2025 _x86_64_ (16 CPU)
avg-cpu: %user %nice %system %iowait %steal %idle
41.20 0.00 12.10 18.90 0.00 27.80
Device r/s w/s rkB/s wkB/s rrqm/s wrqm/s %util await r_await w_await
nvme0n1 220.0 950.0 3520.0 48200.0 0.0 10.0 99.2 18.4 4.2 21.8
Significado: El dispositivo está ~99% ocupado con ~18ms de await; el await de escritura es peor. Eso indica historia de flushing/redo/doublewrite hasta que se demuestre lo contrario.
Decisión: Reduce la presión de escritura (limita jobs por lotes, deshabilita escrituras no esenciales, descarga carga). Luego inspecciona checkpoint/ páginas sucias de InnoDB.
Tarea 3: Verificar tormenta de conexiones
cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Threads%';"
+-------------------+-------+
| Variable_name | Value |
+-------------------+-------+
| Threads_cached | 12 |
| Threads_connected | 1800 |
| Threads_created | 98234 |
| Threads_running | 280 |
+-------------------+-------+
Significado: 1800 conectados, 280 en ejecución. Si esto saltó de golpe, estás en territorio de tormenta. Threads_created alto indica churn.
Decisión: Si tienes thread pool de Percona, habilítalo/ajústalo (planificado). De lo contrario: impón pooling en la app, limita con proxy, reduce max_connections para proteger la máquina y aplica backoff aguas arriba.
Tarea 4: Identificar tipo de wait dominante (bloqueos vs I/O vs CPU)
cr0x@server:~$ mysql -e "SELECT EVENT_NAME, COUNT_STAR, SUM_TIMER_WAIT/1000000000000 AS seconds FROM performance_schema.events_waits_summary_global_by_event_name WHERE COUNT_STAR > 0 ORDER BY SUM_TIMER_WAIT DESC LIMIT 5;"
+--------------------------------------+------------+---------+
| EVENT_NAME | COUNT_STAR | seconds |
+--------------------------------------+------------+---------+
| wait/io/file/innodb/innodb_log_file | 12899322 | 1824.51 |
| wait/synch/mutex/innodb/buf_pool | 8933121 | 610.23 |
| wait/io/file/innodb/innodb_data_file | 4021932 | 488.11 |
| wait/lock/metadata/sql/mdl | 832211 | 120.09 |
| wait/synch/rwlock/innodb/index_tree | 1010021 | 98.77 |
+--------------------------------------+------------+---------+
Significado: Los waits de redo log dominan. Eso es presión de fsync/redo; el mutex del buffer pool también está sufriendo.
Decisión: Trátalo como bound por escritura. Considera tradeoffs de durabilidad solo si el negocio lo permite. De lo contrario reduce concurrencia de escrituras, arregla latencia de almacenamiento y ajusta capacidad de redo/flushing con un cambio planificado.
Tarea 5: Comprobar páginas sucias y presión de checkpoint en InnoDB
cr0x@server:~$ mysql -e "SHOW ENGINE INNODB STATUS\G" | sed -n '1,140p'
...
BUFFER POOL AND MEMORY
----------------------
Total large memory allocated 2197815296
Buffer pool size 131072
Free buffers 128
Database pages 129880
Old database pages 47910
Modified db pages 32120
...
LOG
---
Log sequence number 112903450192
Log flushed up to 112903120991
Last checkpoint at 112901000000
...
Significado: Un alto Modified db pages significa muchas páginas sucias. La gran brecha entre LSN y checkpoint indica crecimiento de la edad del checkpoint. Si esa brecha sigue creciendo, se acerca (o ya está) una tormenta de flush.
Decisión: Incrementa la eficacia del flushing en background (corrige innodb_io_capacity), reduce la tasa de escritura y asegúrate de que el almacenamiento pueda manejar el patrón de escrituras. Evita hacer “set io_capacity=20000” al azar a menos que conozcas tu dispositivo.
Tarea 6: Detectar las huellas de consulta principales durante el pico
cr0x@server:~$ mysql -e "SELECT DIGEST_TEXT, COUNT_STAR, ROUND(SUM_TIMER_WAIT/1e12,2) AS total_s FROM performance_schema.events_statements_summary_by_digest ORDER BY SUM_TIMER_WAIT DESC LIMIT 3\G"
*************************** 1. row ***************************
DIGEST_TEXT: SELECT * FROM orders WHERE user_id = ? ORDER BY created_at DESC LIMIT ?
COUNT_STAR: 481220
total_s: 912.44
*************************** 2. row ***************************
DIGEST_TEXT: UPDATE inventory SET qty = qty - ? WHERE sku = ?
COUNT_STAR: 112881
total_s: 401.18
*************************** 3. row ***************************
DIGEST_TEXT: SELECT COUNT(*) FROM sessions WHERE expires_at > ?
COUNT_STAR: 98011
total_s: 380.02
Significado: Tienes un SELECT de alta frecuencia y un UPDATE caliente sobre inventory. En picos, las “filas calientes” matan la concurrencia incluso si las consultas están indexadas.
Decisión: Para el UPDATE caliente: considera sharding de la clave de contención, rediseñar la reserva de inventario o mover ese contador a una caché atómica con write-behind (con cautela). Para el SELECT: asegúrate de que el índice soporte el ORDER BY; considera cachear el top N por usuario.
Tarea 7: Confirmar soporte de índices para la consulta del pico
cr0x@server:~$ mysql -e "EXPLAIN SELECT * FROM orders WHERE user_id = 123 ORDER BY created_at DESC LIMIT 20;"
+----+-------------+--------+------+---------------+-------------------+---------+------+------+-----------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+--------+------+---------------+-------------------+---------+------+------+-----------------------------+
| 1 | SIMPLE | orders | ref | idx_user_date | idx_user_date | 8 | const| 60 | Using where; Using filesort |
+----+-------------+--------+------+---------------+-------------------+---------+------+------+-----------------------------+
Significado: Usa el índice pero aún hace filesort, lo que implica que el orden del índice no coincide con la consulta (o hay desajuste de colación/ASC-DESC).
Decisión: Crea el índice compuesto correcto (por ejemplo, (user_id, created_at) con la dirección de ordenación coincidente en MySQL 8.0 cuando aplique). Esto reduce CPU y trabajo con tablas temporales durante picos.
Tarea 8: Comprobar presión de tablas temporales (las temp tables en disco son veneno en picos)
cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Created_tmp%';"
+-------------------------+--------+
| Variable_name | Value |
+-------------------------+--------+
| Created_tmp_disk_tables | 982112 |
| Created_tmp_files | 12011 |
| Created_tmp_tables | 1812231|
+-------------------------+--------+
Significado: Muchas tablas temporales en disco. En picos, esto amplifica I/O, a menudo en el mismo dispositivo que InnoDB necesita para redo/data.
Decisión: Arregla las consultas que provocan sorts/group-bys sin índices; aumenta tmp_table_size/max_heap_table_size con cautela; asegúrate de que tmpdir esté en almacenamiento rápido si es necesario.
Tarea 9: Detectar acumulaciones de metadata locking (DDL durante picos es un hobby, no un plan)
cr0x@server:~$ mysql -e "SELECT OBJECT_TYPE, OBJECT_SCHEMA, OBJECT_NAME, LOCK_TYPE, LOCK_STATUS, COUNT(*) cnt FROM performance_schema.metadata_locks GROUP BY 1,2,3,4,5 ORDER BY cnt DESC LIMIT 5;"
+-------------+---------------+-------------+-----------+-------------+-----+
| OBJECT_TYPE | OBJECT_SCHEMA | OBJECT_NAME | LOCK_TYPE | LOCK_STATUS | cnt |
+-------------+---------------+-------------+-----------+-------------+-----+
| TABLE | app | users | SHARED | GRANTED | 980 |
| TABLE | app | users | EXCLUSIVE | PENDING | 12 |
| TABLE | app | orders | SHARED | GRANTED | 620 |
+-------------+---------------+-------------+-----------+-------------+-----+
Significado: Un lock EXCLUSIVE pendiente en users mientras cientos mantienen SHARED: alguien intentó DDL (o una operación que bloquea mucho) y ahora está bloqueando futuras sentencias que necesitan acceso a metadata.
Decisión: Encuentra el bloqueador y mátalo si es seguro. Reprograma el DDL con herramientas online y protecciones.
Tarea 10: Encontrar la sesión que bloquea realmente
cr0x@server:~$ mysql -e "SHOW PROCESSLIST;" | head -n 15
Id User Host db Command Time State Info
1123 app 10.0.4.21:51244 app Query 35 Waiting for table metadata lock ALTER TABLE users ADD COLUMN flags INT DEFAULT 0
1188 app 10.0.4.19:50122 app Query 2 Sending data SELECT * FROM users WHERE id = 98322
...
Significado: El ALTER está esperando, pero también puede ser la causa del churn de locks dependiendo del tipo de DDL y la versión.
Decisión: Durante un pico, mata el DDL a menos que forme parte de una migración controlada y conocida como segura. Luego implementa “no DDL en primario durante peak” como política, no como esperanza.
Tarea 11: Comprobar lag de replicación y estado del applier
cr0x@server:~$ mysql -e "SHOW REPLICA STATUS\G" | egrep -i 'Seconds_Behind|Replica_IO_Running|Replica_SQL_Running|SQL_Delay|Slave_SQL_Running_State|Last_Errno|Last_Error'
Replica_IO_Running: Yes
Replica_SQL_Running: Yes
Seconds_Behind_Source: 840
Replica_SQL_Running_State: Waiting for dependent transaction to commit
Last_Errno: 0
Last_Error:
Significado: El lag es alto, el hilo SQL está bloqueado por dependencias de commits—a menudo debido a transacciones grandes o restricciones de orden de commit.
Decisión: Busca transacciones grandes, reduce la contención de commits, asegúrate de que los aplicadores multihilo estén configurados correctamente y considera relajar la durabilidad de la réplica si el escalado de lecturas es crítico.
Tarea 12: Identificar transacciones grandes que envenenan réplicas
cr0x@server:~$ mysql -e "SELECT trx_id, trx_started, trx_rows_modified, trx_query FROM information_schema.innodb_trx ORDER BY trx_rows_modified DESC LIMIT 3\G"
*************************** 1. row ***************************
trx_id: 90011234
trx_started: 2025-12-30 02:11:09
trx_rows_modified: 820441
trx_query: UPDATE events SET processed=1 WHERE processed=0
Significado: Un UPDATE masivo en una sola transacción. Esto destruirá tu buffer pool, redo y tiempo de aplicación en replicación.
Decisión: Deténlo (mata la sesión) y luego rehace el job para fragmentar las actualizaciones en transacciones acotadas y throttling.
Tarea 13: Comprobar hit rate del buffer pool y presión de lectura
cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%';"
+---------------------------------------+-----------+
| Variable_name | Value |
+---------------------------------------+-----------+
| Innodb_buffer_pool_read_requests | 981220122 |
| Innodb_buffer_pool_reads | 18222122 |
+---------------------------------------+-----------+
Significado: Las lecturas físicas son significativas. Durante picos, si las lecturas se disparan, puedes estar falto de memoria o con caché contaminada.
Decisión: Si RAM lo permite, aumenta buffer pool (planificado). Más a menudo: arregla el patrón de acceso (cache, limitar scans), añade índices y protege la base de datos de comportamientos de «navegar todo» puntuales.
Tarea 14: Revisar si table cache y límites de ficheros abiertos causan churn
cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Opened_tables'; SHOW VARIABLES LIKE 'table_open_cache';"
+---------------+--------+
| Variable_name | Value |
+---------------+--------+
| Opened_tables | 922001 |
+---------------+--------+
+------------------+-------+
| Variable_name | Value |
+------------------+-------+
| table_open_cache | 4000 |
+------------------+-------+
Significado: Opened_tables masivo puede indicar table cache demasiado pequeño o churn por muchas tablas/particiones. En picos, esto añade presión de mutex y descriptores de fichero.
Decisión: Aumenta table_open_cache y límites OS de ficheros (planificado), y reduce el número de tablas/particiones si es autoinfligido.
Tarea 15: Confirmar si estás haciendo swap (si sí, detén todo lo demás)
cr0x@server:~$ free -m
total used free shared buff/cache available
Mem: 32000 30910 210 130 879 420
Swap: 4096 2048 2048
Significado: Hay uso de swap. Para OLTP de baja latencia, swap suele ser un outage a cámara lenta.
Decisión: Reduce la huella de memoria inmediatamente: detén sidecars, reduce buffer pool solo si es necesario y con cuidado, corrige overcommit. A largo plazo: provisiona más RAM y mantén el host dedicado.
Tarea 16: Comprobar si el binary logging es el cuello de botella (común en picos de escritura)
cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Binlog_cache%'; SHOW GLOBAL STATUS LIKE 'Binlog_commits';"
+-------------------------------+-------+
| Variable_name | Value |
+-------------------------------+-------+
| Binlog_cache_disk_use | 1221 |
| Binlog_cache_use | 88212 |
+-------------------------------+-------+
+----------------+--------+
| Variable_name | Value |
+----------------+--------+
| Binlog_commits | 922112 |
+----------------+--------+
Significado: Uso de disco para binlog cache sugiere transacciones grandes que desbordan; la tasa de commits binlog muestra presión. Combinado con waits de redo, podrías estar bound por commit.
Decisión: Arregla el tamaño de las transacciones; asegúrate de que binlog esté en almacenamiento rápido; considera tradeoffs de sync_binlog solo con una postura clara de durabilidad.
Guion de diagnóstico rápido
Esta es la secuencia de “entrar al incidente”. El objetivo es identificar la clase de cuello de botella en menos de 10 minutos y aplicar la mitigación menos peligrosa.
Primero: decide si es CPU, I/O o bloqueo/cola
- Vista OS:
vmstatyiostat -x. Si iowait y await son altos, estás bound por I/O. Si la CPU idle es baja con enormes context switches, estás bound por concurrencia. - Contadores rápidos de MySQL: Threads connected/running y top waits desde Performance Schema. Buscas “redo log waits”, “data file waits”, “MDL” o hotspots de mutex.
- Processlist: ¿Las consultas están atascadas en “Waiting for table metadata lock”, “Waiting for commit”, “Sending data” o simplemente “Sleep” con demasiadas conexiones?
Segundo: clasifica el tipo de pico
- Tormenta de conexiones: Threads_connected se dispara, CPU thrasha, muchas consultas cortas. Arreglo: pooling, thread pool/proxy, limitar concurrencia.
- Ráfaga de escrituras: redo waits, edad de checkpoint, muchas páginas sucias, iostat muestra write await. Arreglo: throttlear escritores, reducir tamaño de transacciones, afinar flushing y capacidad de redo (planificado).
- Inundación de lecturas / falta de caché: Innodb_buffer_pool_reads se dispara, iostat muestra lecturas, la CPU puede estar moderada. Arreglo: cache, índices, proteger contra scans, añadir réplicas de lectura (cuidado con lag).
- Contención de locks / filas calientes: muchas sesiones esperando locks de fila, altas esperas por locks. Arreglo: rediseñar hotspot, bajar aislamiento cuando sea seguro, reducir alcance de transacciones, implementar reintentos idempotentes.
- Evento DDL/MDL: esperas MDL explotan. Arreglo: matar/parar DDL, usar herramientas de schema online con throttling, programar migraciones, imponer puertas para DDL en CI/CD.
Tercero: elegir una mitigación que no dificulte la recuperación
- Descargar carga: limitar tasa, deshabilitar endpoints costosos temporalmente, poner en cola escrituras, apagar jobs background no esenciales.
- Estabilizar concurrencia: reducir max_connections (sí, en serio), imponer pooling en la app, habilitar thread pool donde esté disponible.
- Detener la hemorragia: matar al mayor culpable (la UPDATE gigante, la consulta desbocada, el DDL accidental), y luego seguir investigando.
Errores comunes: síntoma → causa raíz → solución
1) Síntoma: la latencia se dispara cada pocos minutos, incluso después de que el tráfico baja
Causa raíz: presión de checkpoints y flush storms; el flushing en background no puede seguir el ritmo durante ráfagas por lo que “se pone al día” dolorosamente más tarde.
Solución: corregir innodb_io_capacity para que coincida con los IOPS reales del dispositivo, verificar capacidad de redo, reducir tamaño de transacciones y asegurar latencia estable del almacenamiento. No “soluciones” bajando durabilidad a menos que sea aceptable explícitamente.
2) Síntoma: CPU al 100%, pero el throughput empeora
Causa raíz: demasiados hilos ejecutables, cambios de contexto, contención de mutex. A menudo provocado por tormentas de conexiones.
Solución: imponer pooling de conexiones; limitar concurrencia; usar thread pool (Percona Server) o multiplexación ProxySQL. Auditar la app por conexiones por petición.
3) Síntoma: réplicas se quedan atrás y luego el primario colapsa
Causa raíz: el tráfico de lectura “vuelve” al primario cuando las réplicas se atrasan; o la app necesita lecturas consistentes y evita réplicas durante el lag.
Solución: definir requisitos de consistencia; implementar routing consciente del lag; afinar paralelismo de apply en réplicas; fragmentar transacciones grandes.
4) Síntoma: repentino “Waiting for table metadata lock” en todas partes
Causa raíz: cambio de esquema online hecho mal, o DDL ejecutado en una tabla caliente durante el pico.
Solución: matar el DDL; usar tooling de online schema change con throttling; programar migraciones; imponer política de “no peak” para DDL.
5) Síntoma: muchas tablas temporales en disco, I/O alto y consultas que “deberían estar indexadas”
Causa raíz: índices compuestos faltantes para ORDER BY/GROUP BY, o consultas que devuelven demasiadas filas; las temp tables spillan a disco bajo concurrencia.
Solución: añadir índices correctos; reescribir consultas para limitar pronto; considerar índices covering; evitar SELECT * cuando solo se necesitan pocas columnas.
6) Síntoma: “Lock wait timeout exceeded” durante picos en una sola tabla
Causa raíz: fila caliente o hoja de índice secundaria caliente, a menudo por contadores, flags de estado o actualizaciones de “last_seen”.
Solución: rediseñar el hotspot: modelo append-only, contadores particionados, batching, o mover ese contador mutable a otro sistema con reconciliación explícita.
7) Síntoma: habilitar instrumentación empeora el pico
Causa raíz: activar demasiados consumidores de Performance Schema o logging pesado en plena carga pico.
Solución: preconfigurar digests y resúmenes de waits ligeros. Durante incidentes, prioriza muestreo y tablas resumen. Recopila logs off-host.
Tres mini-historias corporativas de las minas de picos
Mini-historia 1: El incidente causado por una suposición errónea
La compañía tenía un plan “simple”: migrar de un MySQL antiguo a uno más nuevo, mantener el esquema y listo. La carga era mayoritariamente de lectura. Asumieron que los picos también serían de lectura. Estaban equivocados de la forma más aburrida posible.
Un lanzamiento de producto creó una ráfaga de escrituras por eventos de tracking. Cada petición escribía una fila y actualizaba un agregado por usuario. La actualización del agregado era “pequeña”, así que nadie se preocupó. En un día tranquilo funcionaba. En el día del lanzamiento, la tabla de agregados se convirtió en el objeto más caliente del edificio.
Cuando la latencia subió, el equipo escaló la app horizontalmente. Eso aumentó la concurrencia, que aumentó la contención de locks en las filas calientes. El lag de replicación subió, las réplicas de lectura quedaron atrás y más lecturas golpearon el primario. El primario no “se quedó sin CPU”. Se quedó sin paciencia.
La suposición errónea fue que “escrituras pequeñas son baratas”. Las escrituras pequeñas son baratas hasta que serializan. La solución no fue un nuevo tamaño de instancia. Fue cambiar el diseño: mover agregados a un pipeline asíncrono, usar eventos append-only y reconstruir agregados fuera de línea. También añadieron un tope de concurrencia en el proxy para que la BD pudiera degradarse sin colapsar.
Mini-historia 2: La optimización que salió mal
Un equipo vio waits de redo durante picos y decidió hacer commits más rápidos relajando durabilidad en el primario. Cambiaron la configuración de flush-at-commit en medio de una prueba de carga de fin de trimestre y obtuvieron un gráfico de rendimiento espectacular. El SRE que lo aprobó todavía lamenta haber tenido ojos.
Dos semanas después, el subsistema de almacenamiento tuvo un pequeño fallo y el host reinició. La base de datos recuperó, pero algunas transacciones “recientemente comprometidas” faltaban. La app no tenía escrituras idempotentes en todos los caminos. Habían construido un sistema que asumía que la base de datos era la fuente de la verdad y luego la hicieron ocasionalmente olvidadiza.
Dedicaron los días siguientes a conciliar pagos y acciones de usuarios desde logs de aplicación. Nadie lo disfrutó. El incidente no fue que perdieron datos; fue que perdieron confianza. Los clientes notan eso antes de notar un pico de latencia de 200ms.
La lección fue clara: los cambios de durabilidad son decisiones de producto. Si quieres relajarlos necesitas controles compensatorios: claves idempotentes, reconciliación y aceptación clara de RPO/RTO. Si no, estás “optimizando” hacia una reunión de cumplimiento.
Mini-historia 3: La práctica aburrida pero correcta que salvó el día
Otra compañía corría Percona Server en primarios y MySQL estándar en servicios internos. Su diferenciador no fue el binario. Fue la disciplina: mantenían un playbook, lo practicaban y prehabilitaron la instrumentación adecuada en una configuración de bajo overhead.
Cuando una integración de un partner se descontroló y machacó un endpoint, la BD empezó a mostrar waits de redo elevados y una edad de checkpoint creciente. En minutos, el on-call extrajo los waits principales, identificó el digest culpable y lo relacionó con un feature flag. Lo desactivaron. Luego throttlearon al partner en el borde.
Porque tenían una regla vigente—no grandes transacciones y todos los jobs por lotes fragmentados—el sistema se recuperó rápido. Las réplicas quedaron con lag pero no inútiles. Nada de tuning heroico. Ningún cambio sorpresa en la config. Solo guardrails aburridos funcionando como se diseñaron.
El postmortem parecía una lista de la compra: endurecer límites de tasa, añadir una caché para ese endpoint y crear una alerta por crecimiento de la edad del checkpoint. Nadie recibió medalla. Todos durmieron.
Listas de verificación / plan paso a paso
Endurecimiento previo al pico (haz esto antes de necesitarlo)
- Imponer pooling de conexiones en cada servicio. Audita con un canario que registre la tasa de creación de conexiones.
- Fijar max_connections sensato y probar qué ocurre cuando se alcanza. El comportamiento correcto es “fallo controlado”, no “DB OOM”.
- Habilitar observabilidad ligera: slow query log (con umbral razonable), Performance Schema digests + resúmenes de waits, y envío de métricas OS.
- Definir postura de durabilidad:
innodb_flush_log_at_trx_commitysync_binlogno son perillas de ajuste; son políticas de pérdida de datos. - Validar latencia de almacenamiento bajo ráfagas de escritura. Mide fsync y escrituras pequeñas aleatorias; no confíes en números pico del proveedor.
- Fragmentar todos los jobs por lotes por defecto: transacciones acotadas, pausas entre fragmentos y “parar si hay lag de replicación”.
- Practicar failover y enrutamiento de lecturas con conciencia de lag. Si las réplicas se atrasan, tu enrutamiento debe degradarse con gracia.
Durante un pico (la secuencia segura)
- Estabilizar al paciente: limitar tasa, desactivar funciones no esenciales, parar jobs por lotes y capear la concurrencia.
- Clasificar el cuello de botella: CPU vs I/O vs bloqueos (Guion de diagnóstico rápido arriba).
- Matar al mayor culpable: la transacción gigante, la consulta fuera de control, el DDL accidental.
- Proteger la replicación: mantener réplicas “lo bastante buenas” para servir lecturas; reducir patrones de transacciones grandes.
- Recopilar evidencia: capturar top waits, top digests, snapshots de iostat y muestras de processlist para el postmortem.
Limpieza post-pico (no te saltes esto)
- Convertir la mitigación en política: el feature flag, el límite de tasa, la fragmentación de lotes, la puerta para DDL.
- Arreglar la forma de las consultas: índices, reescritura de consultas, reducir temp tables, reducir filas calientes.
- Repetir un simulacro de pico: el mismo escenario debería ser aburrido la próxima vez.
Preguntas frecuentes
1) ¿Percona Server es “solo MySQL con extras”?
Mayormente, sí—misma línea base, más características específicas de Percona, decisiones de empaquetado y a menudo instrumentación más amigable para ops. En picos, los “extras” importan cuando proporcionan control de admisión (thread pool) y mejor introspección.
2) Si estoy en MySQL 8.0, ¿sigo beneficiándome de Percona Server?
Quizá. Las mejoras de MySQL 8.0 reducen la brecha. La decisión suele resumirse en: ¿necesitas la disponibilidad del thread pool de Percona, plugins específicos y valores por defecto operativos—y confías en tu pipeline de upgrade/patch con esa distribución?
3) ¿Qué patrón de pico favorece más a Percona Server?
Tormentas de conexiones y OLTP de alta concurrencia donde el thread pool evita el colapso del scheduler de CPU. Si tus picos son mayormente bound por I/O por escrituras, la ventaja es más sobre observabilidad y ergonomía de ajuste que sobre throughput bruto.
4) ¿Un thread pool reducirá mi latencia?
A menudo aumenta ligeramente la latencia media mientras reduce drásticamente la latencia cola y previene el colapso total. Piénsalo como “menos caos por segundo”, no como “consultas más rápidas”.
5) ¿Debo cambiar ajustes de durabilidad para sobrevivir picos?
Sólo si el negocio acepta explícitamente el riesgo y tienes controles compensatorios. Si no, arregla el patrón de escrituras, la latencia del almacenamiento y la concurrencia. La durabilidad es un requisito de producto con una llave de configuración.
6) ¿Por qué la peor latencia ocurre después de que el pico termina?
Porque InnoDB aún tiene que flushar páginas sucias y avanzar checkpoints. El pico crea deuda; la tormenta de flush es el cobrador de esa deuda.
7) ¿Cómo sé si el lag de replicación es “respaldo normal” o “estancado”?
Si el lag disminuye de forma constante una vez que baja la tasa de escritura, es backlog. Si se estanca, revisa el estado del applier por waits de dependencia de commit, waits de locks o errores; busca transacciones enormes y eventos MDL.
8) ¿Pueden mejores índices arreglar problemas de picos?
A veces, sí—especialmente para inundaciones de lectura y tablas temporales en disco. Pero los índices no arreglan tormentas de conexiones, límites de fsync del redo log o contención por filas calientes. Diagnostica la clase de cuello de botella primero.
9) ¿Cuál es la “victoria rápida” más segura para reducir riesgo de picos?
Pooling de conexiones más un tope de concurrencia (proxy o app). Es aburrido, medible y no apuesta por la corrección.
10) ¿Debería ejecutar instrumentación más pesada sólo en réplicas?
A menudo es un buen compromiso. Mantén primarios con instrumentación ligera; ejecuta trazas de sentencias más profundas o consumidores pesados de Performance Schema en una réplica que refleje la carga lo suficiente como para ser útil.
Siguientes pasos que puedes hacer esta semana
- Realiza un simulacro de picos en staging: simula un ramp-up de conexiones 10x y una ráfaga de escrituras 5x. Captura vmstat/iostat y Performance Schema top waits.
- Decide el control de admisión: si puedes usar thread pool de Percona (o un proxy), implántalo; de lo contrario impón pooling y limita conexiones fuertemente.
- Audita por transacciones gigantes: encuentra y fragmenta jobs por lotes; crea guardrails para que un “UPDATE a todo” no se despliegue sin aviso.
- Mide el comportamiento de checkpoints durante pruebas de carga: verifica crecimiento de páginas sucias y patrones de wait en redo; afina hints de flushing para que coincidan con tu almacenamiento real.
- Escribe tu playbook on-call usando la secuencia de Guion de diagnóstico rápido y los comandos anteriores. Practícalo una vez cuando nadie esté en pánico.
Si eliges entre MySQL y Percona Server específicamente por rendimiento frente a picos, escoge el que te dé mejor control de admisión y acceso más rápido a la verdad durante incidentes en tu entorno. Los benchmarks no te llaman a emergencias. Los picos sí.