Hiciste la actualización en staging. Las pruebas pasaron. Los paneles estaban verdes. Y luego la producción te ataca a las 2 a.m. con un misterio: aumento del lag de replicación, duplicación de la latencia p95,
y un slow query log que se lee como una novela de suspense escrita por un ORM nervioso.
La mentira de «funciona en staging» rara vez es maliciosa. Normalmente es física: diferente forma de los datos, distinta concurrencia, comportamiento distinto del almacenamiento, distintos
SQL en casos límite, plugins diferentes, TLS distinto, y aquello que nadie quiere admitir—una urgencia humana diferente.
MariaDB vs Percona Server: qué cambia realmente cuando actualizas
«MariaDB vs Percona Server» suele presentarse como un debate filosófico. En producción, es más como mudarse de apartamento: tus muebles encajan en su mayoría, pero los
marcos de las puertas son distintos, la presión del agua cambia, y el casero tiene opiniones sobre qué cuenta como «estándar».
Percona Server es un fork de MySQL con instrumentación adicional, funciones de rendimiento y perillas operativas. MariaDB es su propio fork con su propia trayectoria de optimizador,
opciones de replicación, motores de almacenamiento y versionado. Ambos pueden ejecutar InnoDB (en épocas antiguas el InnoDB de MariaDB fue XtraDB y con el tiempo divergió),
ambos hablan «mayormente MySQL» y ambos aceptarán con gusto el SQL de tu aplicación hasta el día en que no lo hagan.
La compatibilidad no es una casilla; es una matriz
Cuando la gente dice «compatible», normalmente quieren decir «los clientes se conectan y el CRUD básico funciona». Ese no es el umbral. Tu umbral es:
la misma corrección, el mismo rango de rendimiento, el mismo comportamiento ante fallos bajo carga real y con datos reales.
Las partes complicadas al actualizar entre MariaDB y Percona Server (o al actualizar dentro de cualquiera de las dos familias) tienden a agruparse en cinco áreas:
- Semántica SQL y planes del optimizador: misma consulta, plan diferente; mismo plan, estimaciones de filas diferentes; valores por defecto distintos para modos SQL.
- Comportamiento de replicación: sabor de GTID, valores por defecto de binlog format, seguridad frente a crash, ajustes de replicación paralela.
- Comportamiento de InnoDB/redo/undo/log: dimensionamiento del redo log, valores por defecto de política de flush, cambios en doublewrite, valores por defecto de checksum.
- Autenticación/TLS/plugins: plugins de auth, versiones/cifrados TLS por defecto, disponibilidad y nombre de plugins.
- Suposiciones de herramientas operativas: expectativas de Percona Toolkit, tablas del sistema, diferencias en performance_schema/information_schema.
Si migras entre familias (MariaDB ↔ Percona Server), el error más grande es pensar que la historia de la actualización es la misma que
un «bump» de versión menor de MySQL. No lo es. Estás cruzando una frontera de divergencia, y la divergencia es donde staging miente con más fuerza.
Una idea parafraseada de Gene Kim (autor sobre fiabilidad/operaciones): Mejorar los resultados viene de mejorar el sistema, no de pedirle a la gente que se esfuerce más bajo presión.
Trata tu actualización como diseño de sistema, no como trabajo de héroe.
Contexto histórico útil para un postmortem
La historia no arregla outages, pero explica por qué tu «actualización simple» tiene bordes afilados. Aquí hay hechos que vale la pena conservar al planificar:
- MariaDB nació como un fork tras la adquisición de MySQL por Oracle (era 2009–2010), lo que moldeó su posicionamiento de «gobernanza abierta» y su ritmo de divergencia.
- Percona Server surgió de una cultura de ajuste en producción: el fork priorizó instrumentación de rendimiento y controles operativos para sistemas muy cargados.
- GTID no es una cosa universal: la implementación de GTID de MariaDB difiere de la de Oracle MySQL/Percona, lo que importa en replicación entre familias y en herramientas de failover.
- Performance Schema maduró significativamente entre MySQL 5.6 → 5.7 → 8.0, y Percona suele seguir el modelo de MySQL; la instrumentación y la historia del sys schema de MariaDB difiere.
- MariaDB introdujo características sin equivalente en MySQL (por ejemplo, algunas estrategias del optimizador, tablas versionadas por el sistema), que pueden cambiar el comportamiento de ejecución.
- Percona Toolkit se convirtió en un estándar operativo de facto en muchas empresas; funciona mejor cuando el servidor se comporta según las expectativas de MySQL/Percona.
- Los valores por defecto de InnoDB han cambiado entre generaciones (dimensionamiento de redo log, heurísticas de flushing, manejo de metadatos). Las actualizaciones a menudo cambian valores por defecto incluso si tu archivo de configuración no lo hace.
- Los plugins de autenticación evolucionaron (caching_sha2_password en el mundo MySQL 8.0; valores por defecto diversos en otros sitios), haciendo que «el cliente funciona en staging» dependa de la versión exacta del conector.
- UTF8 se volvió un tema político: la diferencia entre utf8 (3 bytes) y utf8mb4 (4 bytes) y cómo se eligen collations por defecto ha causado innumerables sorpresas de «pasó las pruebas».
Por qué staging miente: modos de fallo principales
1) La distribución de tus datos es falsa
Staging normalmente tiene menos filas, menos claves calientes, menos valores atípicos patológicos y menos fragmentación. InnoDB se comporta distinto cuando el conjunto de trabajo cabe
en memoria frente a cuando no. El optimizador se comporta distinto cuando los histogramas y las estadísticas de índices representan skew real.
Una mentira clásica: en staging la distribución de user_id es uniforme; en producción hay un «tenant empresarial» que posee la mitad de las filas. Tu consulta «segura» se convierte en
un hotspot tipo mutex bajo carga.
2) Tu concurrencia es imaginaria
La mayoría de pruebas de carga en staging se ejecutan con un nivel de concurrencia educado y con cache caliente. Producción es una estampida. La contención de locks, el purge lag, la presión de redo y
el coste de fsync son todos no lineales. No puedes extrapolar de «funciona a 50 QPS» a «funciona a 5.000 QPS» solo porque la consulta sea la misma.
3) Cambiaron los valores por defecto, pero no te diste cuenta
Una actualización puede cambiar valores por defecto para binlog format, modos SQL, heurísticas de flushing de InnoDB, manejo de tablas temporales, versiones TLS y más. Si staging usa un
my.cnf afinado a mano pero producción tiene drift de configuración (o viceversa), no tienes una prueba de actualización—tienes una comparación de dos universos distintos.
4) La pila de almacenamiento no es la misma
«Mismo tipo de instancia» no es «misma E/S». Tu volumen de staging puede estar tranquilo, tu volumen de producción puede estar contendido. Staging puede ser NVMe local,
producción puede ser adjunto por red. Las opciones de montaje del sistema de archivos pueden diferir. Y sí, la política de caché del controlador RAID importa.
Broma #1: El almacenamiento es el lugar donde «pero es SSD» va a morir, usualmente durante esa ventana de mantenimiento que no puedes extender.
5) La observabilidad difiere, así que detectas problemas más tarde
Staging a menudo tiene más logging de depuración, menos tráfico y menos restricciones de cumplimiento. Producción suele tener retención de logs más estricta, menos métricas y más
cosas gritando al CPU. Cuando actualizas, también cambias lo que es medible y lo que es caro de medir.
6) El comportamiento de tu aplicación cambia con flags de producción
Staging rara vez ejecuta exactamente las mismas feature flags, timeouts, lógica de reintento, circuit breakers, profundidades de cola y tamaños de lote. Una actualización de base de datos puede exponer
un cambio menor en la latencia de consulta, lo que desencadena una tormenta de reintentos, que se convierte en amplificación de escrituras, que termina siendo tu fin de semana.
Guía rápida de diagnóstico (primera/segunda/tercera)
Cuando la actualización sale mal, no tienes tiempo para admirar gráficos. Necesitas encontrar el cuello de botella rápido y elegir una mitigación segura. Aquí tienes una guía rápida que funciona tanto para MariaDB como para Percona Server.
Primero: ¿la base de datos está limitada por CPU, por I/O o por locks?
- Limitada por CPU: mysqld con alto uso de CPU, mucho tiempo «executing», muchas handler reads, cambios de plan, índices faltantes o elecciones del optimizador distintas.
- Limitada por I/O: iowait alto, baja tasa de aciertos en buffer pool, presión de redo/fsync, tablas temporales desbordándose, problemas de edad de checkpoint.
- Limitada por locks: hilos esperando row locks, metadata locks o latches internas; hilo SQL de replicación atascado; DDL bloqueando.
Segundo: ¿el problema está concentrado en un patrón de consulta?
Busca una o dos huellas dominantes: una consulta que cambió de plan, un hilo en background saturando I/O, o un cuello de botella en la aplicación de la replicación. Si el principal
culpable explica más del ~30–40% del tiempo, trátalo como la causa del incidente hasta que se demuestre lo contrario.
Tercero: ¿es corrección, rendimiento o estabilidad?
- Corrección: desajustes de datos, truncamiento silencioso, cambios de colación, diferencias en modo SQL, deriva de zonas horarias.
- Rendimiento: regresión de latencia, caída de throughput, lag de replicación, tormentas de conexiones.
- Estabilidad: crashes, OOM kills, disco lleno, logs corruptos, fallos de plugin, fallos de negociación TLS.
Tu respuesta difiere. Los problemas de corrección suelen implicar rollback o modo read-only hasta verificar. Los problemas de rendimiento normalmente significan mitigar carga y revertir configuraciones.
Los problemas de estabilidad significan detener el sangrado: mantener los datos seguros y luego diagnosticar.
Tareas prácticas de verificación (comandos, salidas, decisiones)
Estas son las tareas que separan «probamos» de «probamos y demostramos». Cada tarea tiene: un comando, una salida de ejemplo, qué significa y qué decisión tomar.
Ejecútalas en staging y producción antes y después de la actualización. Haz diff de los resultados. Si no haces diff, básicamente confías en corazonadas.
Tarea 1: Confirma la identidad del servidor y la línea de versión
cr0x@server:~$ mysql -NBe "SELECT VERSION(), @@version_comment, @@version_compile_machine;"
8.0.36-28 Percona Server (GPL), Release 28, Revision 1234abcd x86_64
Significado: VERSION() y version_comment te dicen si estás en MariaDB, Percona u otra cosa disfrazada.
Decisión: Si no es la distribución/versión exacta prevista, para. Tu «actualización» puede ser un problema de repositorio de paquetes mezclados.
Tarea 2: Volcar y diferenciar variables en tiempo de ejecución que causan cambios de comportamiento
cr0x@server:~$ mysql -NBe "SHOW VARIABLES WHERE Variable_name IN ('sql_mode','binlog_format','transaction_isolation','innodb_flush_log_at_trx_commit','sync_binlog','character_set_server','collation_server','log_bin','gtid_mode','enforce_gtid_consistency');"
sql_mode ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION
binlog_format ROW
transaction_isolation REPEATABLE-READ
innodb_flush_log_at_trx_commit 1
sync_binlog 1
character_set_server utf8mb4
collation_server utf8mb4_0900_ai_ci
log_bin ON
gtid_mode ON
enforce_gtid_consistency ON
Significado: Estas variables moldean la corrección, la seguridad de la replicación y el rendimiento. También cambian entre versiones mayores y entre forks.
Decisión: Si staging y producción no coinciden, arregla el drift antes de confiar en los resultados de prueba. Si una actualización cambia valores por defecto, fíjalos explícitamente.
Tarea 3: Verificar el tamaño del buffer pool de InnoDB y la presión
cr0x@server:~$ mysql -NBe "SHOW VARIABLES LIKE 'innodb_buffer_pool_size'; SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_reads'; SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read_requests';"
innodb_buffer_pool_size 34359738368
Innodb_buffer_pool_reads 918273
Innodb_buffer_pool_read_requests 5544332211
Significado: Reads vs read_requests aproxima los misses de cache. Un salto en buffer_pool_reads tras la actualización indica a menudo nuevo conjunto de trabajo,
cambio de plan o churn en background.
Decisión: Si la tasa de misses empeoró significativamente, probablemente estés limitado por I/O. Aumenta el buffer pool (si es seguro), corrige planes de consulta o reduce churn.
Tarea 4: Inspeccionar la presión de redo log y checkpoint (InnoDB log waits)
cr0x@server:~$ mysql -NBe "SHOW GLOBAL STATUS LIKE 'Innodb_log_waits'; SHOW VARIABLES LIKE 'innodb_redo_log_capacity';"
Innodb_log_waits 742
innodb_redo_log_capacity 4294967296
Significado: Innodb_log_waits > 0 significa que transacciones en primer plano esperaron por redo. Eso es el clásico «tu redo es demasiado pequeño o el flush es muy lento.»
Decisión: Si los waits aumentan durante la carga, incrementa la capacidad de redo (donde sea soportado), revisa los ajustes de flushing y comprueba la latencia de fsync del almacenamiento.
Tarea 5: Revisar latencias de disco y saturación en el nivel del SO
cr0x@server:~$ iostat -x 1 3
Linux 6.5.0 (db-prod-01) 12/30/2025 _x86_64_ (16 CPU)
avg-cpu: %user %nice %system %iowait %steal %idle
22.11 0.00 6.08 18.47 0.00 53.34
Device r/s w/s rkB/s wkB/s aqu-sz await svctm %util
nvme0n1 320.0 980.0 20480.0 65536.0 9.40 8.90 0.55 72.00
Significado: Await alto y util alto indican presión de almacenamiento. iowait > ~10–20% durante el incidente es una pista fuerte.
Decisión: Si await se dispara con la actualización, sospecha redo/fsync, spills de tablas temporales o cambios en el flushing en background. Mitiga reduciendo la tasa de escritura,
aumentando memoria o moviendo a volúmenes más rápidos.
Tarea 6: Validar salud y coordenadas de replicación
cr0x@server:~$ mysql -e "SHOW REPLICA STATUS\G" | egrep 'Replica_IO_Running|Replica_SQL_Running|Seconds_Behind_Source|Last_SQL_Error|Retrieved_Gtid_Set|Executed_Gtid_Set'
Replica_IO_Running: Yes
Replica_SQL_Running: Yes
Seconds_Behind_Source: 84
Last_SQL_Error:
Retrieved_Gtid_Set: 3b2c1d10-aaaa-bbbb-cccc-111111111111:1-982733
Executed_Gtid_Set: 3b2c1d10-aaaa-bbbb-cccc-111111111111:1-982649
Significado: Un aumento de Seconds_Behind_Source y una brecha GTID creciente indica que la aplicación no puede ponerse al día.
Decisión: Si el lag aparece solo después de la actualización, revisa ajustes de replicación paralela, binlog format y la presión de fsync en la réplica.
Tarea 7: Atrapar regresiones de plan con EXPLAIN y optimizer trace (dirigido)
cr0x@server:~$ mysql -e "EXPLAIN SELECT o.id FROM orders o JOIN customers c ON c.id=o.customer_id WHERE c.email='x@example.com' AND o.status='OPEN'\G"
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: c
type: ref
possible_keys: idx_email
key: idx_email
rows: 1
filtered: 100.00
Extra: Using index
*************************** 2. row ***************************
table: o
type: ref
possible_keys: idx_customer_status
key: idx_customer_status
rows: 12
filtered: 10.00
Extra: Using where
Significado: Buscas cambios en el orden de joins, tipo de acceso y estimaciones de filas. «type: ALL» donde antes había «ref» es una señal de alarma.
Decisión: Si el plan cambió, considera actualizar estadísticas, añadir índices o forzar con hints (último recurso). También verifica que las colaciones y tipos de datos coincidan.
Tarea 8: Comparar frescura de estadísticas de tablas
cr0x@server:~$ mysql -NBe "SELECT table_schema, table_name, update_time FROM information_schema.tables WHERE table_schema='app' ORDER BY update_time DESC LIMIT 5;"
app orders 2025-12-30 01:12:44
app customers 2025-12-30 01:10:02
app order_items 2025-12-29 23:45:11
app invoices 2025-12-29 21:03:19
app payments 2025-12-29 19:58:07
Significado: update_time puede ser engañoso según el engine y la configuración, pero es una pista rápida sobre si se ejecutó mantenimiento y si las tablas están «frescas».
Decisión: Si las tablas parecen obsoletas después de la migración/importación, ejecuta ANALYZE TABLE en tablas críticas y vuelve a comprobar EXPLAIN para consultas clave.
Tarea 9: Comprobar spills a tablas temporales y presión de sort
cr0x@server:~$ mysql -NBe "SHOW GLOBAL STATUS LIKE 'Created_tmp%'; SHOW VARIABLES LIKE 'tmp_table_size'; SHOW VARIABLES LIKE 'max_heap_table_size';"
Created_tmp_tables 182773
Created_tmp_disk_tables 44291
Created_tmp_files 9312
tmp_table_size 134217728
max_heap_table_size 134217728
Significado: Una alta proporción de Created_tmp_disk_tables significa que las consultas están derramando a disco. Las actualizaciones pueden cambiar el comportamiento interno de tablas temporales y valores por defecto.
Decisión: Si los spills a disco aumentaron tras la actualización, incrementa tmp_table_size/max_heap_table_size con cautela y corrige las consultas e índices culpables.
Tarea 10: Identificar patrones de consulta principales por digest (performance schema)
cr0x@server:~$ mysql -NBe "SELECT DIGEST_TEXT, COUNT_STAR, ROUND(SUM_TIMER_WAIT/1e12,2) AS total_s, ROUND(AVG_TIMER_WAIT/1e9,2) AS avg_ms FROM performance_schema.events_statements_summary_by_digest ORDER BY SUM_TIMER_WAIT DESC LIMIT 3;"
SELECT * FROM orders WHERE customer_id = ? AND status = ? ORDER BY created_at DESC LIMIT ? 882771 912.22 1.03
UPDATE inventory SET qty = qty - ? WHERE sku = ? AND qty >= ? 221009 644.11 2.91
SELECT id FROM customers WHERE email = ? 775002 312.88 0.40
Significado: Si un digest domina el tiempo total, tienes un objetivo quirúrgico. Si todo se volvió más lento de manera uniforme, sospecha I/O o contención global.
Decisión: Arregla primero los digests superiores. Si el digest principal aparece solo después de la actualización, sospecha cambios de plan o SQL cambiado por el conector de la app.
Tarea 11: Buscar acumulaciones de metadata locks (el asesino silencioso)
cr0x@server:~$ mysql -e "SELECT OBJECT_SCHEMA, OBJECT_NAME, LOCK_TYPE, LOCK_STATUS, COUNT(*) cnt FROM performance_schema.metadata_locks GROUP BY 1,2,3,4 ORDER BY cnt DESC LIMIT 5;"
+--------------+-------------+-----------+-------------+-----+
| OBJECT_SCHEMA | OBJECT_NAME | LOCK_TYPE | LOCK_STATUS | cnt |
+--------------+-------------+-----------+-------------+-----+
| app | orders | SHARED_READ | GRANTED | 132 |
| app | orders | EXCLUSIVE | PENDING | 1 |
+--------------+-------------+-----------+-------------+-----+
Significado: Un lock EXCLUSIVE pendiente más un montón de shared locks concedidos es el clásico «alguien inició un DDL y ahora todos esperan.»
Decisión: Si ves esto durante la actualización, detén el DDL o prográmalo apropiadamente con herramientas de cambio de esquema online y ventanas fuera de pico.
Tarea 12: Verificar el error log para regresiones de plugin/auth/TLS
cr0x@server:~$ sudo tail -n 30 /var/log/mysql/error.log
2025-12-30T01:20:11.123456Z 0 [Warning] [MY-010068] [Server] CA certificate ca.pem is self signed.
2025-12-30T01:20:15.765432Z 12 [ERROR] [MY-010054] [Server] Slave SQL: Error 'Unknown collation: 'utf8mb4_0900_ai_ci'' on query. Default database: 'app'. Query: 'CREATE TABLE ...', Error_code: 1273
Significado: Mismatches de colación y advertencias TLS son tripwires comunes entre versiones/forks. «Unknown collation» es un problema de frontera en la migración.
Decisión: Si la colación no es soportada en el destino, debes convertir el esquema/las columnas o elegir una colación compatible antes de replicar/importar.
Tarea 13: Validar drift de esquema entre staging y producción (DDL diff vía mysqldump)
cr0x@server:~$ mysqldump --no-data --routines --triggers --databases app | sha256sum
c0a5f4c2f6d7f6bb38a39b9c1d9e2c11caa8d9b7a0f1b2c3d4e5f6a7b8c9d0e1 -
Significado: Hashear el volcado de esquema es tosco, pero eficaz. Si los hashes difieren, tus entornos no son equivalentes.
Decisión: Si el esquema difiere, deja de afirmar que staging validó producción. Reconstruye staging desde el esquema de producción (y preferiblemente con datos enmascarados).
Tarea 14: Verificar corrección de backup/restore antes de tocar producción
cr0x@server:~$ xtrabackup --prepare --target-dir=/backups/xb_2025-12-30_0100
xtrabackup: This target seems to be prepared.
Significado: Un backup que no has preparado/restaurado es una teoría, no un respaldo. Prepare valida la capacidad del backup para volverse consistente.
Decisión: Si prepare falla o tarda mucho más tras la actualización, considéralo un bloqueador de release: tu ruta de recuperación está comprometida.
Tarea 15: Ejecutar un micro-benchmark con forma de producción (no un benchmark de vanidad)
cr0x@server:~$ sysbench oltp_read_write --mysql-host=127.0.0.1 --mysql-user=sb --mysql-password=sb --mysql-db=sbtest --tables=16 --table-size=500000 --threads=64 --time=120 --report-interval=10 run
SQL statistics:
queries performed:
read: 1456720
write: 416206
other: 208103
transactions: 104051 (867.03 per sec.)
Latency (ms):
min: 2.31
avg: 73.81
max: 512.24
Significado: No se trata del TPS absoluto; se trata de comparar antes/después en el mismo hardware y configuración. Observa la latencia y los picos máximos.
Decisión: Si la latencia máxima explota tras la actualización, probablemente tengas problemas de fsync/redo, contención de mutex o spills temporales. No lo lleves a producción.
Tarea 16: Validar errores de conexión y comportamiento del handshake
cr0x@server:~$ mysqladmin -uroot -p extended-status | egrep 'Aborted_connects|Connection_errors_internal|Connection_errors_max_connections|Threads_connected'
Aborted_connects 17
Connection_errors_internal 0
Connection_errors_max_connections 4
Threads_connected 412
Significado: Las actualizaciones pueden cambiar valores por defecto de plugins de auth, requisitos TLS y timeouts. Picos en aborted_connects pueden indicar incompatibilidad de clientes o carga.
Decisión: Si los aborted connects aumentan solo después de la actualización, valida las versiones de los conectores, la configuración TLS y max_connections/thread_cache.
Broma #2: Lo único más confiado que un ORM es un ORM después de que actualizas la base de datos debajo de él.
Tres microhistorias corporativas (dolorosas, reales, útiles)
Microhistoria 1: El incidente causado por una suposición incorrecta (GTID «es GTID»)
Una empresa SaaS mediana planeó una «migración simple»: reemplazar un cluster MariaDB envejecido por Percona Server porque la nueva pila de observabilidad hablaba
MySQL/Percona con más fluidez. Staging fue bien. La replicación funcionó. Los ejercicios de failover parecían suficientemente limpios para aprobar.
La suposición equivocada fue sutil: asumieron que el comportamiento de GTID era portable como «el puerto 3306 es portable». En staging usaron dumps lógicos y réplicas de corta vida.
En producción dependían de automatización basada en GTID en su tooling de failover y esperaban que siguiera funcionando tras el cutover.
Durante el cutover en producción, la réplica promovida arrancó, pero la automatización se negó a reanexar un nodo rezagado. Luego una segunda réplica aplicó transacciones
en un orden que no coincidía con sus expectativas. Nadie perdió datos, pero perdieron tiempo, y el tiempo es un bug de disponibilidad.
El postmortem encontró el verdadero culpable: el plan de migración trató al GTID de MariaDB y al GTID de MySQL/Percona como intercambiables lo suficiente para «funcionar», sin
validar las suposiciones del tooling de failover. Staging no incluyó la capa de automatización ni la topología de replicación de larga vida.
La solución no fue heroica. Reconstruyeron el plan: trata el sabor de GTID como una frontera rígida, valida procedimientos de failover de extremo a extremo e incluye la herramienta de automatización
en los ensayos de staging con réplicas de larga vida e inyección realista de lag. El siguiente cutover fue aburrido, que es el mayor halago en operaciones.
Microhistoria 2: La optimización que salió mal (redo logs más grandes, peor latencia de cola)
Un equipo fintech actualizó Percona Server dentro de la misma familia mayor y vio stalls intermitentes de escritura en producción. Alguien notó Innodb_log_waits subiendo
y tomó una decisión razonable: aumentar la capacidad de redo. Redo más grande significa menos checkpoints, menos stalls. Esa es la folklore.
Implementaron el cambio, y la latencia media mejoró. Luego la latencia de cola se volvió extraña. La latencia p99 de escritura se disparó durante ráfagas de tráfico, y las réplicas
quedaron más rezagadas. Los gráficos parecían un mar en calma con monstruos marinos ocasionales.
El problema real no era el tamaño del redo; era el comportamiento de I/O bajo carga ráfaga combinado con las características del almacenamiento. Redo más grande cambió el timing
del flush y del trabajo de checkpoint. En lugar de trabajo de fondo pequeño y frecuente, crearon fases de writeback más grandes y menos predecibles que se alinearon mal con la variabilidad
de throughput del volumen de almacenamiento.
La solución fue dejar de tratar el redo como una perilla única. Instrumentaron la latencia de fsync, ajustaron el flushing, verificaron los settings de doublewrite y garantizaron
que las réplicas tuvieran suficiente margen de I/O. El tamaño del redo quedó mayor que antes—pero no «lo más grande posible» y no sin verificar el comportamiento de las colas.
La lección: los ajustes de rendimiento no son caramelos gratis. Cualquier perilla que cambie cuándo sucede el trabajo puede convertirse en generadora de latencia tail. Si solo observas promedios,
vas a liberar regresiones con una sonrisa.
Microhistoria 3: La práctica aburrida pero correcta que salvó el día (hash de esquema + congelación de config)
Un equipo empresarial tuvo que pasar de MariaDB a Percona Server por restricciones de proveedor en su plataforma. No estaban entusiasmados. También eran disciplinados, que es la versión adulta de estar entusiasmado.
Comenzaron con dos no negociables: una congelación de configuración y una puerta de hash de esquema. Durante cuatro semanas previas al cutover, se prohibieron cambios en my.cnf de producción
a menos que estuvieran ligados a un incidente. Cualquier cambio requería revisión de diff y un replay en staging. Mientras tanto, staging se reconstruía nightly desde el esquema de producción, y el volcado de esquema se
hasheaba y comparaba en CI.
Durante el ensayo general, la puerta del hash de esquema detectó una pequeña diferencia: una colación en una tabla de staging no coincidía con producción. No fue intencional.
Fue el residuo de una migración a medias meses antes que nunca llegó a producción.
Arreglaron staging para que coincidiera con producción, reejecutaron el ensayo de actualización y encontraron una regresión de plan de consulta que solo aparecía con la colación de producción y
la cardinalidad del índice. Esta es la parte donde la gente dice «captura afortunada». No fue suerte. Fue proceso aburrido cumpliendo su función.
La noche del cutover fue sin incidentes. Las mejores historias de actualizaciones no entran en la tradición de la empresa porque a nadie le gusta recordar cosas que salieron según lo planeado.
Errores comunes: síntoma → causa raíz → solución
1) Síntoma: la replicación falla con «Unknown collation» o «Unknown character set»
Causa raíz: El esquema de origen usa una colación/charset no soportada en el destino (común en fronteras MariaDB ↔ MySQL/Percona, y entre MySQL 5.7 → 8.0).
Solución: Convierte el esquema antes de la migración. Estandariza en utf8mb4 y elige collations soportadas por ambos lados. Verifica restaurando un volcado solo de esquema en el destino.
2) Síntoma: las consultas se volvieron más lentas, pero la CPU es la misma y el I/O es mayor
Causa raíz: los misses de buffer pool aumentaron por cambios de plan o comportamiento distinto de tablas temporales; los datos de staging cabían en RAM, producción no.
Solución: Compara planes EXPLAIN, actualiza estadísticas (ANALYZE), añade/ajusta índices y verifica tmp_table_size y diferencias del motor temporal interno.
3) Síntoma: p99 de latencia de escritura se dispara después de la actualización
Causa raíz: presión sobre redo/fsync, heurísticas de flushing cambiadas, o variabilidad de la pila de almacenamiento. A veces causado por cambios «útiles» de configuración durante la actualización.
Solución: Mide latencia de fsync (SO + base de datos), revisa innodb_flush_log_at_trx_commit y sync_binlog, ajusta la capacidad de redo con responsabilidad y asegura margen de I/O en los volúmenes.
4) Síntoma: «demasiadas conexiones» o tormentas de conexiones tras el cutover
Causa raíz: incompatibilidad de handshake/auth plugin de clientes, fallos en negociación TLS o reintentos de la aplicación que amplifican regresiones leves de latencia.
Solución: Fija el auth plugin y settings TLS, valida versiones de conectores en pruebas tipo producción, ajusta timeouts y despliega límites sensatos de connection pooling.
5) Síntoma: DDL provoca stalls en todo el sistema durante la ventana de actualización
Causa raíz: metadata locks; las suposiciones de DDL online no son válidas para esa operación/versión; transacciones largas reteniendo locks.
Solución: Usa herramientas de cambio de esquema online (o DDL online nativo donde sea seguro), mata/evita transacciones largas, establece timeouts de lock y programa DDL antes.
6) Síntoma: las réplicas se atrasan solo después de la actualización
Causa raíz: replicación paralela no afinada o valores por defecto cambiados; binlog format cambiado; diferencias en el orden de commit; I/O de réplica más débil que el primario.
Solución: Verifica binlog_format=ROW, ajusta paralelismo de réplica, aumenta capacidad de I/O de réplica y asegura que la configuración de réplica coincida con la carga post-actualización.
7) Síntoma: «Deadlocks aumentaron» y errores en la app se disparan
Causa raíz: cambios en planes de ejecución o uso de índices que provocan distinto orden de adquisición de locks; mayor concurrencia; valores por defecto de aislamiento/modo SQL distintos.
Solución: Compara planes, añade índices de soporte, reduce el alcance de las transacciones y asegura que transaction_isolation y sql_mode estén fijados y revisados.
8) Síntoma: el disco se llena inesperadamente durante la migración o poco después
Causa raíz: binlogs más grandes, spills de tablas temporales, slow log en modo verbose, artefactos de backup no rotados o crecimiento de redo/undo.
Solución: Reserva presupuesto de espacio, aplica rotación de logs, limita retención, monitoriza /var/lib/mysql y tmpdir, y valida que los scripts de migración limpien sus artefactos.
Listas de verificación / plan paso a paso (aburrido a propósito)
Fase 0: Decide qué tipo de «actualización» estás haciendo realmente
- Actualización dentro de la familia (Percona → Percona, MariaDB → MariaDB): sigue siendo riesgosa, pero las herramientas y la semántica son más cercanas.
- Migración entre familias (MariaDB ↔ Percona): trátala como una migración, no como una actualización. Diferente GTID, diferentes collations, distinto optimizador, tablas del sistema diferentes.
- Salto de versión mayor: asume cambios de comportamiento a menos que se demuestre lo contrario.
Fase 1: Haz que staging deje de mentir
- Reconstruye staging nightly desde producción. Hashea el volcado de esquema. Bloquea si hay mismatch.
- Carga staging con una forma de datos similar a producción: snapshot enmascarado de producción o un dataset generado con skew y cardinalidad similares.
- Iguala la configuración: mismo my.cnf, mismos kernel/sysctl, mismas opciones de montaje del sistema de archivos, misma clase de almacenamiento.
- Reproduce patrones de tráfico de producción (incluyendo ráfagas). No te limites a ejecutar tests unitarios y dar por finalizada la prueba.
- Incluye la capa de automatización: tooling de failover, backups, monitorización, sistema de migración de esquema.
Fase 2: Puertas de preflight de compatibilidad (bloqueadores)
- Chequeo de compatibilidad de colación/charset en todo el esquema.
- Modo SQL y strictness: asegura comportamiento esperado para inserts, group by y conversiones implícitas.
- Plugins de auth y TLS: asegura que cada librería cliente pueda conectarse con los nuevos valores por defecto.
- Plan de replicación: decide la estrategia de GTID; evita «lo resolveremos durante el cutover».
- Ensayo de backup/restore: demuestra que puedes restaurar y promocionar dentro de tu RTO.
Fase 3: Puertas de rendimiento (demuestra que no moviste el cuello de botella a la oscuridad)
- Benchmark con concurrencia de producción y patrones de ráfaga; registra p95/p99 y latencias máximas.
- Compara top digests antes/después; marca nuevos offenders principales y regresiones de plan.
- Valida margen de I/O y comportamiento de redo bajo ráfagas de escritura.
- Verifica spills de tablas temporales y presión de sort.
Fase 4: Plan de cutover que respete la realidad del rollback
- Define triggers de rollback: error de corrección, inestabilidad de replicación, regresión sostenida de latencia, aumento de la tasa de errores.
- Congela cambios de esquema durante la ventana de cutover.
- Reduce el radio de blast: haz canary a un subconjunto de tráfico o tenants, si tu arquitectura lo permite.
- Ejecuta las mismas tareas de verificación durante el cutover que en el ensayo. Mismos comandos, mismos diffs, mismas puertas.
- No improvises «optimizaciones rápidas» durante un incidente a menos que puedas medirlas y revertirlas con seguridad.
FAQ
1) ¿Percona Server es «solo MySQL» y MariaDB es «solo MySQL»?
No en la forma que le importa a tu plan de actualización. Percona sigue de cerca a MySQL pero añade funciones y builds. MariaDB divergió más con el tiempo. Trata los movimientos entre familias
como migraciones con validación de compatibilidad, no como actualizaciones casuales.
2) ¿Puedo replicar de MariaDB a Percona Server (o al revés) durante la migración?
A veces se puede, pero los edge cases son donde sangras: mismatch de sabor GTID, diferencias de colación y suposiciones de formato statement/row. Si lo haces, demuéstralo con un ensayo de replicación
de larga duración, no con una prueba de staging de 30 minutos.
3) ¿Cuál es la causa más común de «funciona en staging»?
La forma de los datos. Los datasets de staging rara vez reproducen skew de producción, fragmentación y claves calientes. Los optimizadores y caches se comportan distinto cuando la realidad es desordenada.
4) ¿Debo fijar todas las variables de configuración antes de actualizar?
Fija las que cambian semántica o durabilidad: sql_mode, binlog_format, transaction isolation, flush y sync_binlog, charset/collation y política de autenticación/TLS. No fijes perillas al azar que no entiendas; vas a fosilizar errores antiguos.
5) ¿Cómo detecto regresiones de plan de consulta temprano?
Captura los digests de consultas principales, ejecuta EXPLAIN en consultas representativas, compara antes/después y valida con estadísticas tipo producción (ANALYZE). También vigila spills temporales y patrones de handler read, no solo el tiempo de consulta.
6) ¿Por qué aumentó el lag de replicación tras la actualización si las escrituras no lo hicieron?
La aplicación en réplica puede estar limitada por apply single-threaded, restricciones de orden de commit, presión de fsync o valores por defecto de replicación paralela diferentes. El primario puede «aguantarlo», pero la réplica no puede aplicarlo con su presupuesto de I/O.
7) ¿Es seguro cambiar ajustes de redo/flush durante un incidente?
Solo si puedes medir el resultado rápidamente y entiendes la compensación de durabilidad. Algunos ajustes reducen el coste de fsync a costa de seguridad frente a crash. Si tu incidente toca la corrección, no cambies integridad por velocidad sin aprobación de nivel ejecutivo.
8) ¿Cómo sé si staging es «suficientemente parecido a producción»?
Cuando tus tareas de verificación difieren limpiamente (esquema/config), tu benchmark reproduce los cuellos de botella de producción y tus drills de fallo (restore de backup, reconstrucción de réplica, failover) se comportan igual. Si no puedes reproducir el dolor de producción en staging, staging no es realista.
9) ¿Debemos usar dumps lógicos o backups físicos para la migración?
Los dumps lógicos son portables pero lentos y pueden cambiar cosas sutiles (definers, collations, character sets). Los backups físicos son rápidos pero requieren compatibilidad de engine/versión. Para movimientos entre familias, muchos equipos combinan métodos: físico dentro de la familia, lógico a través de fronteras, siempre ensayado.
10) ¿Cuál es un criterio razonable de éxito para una actualización?
Corrección validada, replicación estable y rendimiento dentro de un presupuesto definido de regresión (por ejemplo, sin regresión sostenida en p95 y sin explosión en p99). Además: backups/restores siguen funcionando y la monitorización sigue diciendo la verdad.
Próximos pasos que puedes hacer esta semana
Si estás planificando un movimiento MariaDB ↔ Percona Server (o incluso una actualización «fácil» dentro de la familia), no empieces debatiendo características. Empieza por eliminar las mentiras de staging.
- Construye un informe de diferencias: ejecuta las Tareas 1–3 y 13 en ambos entornos y arregla el drift hasta que coincidan.
- Elige 10 consultas críticas: ejecuta EXPLAIN antes/después y registra los planes como artefactos.
- Haz un replay de carga realista: sysbench está bien como baseline, pero también reejecuta una porción de tráfico de producción si puedes.
- Ensaya la recuperación: prepara y restaura backups, demuestra la promoción y verifica que puedes reconstruir una réplica dentro de tu RTO.
- Escribe triggers de rollback: hazlos medibles (lag, tasa de errores, p99) y consigue acuerdo antes de la ventana de mantenimiento.
Las actualizaciones no fallan porque los ingenieros sean descuidados. Fallan porque la realidad es diferente a escala y el plan asumió que no lo sería. Haz de la realidad parte de tu arnés de pruebas, y «funciona en staging» dejará de ser una mentira y será más una predicción útil.