Los 504 en WordPress rara vez son “un problema de WordPress”. Son un síntoma: la petición llegó a tu stack y no pudo salir antes de que el temporizador del gateway llegara a cero. La mayoría de las veces, la base de datos es donde fue a morir —silenciosa, educadamente y con los logs justos para arruinarte la tarde.
Si estás decidiendo entre MySQL y MariaDB para WordPress bajo tráfico con picos, aquí va la verdad directa: cualquiera puede sobrevivir y cualquiera puede colapsar. La base de datos que “colapsa primero” suele ser la que configuraste como si fuera 2014, desplegaste como si fuera una mascota y monitorizaste como si fuera un rumor. Hablemos de modos de fallo, no de lealtad de marca.
Lo que realmente significa un 504 en producción
Un 504 no es “WordPress agotó el tiempo”. Es tu gateway (a menudo Nginx, a veces un ALB, a veces un CDN) diciendo: “Le pedí a un upstream una respuesta y no contestó suficientemente rápido.” El upstream puede ser PHP-FPM, que podría estar bloqueado en MySQL, que podría estar bloqueado en disco, que podría estar bloqueado en un mutex, que podría estar bloqueado por tus decisiones de vida cuestionables.
En un stack típico de WordPress:
- Cliente solicita a CDN o balanceador de carga.
- La petición llega a Nginx/Apache.
- El camino dinámico va a PHP-FPM.
- PHP realiza trabajo de aplicación y llama a MySQL/MariaDB.
- La BD lee desde el buffer pool o disco, bloquea filas, escribe redo/undo y devuelve resultados.
Ante un pico, el eslabón más débil no es necesariamente el componente más lento; es el componente que renuncia menos elegantemente a la carga. Una BD saturada típicamente no falla rápido. Falla encolando. La encolación parece “todavía funciona, solo más lento”, que se convierte en “todo caduca”, que se convierte en “el canal de incidentes ahora es terapia grupal”.
Hay dos formas principales en que una base de datos convierte un pico en 504s:
- Presión de conexiones: demasiados workers simultáneos de PHP intentando conectar o ejecutar consultas, causando explosión de hilos, cambios de contexto o agotamiento de recursos.
- Inflación de latencia: las consultas se vuelven más lentas por IO en disco, contención de locks, misses del buffer pool, lag de purge, presión en redo log o simplemente planes de consulta malos.
Así que “quién colapsa primero” es realmente: qué motor + configuración te da mejor latencia de cola (tail latency) y comportamiento más predecible cuando la cola empieza a formarse.
MySQL vs MariaDB ante un pico de tráfico: quién falla primero (y por qué)
Realidad de base: WordPress no es un benchmark de bases de datos
WordPress no es pureza OLTP. Son muchas lecturas pequeñas, algunas escrituras y un truco de feria llamado wp_options que puede convertirse en tu tabla más caliente del edificio. También incluye SQL de plugins de… moral variable.
Lo que importa en picos:
- Manejo de conexiones: modelo hilo-por-conexión frente a pooling/comportamiento de pool de hilos.
- Comportamiento de InnoDB bajo contención: locks de fila, locks de metadata, purge, flushing.
- Predictibilidad del optimizador: planes estables, decisiones de indexación acertadas.
- Observabilidad y herramientas: ¿puedes ver qué está pasando antes de que los 504 se conviertan en un apagón total?
MySQL: predecible si lo mantienes aburrido, peligroso si lo dejas “por defecto”
MySQL moderno (8.x) es sólido para WordPress. También es, sin pedir disculpas, complejo. Los valores por defecto están diseñados para arrancar en todas partes, no para sobrevivir a que tu homepage salga en la primera página de Internet.
Dónde MySQL suele comportarse bien ante picos:
- Núcleo InnoDB estable con instrumentación madura (Performance Schema, sys schema).
- Mejoras del optimizador a lo largo de los años que a menudo ayudan con cargas mixtas.
- Herramientas y ecosistema de replicación maduras en muchas organizaciones.
Dónde MySQL suele colapsar primero (en stacks reales de WordPress):
- Sismos de conexiones cuando PHP-FPM aumenta workers y cada uno abre una conexión; terminas limitado por CPU en hilos y mutex mucho antes de “quedarte sin CPU” en el sentido habitual.
- Paradas IO-bound cuando el buffer pool está subdimensionado y redo log/flushing están tuneados como si fuera un portátil.
- Acumulación de locks de metadata por DDL o transacciones largas (a menudo tareas de administración “inofensivas” durante pico).
MariaDB: a veces más indulgente con la concurrencia, a veces una trampa de compatibilidad
MariaDB nació como un fork con una cara conocida. Con el tiempo se convirtió en su propia base de datos con personalidad propia. Para WordPress, la diferenciación más práctica que verás en picos está alrededor del comportamiento de concurrencia (especialmente si usas el thread pool de MariaDB) y la familiaridad operativa según lo que tu equipo haya corrido antes.
Dónde MariaDB tiende a comportarse bien ante picos:
- Thread pool (en muchas builds de MariaDB) puede reducir el thrash de hilos bajo cuentas altas de conexiones limitando workers activos y planificando ejecución.
- Perillas operativas que a veces los equipos encuentran más fáciles de manejar, según el empaquetado de la distro y los valores por defecto.
Dónde MariaDB colapsa primero (otra vez, en campo):
- Suposiciones de “drop-in MySQL” que se rompen en comportamientos SQL de borde, variables del sistema o expectativas de herramientas—especialmente si mezclas componentes del ecosistema que suponen semántica de MySQL 8.
- Sorpresas en planes de consulta si estás acostumbrado al optimizador de MySQL y no validas con
EXPLAINdespués de actualizaciones. - Mismatches en replicación/GTID en entornos mixtos durante migraciones, lo que convierte “solo añade un réplica” en “¿por qué está enfadada la réplica?”.
Si quieres la respuesta directa: ante un pico repentino de tráfico en WordPress, lo primero que suele colapsar es la gestión de conexiones, no “MySQL vs MariaDB”. MariaDB con thread pool puede mantenerse en pie más tiempo en una tormenta de conexiones. MySQL también puede hacerlo bien, pero a menudo necesita que seas deliberado: limitar workers de PHP-FPM, usar pooling (ProxySQL u otro), y dejar de fingir que max_connections es una característica de rendimiento.
Broma #1: Un pico de tráfico es la manera que tiene la naturaleza de preguntar si tu “funciona en staging” también paga el alquiler.
Hechos e historia que siguen importando para tu incidente
- Hecho 1: MariaDB se bifurcó de MySQL tras la adquisición de Sun/ MySQL por Oracle en 2010, impulsada por preocupaciones sobre la gobernanza futura de MySQL.
- Hecho 2: MySQL 8.0 introdujo cambios mayores—reforma del diccionario de datos, Performance Schema mejorado y muchas mejoras del optimizador—hacerte un modelo mental de MySQL 5.7 es errado.
- Hecho 3: Los números de versión de MariaDB divergieron deliberadamente (10.x) y no son directamente comparables con MySQL 8.0; tratarlos como “mayor = más nuevo” conduce a expectativas erróneas.
- Hecho 4: En muchas distros, los paquetes “mysql” acabaron siendo meta-paquetes apuntando a MySQL o MariaDB; equipos han descubierto que estaban ejecutando MariaDB por accidente. Esto es divertido exactamente una vez.
- Hecho 5: WordPress históricamente dependió de la compatibilidad con MySQL, pero WordPress moderno no te protege de SQL de plugins malo; el motor de BD no te salvará de un plugin que hace full table scans en cada petición.
- Hecho 6: InnoDB se convirtió en el motor por defecto de MySQL hace años, y casi todas las cargas serias de WordPress deberían usar InnoDB; MyISAM en producción es una cápsula del tiempo con filos afilados.
- Hecho 7: Query cache (antigua característica de MySQL) fue removida en MySQL 8 y está deshabilitada/irrelevante en la mayoría de setups modernos; si alguien sugiere activarla, está citando folclore.
- Hecho 8: “Más réplicas” no arregla la contención de escrituras; WordPress tiene un patrón hotspot de escrituras (sesiones, options, actualizaciones de transients) que puede bloquear un primario aunque las lecturas estén descargadas.
- Hecho 9: Los modelos hilo-por-conexión pueden volverse limitados por la planificación de CPU mucho antes de que la utilización de CPU parezca “alta”; el gráfico engaña porque el tiempo se pasa en cambios de contexto y espera.
Guía de diagnóstico rápido: encuentra el cuello de botella en minutos
Este es el orden que gana incidentes. No porque sea teóricamente puro, sino porque reduce la búsqueda rápido.
Primero: prueba dónde ocurre el timeout
- Logs del gateway (Nginx/ALB/CDN): ¿es un timeout del upstream, o del cliente?
- Estado de PHP-FPM: ¿están saturados los workers, se dispara el slowlog o están bloqueados en la BD?
- BD: conexiones y consultas activas: ¿están explotando los hilos, o hay unas pocas consultas atascadas?
Segundo: decide “tormenta de conexiones” vs “consultas lentas” vs “locks”
- Tormenta de conexiones: muchos hilos sleeping/conectando, threads_connected alto, tiempo CPU sys en alza, muchas consultas cortas caducando, PHP-FPM al máximo.
- Consultas lentas / IO: misses del buffer pool, latencia alta de lectura en disco, lecturas InnoDB altas, tiempos largos de consulta sin locks obvios.
- Locks: “Waiting for table metadata lock”, “Waiting for record lock”, muchas consultas bloqueadas detrás de un escritor, timeouts de espera por locks.
Tercero: detén la hemorragia de forma segura
- Aligera la carga en el borde: activa caché, limita peticiones abusivas, deshabilita temporalmente rutas caras (búsqueda, wp-cron vía web, XML-RPC si aplica).
- Limita la concurrencia: reduce los hijos de PHP-FPM si la BD se está ahogando; dejar que infinitos workers de PHP golpeen la BD no es “escalar”.
- Elimina los peores ofensores: termina la consulta que mantiene locks o que lleva minutos ejecutándose, pero confirma que no sea un DDL o backup crítico.
Idea parafraseada (John Allspaw): La fiabilidad es una propiedad de todo el sistema, no de un solo componente.
Tareas prácticas: comandos, salidas y decisiones (12+)
Todo lo siguiente está diseñado para ser ejecutable en un host Linux típico con WordPress + Nginx + PHP-FPM + MySQL/MariaDB. Ajusta rutas y nombres de servicio según tu distro.
Tarea 1: Confirma que el 504 es un timeout de upstream (no DNS, no cliente)
cr0x@server:~$ sudo tail -n 20 /var/log/nginx/error.log
2025/12/29 10:11:45 [error] 1187#1187: *28491 upstream timed out (110: Connection timed out) while reading response header from upstream, client: 203.0.113.10, server: example.com, request: "GET / HTTP/1.1", upstream: "fastcgi://unix:/run/php/php8.2-fpm.sock", host: "example.com"
Qué significa: Nginx esperó por PHP-FPM y no obtuvo cabeceras a tiempo. La BD puede seguir siendo la causa, pero el choke inmediato es la latencia del upstream.
Decisión: Ve al estado de saturación de PHP-FPM y al trazado de requests lentos a continuación.
Tarea 2: Comprueba rápidamente la saturación de PHP-FPM
cr0x@server:~$ sudo ss -s
Total: 1560 (kernel 0)
TCP: 932 (estab 210, closed 590, orphaned 0, timewait 560)
Transport Total IP IPv6
RAW 0 0 0
UDP 12 10 2
TCP 342 290 52
INET 354 300 54
FRAG 0 0 0
Qué significa: Alto timewait y mucho churn TCP pueden insinuar tormentas de conexión (ya sea PHP↔DB o cliente↔web), pero no es prueba definitiva.
Decisión: Inspecciona el estado y slowlog de PHP-FPM, luego los hilos de la BD.
Tarea 3: Comprueba la presión de procesos PHP-FPM
cr0x@server:~$ ps -o pid,etime,pcpu,pmem,cmd -C php-fpm8.2 --sort=-pcpu | head
PID ELAPSED %CPU %MEM CMD
2261 01:12:08 18.3 1.6 php-fpm: pool www
2377 00:45:12 16.9 1.5 php-fpm: pool www
2410 00:21:33 15.2 1.5 php-fpm: pool www
2011 02:10:40 0.3 0.6 php-fpm: master process (/etc/php/8.2/fpm/php-fpm.conf)
Qué significa: Los workers están ocupados por largos tiempos transcurridos. Eso a menudo significa que están bloqueados—BD, sistema de archivos, llamadas HTTP externas o PHP intensivo en CPU.
Decisión: Habilita/revisa el slowlog de PHP-FPM o la página de estado para identificar dónde se gasta el tiempo.
Tarea 4: Encuentra “server reached pm.max_children” en logs de PHP-FPM
cr0x@server:~$ sudo grep -R "max_children" -n /var/log/php8.2-fpm.log | tail -n 5
[29-Dec-2025 10:11:44] WARNING: [pool www] server reached pm.max_children setting (40), consider raising it
Qué significa: Las peticiones se están encolando en PHP-FPM. Aumentar max_children puede ayudar, o puede simplemente incrementar la presión sobre la BD y empeorar el incidente.
Decisión: Si la BD ya está saturada, no lo aumentes a ciegas. Confirma primero el headroom de la BD.
Tarea 5: Comprueba identidad y versión de la base de datos (no adivines)
cr0x@server:~$ mysql -NBe "SELECT VERSION(), @@version_comment;"
10.6.18-MariaDB-0ubuntu0.22.04.1 Ubuntu 22.04
Qué significa: Estás en MariaDB 10.6, no en Oracle MySQL. Esto afecta variables disponibles, comportamiento de performance schema y supuestos de herramientas.
Decisión: Usa tuning y métricas apropiadas para el motor. No pegues consejos solo válidos para MySQL 8 en MariaDB.
Tarea 6: Comprueba el recuento de conexiones y distribución de estados de hilos
cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Threads_%';"
+-------------------+-------+
| Variable_name | Value |
+-------------------+-------+
| Threads_cached | 18 |
| Threads_connected | 420 |
| Threads_created | 9812 |
| Threads_running | 64 |
+-------------------+-------+
Qué significa: 420 conexiones y 64 hilos en ejecución. Threads_created es alto, lo que sugiere churn o cache de hilos insuficiente (o tormentas de conexión). En picos, esto puede volverse una pelea de scheduler.
Decisión: Si las conexiones escalan linealmente con el tráfico, prioriza pooling/capacidad de conexiones (ProxySQL, conexiones persistentes con cuidado, o menos workers de PHP).
Tarea 7: Identifica las consultas top en ejecución ahora mismo
cr0x@server:~$ mysql -e "SHOW FULL PROCESSLIST;" | head -n 15
Id User Host db Command Time State Info
1283 wp wpapp:42110 wpdb Query 12 Sending data SELECT option_name, option_value FROM wp_options WHERE autoload = 'yes'
1290 wp wpapp:42118 wpdb Query 11 Waiting for table metadata lock ALTER TABLE wp_posts ADD INDEX idx_post_date (post_date)
1299 wp wpapp:42130 wpdb Query 10 Locked UPDATE wp_options SET option_value='...' WHERE option_name='_transient_timeout_x'
Qué significa: Tienes un ALTER TABLE esperando un bloqueo de metadata y un UPDATE que está bloqueado. Mientras tanto, muchas peticiones golpean wp_options.
Decisión: Detén el DDL durante el pico (mátalo si es seguro), luego aborda los patrones de acceso a wp_options y el bloat de autoload.
Tarea 8: Confirma contención de locks de metadata (MySQL y MariaDB)
cr0x@server:~$ mysql -e "SELECT OBJECT_SCHEMA, OBJECT_NAME, LOCK_TYPE, LOCK_STATUS FROM performance_schema.metadata_locks WHERE LOCK_STATUS='PENDING' LIMIT 10;"
+--------------+-------------+-----------+-------------+
| OBJECT_SCHEMA| OBJECT_NAME | LOCK_TYPE | LOCK_STATUS |
+--------------+-------------+-----------+-------------+
| wpdb | wp_posts | EXCLUSIVE | PENDING |
+--------------+-------------+-----------+-------------+
Qué significa: Algo quiere un lock de metadata exclusivo (típicamente DDL) y está esperando, a menudo bloqueado por consultas/transacciones largas.
Decisión: Encuentra el bloqueador (transacción larga) y espera o mátalo. Mueve DDL a fuera de pico y usa tooling de cambio de esquema online si hace falta.
Tarea 9: Mide la presión del buffer pool de InnoDB
cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%';"
+---------------------------------------+-----------+
| Variable_name | Value |
+---------------------------------------+-----------+
| Innodb_buffer_pool_read_requests | 986543210 |
| Innodb_buffer_pool_reads | 5432109 |
+---------------------------------------+-----------+
Qué significa: Innodb_buffer_pool_reads son lecturas desde disco. Si esto sube rápidamente durante picos, estás perdiendo caché y pagando latencia de IO por petición.
Decisión: Aumenta el buffer pool (dentro de restricciones de RAM), reduce el working set (limpia autoload, índices) o añade capas de caché para reducir lecturas a BD.
Tarea 10: Comprueba señales de presión en redo log / checkpoints
cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Innodb_os_log_written'; SHOW GLOBAL STATUS LIKE 'Innodb_log_waits';"
+------------------------+------------+
| Variable_name | Value |
+------------------------+------------+
| Innodb_os_log_written | 8123456789 |
+------------------------+------------+
+------------------+-------+
| Variable_name | Value |
+------------------+-------+
| Innodb_log_waits | 421 |
+------------------+-------+
Qué significa: Un Innodb_log_waits distinto de cero e incrementándose indica sesiones esperando espacio en redo log/flush. En picos esto puede hundir latencia de escrituras y cascada en timeouts.
Decisión: Revisa el dimensionamiento del redo log y la capacidad de IO; reduce la amplificación de escritura (comportamiento de plugins, churn de transients) y asegúrate de que la latencia del almacenamiento sea razonable.
Tarea 11: Identifica esperas por locks (transacciones que bloquean a otras)
cr0x@server:~$ mysql -e "SHOW ENGINE INNODB STATUS\G" | sed -n '1,120p'
TRANSACTIONS
------------
Trx id counter 4892011
Purge done for trx's n:o < 4891900 undo n:o < 0 state: running
History list length 3212
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 4892007, ACTIVE 19 sec
2 lock struct(s), heap size 1136, 1 row lock(s), undo log entries 1
MySQL thread id 1299, OS thread handle 140312351889152, query id 712381 wpapp 10.0.0.12 wpdb updating
UPDATE wp_options SET option_value='...' WHERE option_name='_transient_timeout_x'
Qué significa: Una transacción ha estado activa 19 segundos y está actualizando wp_options. La longitud de la lista de history sugiere lag de purge; transacciones largas pueden bloquear purge y aumentar la presión de undo.
Decisión: Encuentra qué mantiene transacciones largas (jobs por lotes, backups, pantallas de administración), arréglalo y considera reducir el alcance de las transacciones en la app (difícil con WordPress, pero los plugins pueden corregirse o eliminarse).
Tarea 12: Comprueba el bloat de opciones autoload (hotspot de WordPress)
cr0x@server:~$ mysql -NBe "SELECT COUNT(*) AS autoload_rows, ROUND(SUM(LENGTH(option_value))/1024/1024,2) AS autoload_mb FROM wp_options WHERE autoload='yes';"
4123 18.74
Qué significa: 18.74 MB de opciones autoloaded cargadas con frecuencia. Eso no es “un poco de overhead”. Es un impuesto en cada petición no cacheada.
Decisión: Reduce el conjunto autoload (arregla plugins, mueve blobs grandes fuera, establece autoload=no donde sea seguro). Esto puede ser más impactante que cambiar MySQL por MariaDB.
Tarea 13: Confirma latencia de almacenamiento (porque las bases de datos sienten)
cr0x@server:~$ iostat -xz 1 3
Linux 6.5.0 (server) 12/29/2025 _x86_64_ (8 CPU)
avg-cpu: %user %nice %system %iowait %steal %idle
18.22 0.00 9.31 14.70 0.00 57.77
Device r/s rkB/s rrqm/s %rrqm r_await rareq-sz w/s wkB/s wrqm/s %wrqm w_await wareq-sz aqu-sz %util
nvme0n1 180.0 8200.0 0.0 0.0 6.20 45.6 95.0 9100.0 5.0 5.0 18.40 95.8 2.10 92.0
Qué significa: w_await es 18ms y la utilización del dispositivo es 92%. En picos, eso es suficiente para inflar la latencia de consultas y crear colas.
Decisión: O reduces la carga de escritura (caching, ajustar flushing, arreglar plugins) o mejoras el almacenamiento / separas el volumen de BD / aseguras la configuración adecuada de RAID/NVMe.
Tarea 14: Habilita el log de consultas lentas temporalmente (quirúrgico, no para siempre)
cr0x@server:~$ mysql -e "SET GLOBAL slow_query_log=ON; SET GLOBAL long_query_time=0.5; SET GLOBAL log_queries_not_using_indexes=ON;"
Qué significa: Capturarás consultas más lentas que 0.5s y las que no usan índices. Esto puede ser ruidoso; no lo dejes activado para siempre a alto volumen.
Decisión: Úsalo durante la ventana del incidente, luego analiza los principales ofensores y apágalo/bajalo.
Tarea 15: Observa las tablas con más presión de lectura/escritura (métricas InnoDB)
cr0x@server:~$ mysql -e "SELECT * FROM sys.schema_table_statistics ORDER BY rows_read DESC LIMIT 5;"
+----------------+------------+-----------+----------------+----------------+------------+------------+------------+-------------------+-------------------+
| table_schema | table_name | total_rows| rows_fetched | fetch_latency | rows_insert| rows_update| rows_delete| io_read_requests | io_write_requests |
+----------------+------------+-----------+----------------+----------------+------------+------------+------------+-------------------+-------------------+
| wpdb | wp_options | 52341 | 98122312 | 00:14:12.123456| 120 | 53210 | 8 | 842113 | 290112 |
| wpdb | wp_postmeta| 984122 | 55122311 | 00:12:40.654321| 210 | 1201 | 12 | 721223 | 120991 |
+----------------+------------+-----------+----------------+----------------+------------+------------+------------+-------------------+-------------------+
Qué significa: Son los sospechosos habituales. Si wp_options y wp_postmeta dominan, arregla patrones de WordPress antes de comprar hardware más grande.
Decisión: Prioriza indexación, higiene de autoload, caché de objetos y auditoría de plugins.
Los patrones habituales de colapso (y cómo se ven)
Patrón A: Tormenta de conexiones y thrash de hilos
Este ocurre cuando una publicación se vuelve viral o un botnet empieza a “rastrear” tus endpoints dinámicos. PHP-FPM escala workers. Cada worker abre una conexión a BD (o varias). La BD crea hilos. El tiempo de CPU se consume en cambios de contexto, contención de mutex y en gestionar demasiadas sesiones.
Lo que ves:
- Threads_connected se dispara.
- Threads_created sube rápido.
- Tiempo de sistema CPU aumenta, la carga media sube, pero el “trabajo útil” no.
- Muchas consultas son cortas, pero todo espera en la fila.
¿Quién colapsa primero? Cualquiera. Pero MariaDB con thread pool puede ser más elegante aquí si está configurado, porque limita la ejecución concurrente y reduce el thrash. MySQL puede igualarlo si limitas la concurrencia en la capa de aplicación y/o añades un proxy pool.
Patrón B: Hotspot en wp_options + autoload bloat
Las opciones autoloaded de WordPress se cargan con frecuencia. Añade unos cuantos plugins que almacenan arrays serializados grandes con autoload=yes, y te habrás creado un pequeño DoS contra ti mismo.
Lo que ves:
SELECT option_name, option_value FROM wp_options WHERE autoload='yes'aparece constantemente.- Churn del buffer pool (si el working set no cabe en RAM).
- CPU sube por parsing de consultas y procesamiento de filas.
¿Quién colapsa primero? El que tenga menos RAM, peores índices o peor estrategia de caché. La elección del motor no te salvará del abuso de autoload.
Patrón C: Acumulación de locks por escrituras y tareas background
Los picos de tráfico a menudo coinciden con más comentarios, inicios de sesión, actualizaciones de carrito (si usas WooCommerce) y actualizaciones de transients. Las escrituras generan locks. Consultas de larga duración o tareas de admin “inocentes” pueden mantener locks y bloquear todo.
Lo que ves:
- Estados en processlist: Locked, Waiting for…
- InnoDB status muestra transacciones largas y longitud de history list creciente.
- Timeouts por espera de locks y deadlocks.
¿Quién colapsa primero? Otra vez, ambos. La diferencia es operativa: qué tan rápido puedes ver el bloqueador y cómo puedes mitigar de forma segura sin empeorar la situación.
Patrón D: Saturación de IO (el asesino silencioso)
Cuando la latencia de disco sube, todo se ralentiza. La base de datos no “cae”. Simplemente se convierte en un amplificador de latencia. Los workers de PHP se apilan. Nginx hace timeouts. Obtienes 504s y muchas opiniones confundidas.
Lo que ves:
- iowait aumenta.
- %util de disco alto, await crece.
- Innodb_buffer_pool_reads sube rápido.
- Checkpoints / esperas en logs.
Broma #2: La base de datos no “se cayó”. Simplemente entró en una larga y significativa pausa para reflexionar sobre tus elecciones de almacenamiento.
Tres mini-historias corporativas desde el campo
Mini-historia 1: El incidente causado por una suposición equivocada
La empresa estaba en migración de un MySQL antiguo a MariaDB porque “es drop-in”. Hicieron las partes sensatas: replicaron datos, ensayaron el cutover, validaron pruebas de aplicación. Hicieron la parte menos sensata: asumieron que sus herramientas operativas se comportarían igual.
En el día del lanzamiento, el tráfico se disparó—marketing cumplió su trabajo. WordPress empezó a devolver 504s. El on-call siguió el playbook normal: sacar las consultas top del dashboard de monitoring, comprobar el retraso de replicación e inspeccionar los contadores habituales de estado de MySQL.
El dashboard parecía extrañamente calmado. Las conexiones estaban “bien”. El tiempo de consulta estaba “bien”. Aún así Nginx hacía timeouts y PHP-FPM gritaba sobre max_children. El equipo persiguió la capa web durante media hora porque los gráficos de la base de datos insistían en que todo estaba bien.
La raíz fue simple y embarazosa: su exporter estaba atado a tablas específicas de performance schema de MySQL y devolvía métricas parciales en MariaDB. La BD estaba saturada con esperas de locks y latencia de disco, pero los gráficos mentían por omisión. Lo arreglaron desplegando un exporter compatible con MariaDB y añadiendo paneles directos de muestreo de SHOW PROCESSLIST. Los 504 eran un problema de base de datos; el incidente fue un problema de observabilidad.
Mini-historia 2: La optimización que salió mal
Otra organización tenía un sitio WordPress que periódicamente era golpeado por crawlers. Alguien decidió que la solución era aumentar la concurrencia por todos lados: subir PHP-FPM max_children, aumentar MySQL max_connections y subir el número de workers web. La intención era noble: “manejar más tráfico.” El efecto fue más como “manejar más sufrimiento”.
El siguiente pico llegó y la BD cayó más fuerte que antes. No un crash—peor. Se mantuvo arriba y respondió lentamente. La CPU no estaba al 100%, pero la latencia se fue vertical. Threads_created explotó. El kernel pasaba su tiempo en cambios de contexto. La cola de almacenamiento se mantuvo alta porque las escrituras llegaban más rápido de lo que podían flusharse. Nginx devolvía 504s como si le pagaran por cada error.
El equipo revirtió los cambios de concurrencia y el sistema se estabilizó. Más tarde implementaron la solución poco glamurosa: limitar PHP-FPM a un número que la BD pudiera sostener, añadir un pooler de conexiones y poner caché delante de los endpoints más pesados. La lección quedó: subir límites aumenta el tamaño del radio de explosión. No aumenta la capacidad a menos que cambie otra cosa.
Mini-historia 3: La práctica aburrida pero correcta que salvó el día
Este equipo corría WordPress con un calendario estricto de cambios y un hábito que todos se burlaban: ensayaban respuesta a incidentes y mantenían una política de “congelamiento en picos”. Durante eventos de tráfico previstos (lanzamientos de producto, menciones en medios grandes), no desplegaban cambios de esquema, actualizaciones de plugins ni “arreglos rápidos” en wp-admin.
Una tarde, el tráfico se triplicó inesperadamente por un tirón en redes sociales. El sitio se ralentizó, pero no colapsó. Su caché de Nginx sirvió la mayor parte del tráfico anónimo. PHP-FPM tenía un tope estricto. La BD tenía holgura porque el working set cabía en el buffer pool y el slow query log había sido muestreado y limpiado semanas antes.
Siguieron recibiendo alertas: el lag de replicación creció un poco y la latencia p95 de consultas subió. Pero nada alcanzó los umbrales de timeout. Su canal de incidentes se mantuvo aburrido, que es el mayor cumplido que le puedes hacer a un sistema SRE.
La acción salvadora no fue un motor de base de datos mágico. Fue disciplina operativa: planificación de capacidad, caching, higiene de consultas y negarse a hacer DDL en el peor momento posible.
Errores comunes: síntoma → causa raíz → reparación
-
Síntoma: pico de 504s, logs de PHP-FPM muestran max_children alcanzado, la CPU de la BD parece “no tan alta”.
Causa raíz: tormenta de conexiones y overhead de scheduling de hilos; inflación de latencia sin CPU saturada.
Arreglo: Limitar PHP-FPM, añadir pooling de conexiones, reducir max_connections para forzar backpressure y cachear tráfico anónimo. -
Síntoma: muchas consultas atascadas en “Waiting for table metadata lock”.
Causa raíz: DDL ejecutándose en pico, bloqueado por transacciones largas (o bloqueando a otras).
Arreglo: Parar DDL en picos; encontrar y terminar transacciones largas; programar cambios de esquema online fuera de hora punta. -
Síntoma: picos que correlacionan con %util de disco cerca del 100% y await alto; la BD se siente “lenta en todas partes”.
Causa raíz: saturación de IO e insuficiente buffer pool; presión en redo/checkpoint.
Arreglo: Aumentar buffer pool, ajustar tamaño y flushing de redo logs, reducir amplificación de escrituras (transients), mejorar almacenamiento o aislar disco de BD. -
Síntoma: Un tipo de página (búsqueda, páginas de categoría) provoca timeouts mientras otras están bien.
Causa raíz: Consultas específicas lentas (a menudo joins enwp_postmeta) sin índices o que usan filesorts/temporary tables.
Arreglo: Captura consultas lentas, añade índices dirigidos o cambia patrones de consulta del plugin/tema. No “optimices todo”. -
Síntoma: Tras “moverse a MariaDB/MySQL”, las cosas van un poco peor, no catastróficamente.
Causa raíz: Cambios en planes del optimizador, diferencias de collation o valores por defecto desajustados; dashboards de métricas incompletos.
Arreglo: Compara planes conEXPLAIN, valida consultas críticas, estandariza collations/charsets, actualiza exporters y umbrales de alertas. -
Síntoma: Réplicas están bien pero el primario muere en picos.
Causa raíz: Hotspot de escrituras en el primario (options/transients/sessions); las réplicas no ayudan con escrituras.
Arreglo: Reduce escrituras (cache de objetos, deshabilita wp-cron por web, ajusta comportamiento de transients), considera separar cargas y optimiza tablas calientes.
Listas de verificación / plan paso a paso
Durante el incidente (detener los 504)
- Confirma la fuente del timeout: timeouts upstream de Nginx vs PHP-FPM vs BD.
- Congela cambios: no hay actualizaciones de plugins, no cambios de esquema, no “arreglos rápidos” en wp-admin.
- Reduce carga dinámica:
- Habilita/extiende microcache de Nginx para tráfico anónimo si lo tienes.
- Rate limit a endpoints abusivos (búsqueda, xmlrpc.php, wp-login.php) si ese es el patrón.
- Desactiva wp-cron vía hits web; ejecútalo por cron del sistema si es posible.
- Limita la concurrencia: mantén workers de PHP-FPM en un número que la BD pueda servir con latencia aceptable.
- Encuentra y mata los bloqueadores: DDL largos, locks de metadata, consultas fuera de control.
- Captura evidencia: muestras de processlist, snippet de slow log, InnoDB status, iostat.
Después del incidente (hacer que sea más difícil repetirlo)
- Implementa pooling de conexiones si ves churn de hilos o conexiones altas en picos.
- Corrige autoload de wp_options: audita y reduce; elimina blobs autoloaded sobredimensionados.
- Agrega/valida índices para las consultas lentas top. No adivines; mide.
- Dimensiona InnoDB correctamente: buffer pool, redo logs, comportamiento de flush según el almacenamiento.
- Mejora la observabilidad: asegúrate de que las métricas coincidan con el motor, que los dashboards muestren consultas activas y esperas por locks.
- Practica higiene en picos: no hacer cambios de esquema durante picos previstos; ensaya rollback.
Preguntas frecuentes
1) ¿MariaDB es más rápido que MySQL para WordPress?
A veces, en escenarios de concurrencia específicos—especialmente si entra en juego el thread pool de MariaDB. Pero los determinantes mayores son caché, calidad de consultas, tamaño del buffer pool y controlar tormentas de conexiones.
2) ¿Cambiar de motor arreglará mis 504s?
Rara vez por sí solo. La mayoría de 504s en picos vienen de concurrencia desatada, tablas calientes (wp_options), consultas lentas o saturación de IO. Cambiar de motor sin arreglar eso es un movimiento lateral con nuevos modos de fallo.
3) ¿Cuál es la mejor solución única para picos en WordPress?
Cachear agresivamente el tráfico anónimo y limitar la concurrencia dinámica. Si tu borde puede servir el 80–95% de las peticiones sin tocar PHP/BD, los picos se vuelven aburridos.
4) ¿Debo subir max_connections para evitar errores “too many connections”?
Sólo si has demostrado que la BD puede manejar más concurrencia. Si no, estás cambiando una falla rápida por una muerte lenta: más trabajo en cola, más contención, más presión de memoria, más 504s.
5) ¿Vale la pena ProxySQL para WordPress?
Si tienes picos frecuentes y muchas conexiones de corta duración, sí. Puede suavizar tormentas de conexiones, enrutar lecturas a réplicas y darte un punto de control. También añade complejidad; trátalo como una capa real, no como un juguete sidecar.
6) ¿Las réplicas ayudan con los 504s?
Ayudan si las lecturas son tu cuello de botella y realmente puedes enviar lecturas a réplicas de forma segura. No ayudan con hotspots de escritura, contención de locks en el primario o escrituras lentas por disco.
7) ¿Por qué la CPU parece bien mientras el sitio hace timeout?
Porque la latencia puede estar dominada por espera: espera de IO, espera por locks, thrash del scheduler, flushing. Tu gráfico de CPU no es un oráculo de la verdad; es una pista.
8) ¿Qué debo afinar primero en InnoDB para WordPress?
Tamaño del buffer pool (para que lecturas estén en RAM), dimensionamiento del redo log acorde a tu tasa de escrituras y un flushing sensato. Luego céntrate en consultas/índices y el bloat de autoload—esos suelen ser los verdaderos villanos.
9) ¿MySQL 8 es una elección más segura para compatibilidad?
Si tus herramientas y ecosistema asumen semánticas de MySQL 8, sí, suele ser operativamente más sencillo. MariaDB es compatible en muchos casos, pero no idéntico; trátalo como su propio producto y valida comportamientos.
Siguientes pasos que puedes hacer esta semana
- Mide un pico: captura muestras de processlist, slow query logs (brevemente) y estadísticas de IO durante la hora punta. No ajustes a ciegas.
- Controla la concurrencia: fija límites de PHP-FPM en función de la capacidad de la BD; considera un pooler si hay churn de conexiones.
- Arregla los hotspots de WordPress: reduce el tamaño de opciones autoloaded, audita plugins que golpean
wp_postmetay añade índices faltantes. - Haz el caching obligatorio: caché de página en el borde para usuarios anónimos, caché de objetos para rutas pesadas en BD.
- Elige tu motor según operaciones: escoge el que tu equipo pueda monitorizar correctamente, actualizar con seguridad y recuperar rápido. El rendimiento es una característica; la recuperación es un estilo de vida.