Solucionar MySQL de WordPress: «MySQL server has gone away» y «Too many connections»

¿Te fue útil?

Si tu sitio WordPress lanza aleatoriamente “MySQL server has gone away” o “Too many connections”, no tienes un problema de filosofía. Tienes un problema de contabilidad de recursos. Algo está cerrando conexiones, agotándolas o manteniéndolas demasiado tiempo—normalmente durante un pico de tráfico, una consulta lenta o un plugin que hace el equivalente en base de datos de dejar el grifo abierto.

Esta guía está escrita para quienes gestionan sistemas en producción y necesitan una forma tranquila y repetible de pasar de “está caído” a “está estable” sin adivinar. Obtendrás triage rápido, comandos concretos, qué significa cada salida y las decisiones que deberías tomar después.

Qué significan realmente estos errores (y qué no significan)

“MySQL server has gone away”

Este mensaje es la aplicación diciendo: “Intenté usar una conexión MySQL, pero el lado servidor ya no está ahí.” Eso puede ocurrir porque:

  • El servidor cerró la conexión por tiempo de inactividad (wait_timeout).
  • El servidor se estrelló o reinició (OOM killer, reinicio manual, actualización, panic).
  • Un dispositivo de red cortó la conexión (NAT, firewall, timeout de balanceador de carga por inactividad).
  • El cliente envió un paquete más grande de lo permitido (max_allowed_packet).
  • El servidor alcanzó límites de recursos y empezó a rechazar o abortar trabajo (disco lleno, corrupción de tablas, memoria desbocada, agotamiento de hilos).

No es “WordPress está roto.” WordPress es solo el mensajero. No lo culpes; interrógalo.

“Too many connections”

Esto es MySQL hablando claro: el número de conexiones cliente concurrentes excedió max_connections. En pilas WordPress, esto suele ocurrir porque PHP abre conexiones más rápido de lo que MySQL puede terminar consultas.

Dos puntos sutiles importan:

  • Elevar max_connections no soluciona por sí mismo. Puede comprar tiempo, o puede convertir “conexiones rechazadas” en “tormenta de swap” y tumbar todo el host.
  • El recuento de conexiones es un síntoma. La raíz casi siempre es latencia: consultas lentas, tablas bloqueadas, I/O saturado, CPU al máximo o PHP-FPM generando demasiados workers.

Una idea parafraseada a menudo atribuida al liderazgo de ingeniería: “La fiabilidad es una característica que construyes, no un deseo que formulas.” (idea parafraseada, John Allspaw)

Broma corta #1: MySQL no “se va.” Es más bien que se enfurece y abandona porque le pidieron cinco cosas a la vez sobre un disco de portátil.

Playbook de diagnóstico rápido (primeras/segundas/terceras comprobaciones)

Cuando las páginas fallan, tienes un trabajo: encontrar el cuello de botella rápido, detener la hemorragia y luego hacerlo aburrido.

Primero: identifica la clase de fallo en 2 minutos

  1. ¿Está MySQL arriba y aceptando conexiones? Si no, esto es crash/restart/OOM/disco.
  2. ¿Están las conexiones saturadas? Si sí, esto es “too many connections”, a menudo impulsado por consultas lentas o explosión de workers de PHP.
  3. ¿Se están matando conexiones a mitad de camino? Si sí, piensa en timeouts de red, timeouts de MySQL, max packet o comportamiento de proxy.

Segundo: decide dónde se está formando la cola

  • Cola en el lado PHP: PHP-FPM tiene una larga cola de escucha, muchos workers activos, solicitudes lentas.
  • Cola en el lado MySQL: MySQL muestra muchos hilos en ejecución, consultas largas, esperas de bloqueo, fallos de buffer pool, esperas de I/O.
  • Cola en red/proxy: resets intermitentes, timeouts NAT, “gone away” después de periodos inactivos.

Tercero: aplica la mitigación inmediata más segura

  • Si MySQL está caído: arregla disco/OOM, reinicia y limita la concurrencia de PHP antes de dejar que el tráfico lo vuelva a saturar.
  • Si las conexiones están saturadas: reduce temporalmente la concurrencia de PHP-FPM; opcionalmente aumenta max_connections ligeramente si la memoria lo permite; encuentra el patrón de consultas lentas.
  • Si “gone away” ocurre tras inactividad: alinea timeouts entre MySQL, PHP y dispositivos de red; considera habilitar keepalives o evitar conexiones persistentes.

Hechos interesantes y contexto histórico (para que el comportamiento tenga sentido)

  1. Los timeouts por defecto de MySQL fueron diseñados para servidores de aplicaciones de larga vida, no para las modernas capas de proxies, NAT y edges serverless que cortan TCP inactivo.
  2. En la era LAMP temprana, las conexiones persistentes eran tendencia porque abrir TCP + autenticar era caro. Hoy, las conexiones persistentes suelen amplificar modos de fallo bajo picos de tráfico.
  3. “Too many connections” solía ser un símbolo de orgullo en algunas organizaciones—hasta que descubrieron que cada hilo cuesta memoria y tiempo de conmutación de contexto.
  4. WordPress históricamente fomentó ecosistemas de plugins, lo que es genial para funciones y terrible para la disciplina de consultas. Un solo plugin puede añadir 20 consultas por página sin avisarte.
  5. InnoDB se convirtió en el motor por defecto en MySQL 5.5, y eso cambió el enfoque de afinación “correcto”. Los consejos de la era MyISAM todavía persisten en foros.
  6. Las tormentas de conexiones son una clase conocida de fallo en cascada: cuando MySQL se ralentiza, PHP abre más conexiones, lo que ralentiza más a MySQL, lo que provoca reintentos… y ahora todo arde.
  7. Históricamente, max_allowed_packet era pequeño por defecto porque la memoria importaba y los blobs grandes estaban desaconsejados. El multimedia y las opciones serializadas de WordPress ignoran esa historia.
  8. Muchos incidentes de “server has gone away” no son bugs de MySQL; son timeouts de infraestructura (balanceadores, firewalls) con valores por defecto como 60 segundos que nadie recuerda haber configurado.
  9. Las conexiones abortadas no siempre son malas: pueden reflejar usuarios que cierran el navegador a mitad de la solicitud. Pero un pico de conexiones abortadas junto con errores suele ser la evidencia más clara.

Modos de fallo principales detrás de “gone away” y “too many connections”

1) MySQL se está reiniciando (a menudo OOM o disco)

Si mysqld se reinicia, cada conexión activa muere. WordPress reporta “server has gone away” porque el socket que tenía ahora es un fósil.

Desencadenantes comunes:

  • Eliminación por falta de memoria (OOM): buffer pool demasiado grande, demasiados buffers por hilo, demasiadas conexiones o fuga de memoria en servicios contiguos.
  • Disco lleno: binlogs, tmpdir, ibtmp o logs de consultas lentas crecen; MySQL empieza a fallar escrituras; luego todo se desborda.
  • Latencia del sistema de archivos: los stalls de I/O llevan a hilos colgados; watchdog reinicia; los clientes hacen timeout.

2) Agotamiento de conexiones por consultas lentas

WordPress habla mucho. Añade unas cuantas consultas meta sin índice, un plugin de informes y un rastreo de bots entusiasta, y puedes ocupar todos los hilos. Las nuevas conexiones son rechazadas: “too many connections.”

3) PHP-FPM o la capa web puede crear demasiada paralelización

La mayoría de las caídas de WordPress culpan a MySQL, pero PHP-FPM a menudo tiene la cerilla. Si permites 200 workers de PHP y cada uno puede abrir una conexión MySQL, en la práctica has configurado una avalancha de conexiones.

4) Timeouts desalineados entre capas

Imagina que wait_timeout de MySQL es 60 segundos. Tu balanceador de carga corta TCP inactivo a 50 segundos. PHP intenta reutilizar una conexión a los 55 segundos. Resultado: “server has gone away”, pero MySQL nunca la cerró.

5) Paquetes sobredimensionados y datos extraños de WordPress

WordPress guarda arrays en wp_options como blobs serializados. Algunos plugins meten ahí cargas enormes (caches de page builder, volcados de analítica). Si ese blob supera max_allowed_packet, MySQL corta la conexión y obtienes “server has gone away”.

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

Estos son los movimientos que espero que un on-call ejecute durante un incidente. Cada tarea incluye un comando, salida de ejemplo, qué significa y la decisión que tomas.

Task 1: Confirmar que MySQL está vivo y anotar el tiempo de actividad

cr0x@server:~$ systemctl status mysql --no-pager
● mysql.service - MySQL Community Server
     Loaded: loaded (/lib/systemd/system/mysql.service; enabled; vendor preset: enabled)
     Active: active (running) since Sat 2025-12-27 10:11:03 UTC; 3min ago
       Docs: man:mysqld(8)
   Main PID: 1423 (mysqld)
     Status: "Server is operational"

Qué significa: Si el tiempo de actividad es de minutos y no lo reiniciaste, tienes un bucle de crash o reinicio del host.

Decisión: Si se reinició recientemente, revisa inmediatamente logs por OOM/disco y limita la concurrencia de PHP antes de que el tráfico lo vuelva a saturar.

Task 2: Revisar el log de errores de MySQL en busca de firmas de crash

cr0x@server:~$ journalctl -u mysql --since "30 min ago" --no-pager | tail -n 40
Dec 27 10:10:58 server mysqld[1423]: 2025-12-27T10:10:58.221Z 0 [Warning] Aborted connection 891 to db: 'wpdb' user: 'wpuser' host: '10.0.2.15' (Got an error reading communication packets)
Dec 27 10:11:02 server systemd[1]: mysql.service: Main process exited, code=killed, status=9/KILL
Dec 27 10:11:03 server systemd[1]: mysql.service: Scheduled restart job, restart counter is at 1.
Dec 27 10:11:03 server systemd[1]: Started MySQL Community Server.

Qué significa: Status 9/KILL suele apuntar al OOM killer o a una terminación administrativa.

Decisión: Valida eventos OOM y dimensionado de memoria; no “solo reinicies” repetidamente esperando que la base de datos recupere la calma.

Task 3: Confirmar eventos del OOM killer

cr0x@server:~$ dmesg -T | egrep -i "oom|killed process|out of memory" | tail -n 20
[Sat Dec 27 10:11:01 2025] Out of memory: Killed process 1423 (mysqld) total-vm:4123988kB, anon-rss:2450180kB, file-rss:0kB, shmem-rss:0kB, UID:110 pgtables:6420kB oom_score_adj:0

Qué significa: MySQL fue matado por memoria. “Server has gone away” es la nota final.

Decisión: Reducir huella de memoria (buffer pool, buffers por hilo, conteo de conexiones), añadir RAM o mover MySQL fuera del host congestionado.

Task 4: Comprobar espacio libre y presión de inodos (sí, inodos)

cr0x@server:~$ df -hT
Filesystem     Type  Size  Used Avail Use% Mounted on
/dev/nvme0n1p2 ext4   80G   76G  2.1G  98% /
tmpfs          tmpfs 3.9G   12M  3.9G   1% /run
cr0x@server:~$ df -ihT
Filesystem     Type  Inodes IUsed IFree IUse% Mounted on
/dev/nvme0n1p2 ext4     5M   4.9M  120K   98% /

Qué significa: Disco o inodos casi llenos pueden romper tablas temporales, logs, binlogs e incluso escrituras de sockets.

Decisión: Libera espacio de inmediato (rotar logs, purgar binlogs de forma segura), luego corrige el crecimiento (retención de logs, alertas de monitoreo).

Task 5: Medir la presión de conexiones y margen de max_connections

cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Threads_connected'; SHOW VARIABLES LIKE 'max_connections';"
+-------------------+-------+
| Variable_name     | Value |
+-------------------+-------+
| Threads_connected | 198   |
+-------------------+-------+
+-----------------+-------+
| Variable_name   | Value |
+-----------------+-------+
| max_connections | 200   |
+-----------------+-------+

Qué significa: Estás pegado al techo. MySQL está pasando tiempo en conmutación de contexto y los nuevos logins fallarán.

Decisión: No subas directamente a 1000 conexiones. Primero reduce la concurrencia upstream (PHP-FPM), luego encuentra qué está lento.

Task 6: Identificar quién consume conexiones (por usuario/host)

cr0x@server:~$ mysql -e "SELECT user, host, COUNT(*) AS conns FROM information_schema.processlist GROUP BY user, host ORDER BY conns DESC LIMIT 10;"
+--------+------------+-------+
| user   | host       | conns |
+--------+------------+-------+
| wpuser | 10.0.2.15  | 184   |
| wpuser | 10.0.2.16  | 12    |
| root   | localhost  | 1     |
+--------+------------+-------+

Qué significa: Un nodo web está abrumando la base de datos o atascado reintentando.

Decisión: Limita la tasa o drena ese nodo; revisa su cola PHP-FPM y logs de error; verifica que no esté en un bucle por fallos en DB.

Task 7: Detectar consultas de larga duración y esperas de bloqueo

cr0x@server:~$ mysql -e "SHOW FULL PROCESSLIST;" | head -n 30
Id	User	Host	db	Command	Time	State	Info
812	wpuser	10.0.2.15:54012	wpdb	Query	42	statistics	SELECT SQL_CALC_FOUND_ROWS wp_posts.ID FROM wp_posts ...
901	wpuser	10.0.2.15:54101	wpdb	Query	39	Waiting for table metadata lock	ALTER TABLE wp_postmeta ADD INDEX meta_key (meta_key)
...

Qué significa: Consultas largas y locks de metadata pueden bloquear hilos. Eso impulsa acumulaciones de conexiones.

Decisión: Mata consultas evidentemente malas, pospone DDL y arregla la consulta/índice. Si un DDL está bloqueando, muévelo a horas de baja y usa herramientas de cambio de esquema en línea.

Task 8: Revisar salud de InnoDB y pistas reales del “por qué va lento”

cr0x@server:~$ mysql -e "SHOW ENGINE INNODB STATUS\G" | egrep -A3 -B2 "LATEST DETECTED DEADLOCK|History list length|buffer pool|I/O|semaphore" | head -n 80
History list length 124873
Pending reads 0
Pending writes: LRU 0, flush list 12, single page 0
Buffer pool size   131072
Free buffers       12
Database pages     130984
Modified db pages  9812

Qué significa: Un history list length grande puede implicar retraso en purge; muchas páginas modificadas pueden implicar presión en flush de páginas; pocos buffers libres indican un buffer pool caliente.

Decisión: Si el flush de I/O está atrasado, mejora la latencia de almacenamiento, ajusta parámetros de flush y reduce ráfagas de escritura (caches, batching). Si hay retraso de purge, investiga transacciones largas.

Task 9: Validar settings de timeout que causan “gone away” tras inactividad

cr0x@server:~$ mysql -e "SHOW VARIABLES WHERE Variable_name IN ('wait_timeout','interactive_timeout','net_read_timeout','net_write_timeout','max_allowed_packet');"
+--------------------+-----------+
| Variable_name      | Value     |
+--------------------+-----------+
| interactive_timeout| 28800     |
| max_allowed_packet | 67108864  |
| net_read_timeout   | 30        |
| net_write_timeout  | 60        |
| wait_timeout       | 60        |
+--------------------+-----------+

Qué significa: Un wait_timeout de 60 segundos es agresivo. Puede estar bien si tu app no reutiliza conexiones inactivas. Las pilas WordPress a veces sí lo hacen (dependiendo de la librería cliente y persistencia).

Decisión: Alinea timeouts: establece wait_timeout en algo sensato (p. ej., 300–900) y asegúrate de que los proxies no corten antes que MySQL ni al revés.

Task 10: Inspeccionar contadores de conexiones abortadas

cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Aborted_%';"
+------------------+-------+
| Variable_name    | Value |
+------------------+-------+
| Aborted_clients  | 1821  |
| Aborted_connects | 94    |
+------------------+-------+

Qué significa: Aborted_clients aumenta cuando clientes se desconectan sin limpiar; Aborted_connects aumenta cuando falla autenticación/handshake o límites de recursos bloquean conexiones.

Decisión: Si ambos suben durante incidentes, correlaciona con drops de red y saturación de max_connections. Si solo crece Aborted_clients, no entres en pánico—confirma con tasa de errores y tiempos.

Task 11: Comprobar concurrencia y encolamiento de PHP-FPM

cr0x@server:~$ systemctl status php8.2-fpm --no-pager
● php8.2-fpm.service - The PHP 8.2 FastCGI Process Manager
     Active: active (running) since Sat 2025-12-27 09:41:10 UTC; 33min ago
cr0x@server:~$ sudo ss -lntp | egrep "php-fpm|:9000"
LISTEN 0 511 127.0.0.1:9000 0.0.0.0:* users:(("php-fpm8.2",pid=2201,fd=8))

Qué significa: El backlog de escucha (aquí 511) es solo capacidad para solicitudes en cola. La verdadera pregunta es: ¿están saturados los workers?

Decisión: Revisa la página de estado de FPM o los logs; si está saturado, baja pm.max_children para que coincida con la capacidad de la BD, no con “lo que cabe en RAM un buen día”.

Task 12: Confirmar que Nginx/Apache no reintenta upstream con demasiada agresividad

cr0x@server:~$ sudo tail -n 30 /var/log/nginx/error.log
2025/12/27 10:12:01 [error] 3112#3112: *9821 upstream timed out (110: Connection timed out) while reading response header from upstream, client: 203.0.113.55, server: example, request: "GET / HTTP/2.0", upstream: "fastcgi://127.0.0.1:9000"
2025/12/27 10:12:03 [error] 3112#3112: *9838 FastCGI sent in stderr: "PHP message: WordPress database error Too many connections for query SELECT option_name, option_value FROM wp_options WHERE autoload = 'yes'"

Qué significa: Un upstream lento causa timeouts web; los logs de PHP muestran errores de BD. Ambos son síntomas de la misma cola.

Decisión: Reduce concurrencia y arregla la latencia de BD. Extender timeouts de Nginx sin arreglar la BD es la forma de crear outages más largos con más usuarios enfadados.

Task 13: Encontrar las peores consultas de WordPress (slow log a demanda)

cr0x@server:~$ mysql -e "SET GLOBAL slow_query_log=ON; SET GLOBAL long_query_time=0.5; SHOW VARIABLES LIKE 'slow_query_log_file';"
+---------------------+----------------------------------+
| Variable_name       | Value                            |
+---------------------+----------------------------------+
| slow_query_log_file | /var/log/mysql/mysql-slow.log    |
+---------------------+----------------------------------+
cr0x@server:~$ sudo tail -n 20 /var/log/mysql/mysql-slow.log
# Time: 2025-12-27T10:12:22.114Z
# Query_time: 3.218  Lock_time: 0.004 Rows_sent: 10  Rows_examined: 184221
SET timestamp=1766830342;
SELECT p.ID FROM wp_posts p
JOIN wp_postmeta pm ON pm.post_id=p.ID
WHERE pm.meta_key='some_key' AND pm.meta_value LIKE '%needle%'
ORDER BY p.post_date DESC LIMIT 10;

Qué significa: Rows examined es enorme. Ese es tu asesino de conexiones: cada consulta lenta ocupa un hilo y hace crecer la pila.

Decisión: Añade/ajusta índices, reduce consultas a meta o elimina/reemplaza el patrón de plugin. Luego reduce el logging para que no llene el disco.

Task 14: Verificar salud de tablas/índices rápidamente

cr0x@server:~$ mysql -e "CHECK TABLE wp_options, wp_posts, wp_postmeta QUICK;"
+-------------+-------+----------+----------+
| Table       | Op    | Msg_type | Msg_text |
+-------------+-------+----------+----------+
| wpdb.wp_options  | check | status   | OK       |
| wpdb.wp_posts    | check | status   | OK       |
| wpdb.wp_postmeta | check | status   | OK       |
+-------------+-------+----------+----------+

Qué significa: “OK” descarta algunos escenarios de corrupción. No prueba que el rendimiento esté bien.

Decisión: Si ves corrupción, no hagas reparaciones ad-hoc durante la peak a menos que conozcas el blast radius. Estabiliza, crea snapshot/backup y repara deliberadamente.

Task 15: Cuantificar memoria por conexión (dimensionado aproximado)

cr0x@server:~$ mysql -e "SHOW VARIABLES WHERE Variable_name IN ('read_buffer_size','read_rnd_buffer_size','sort_buffer_size','join_buffer_size','tmp_table_size','max_heap_table_size');"
+----------------------+----------+
| Variable_name        | Value    |
+----------------------+----------+
| join_buffer_size     | 262144   |
| max_heap_table_size  | 16777216 |
| read_buffer_size     | 131072   |
| read_rnd_buffer_size | 262144   |
| sort_buffer_size     | 262144   |
| tmp_table_size       | 16777216 |
+----------------------+----------+

Qué significa: Los buffers por conexión pueden asignarse bajo demanda. En alta concurrencia, “bajo demanda” se vuelve “todos a la vez”.

Decisión: Mantén los buffers por hilo modestos. No los subas para arreglar una consulta puntual; arregla la consulta.

Perillas de afinación que realmente importan (y las que hacen perder tiempo)

Empieza con la restricción: los hilos de MySQL no son gratis

Cada conexión consume memoria y atención del planificador. Subir max_connections aumenta el tamaño de la estampida posible.

Cuando debas aumentarlo, hazlo como adulto:

  • Estima primero el margen de memoria (buffer pool + overhead + caché del SO + otros demonios).
  • Súbelo por pasos (p. ej., +25% o +50%), no 10x de golpe.
  • Combínalo con limitación upstream (PHP-FPM max_children, límites del servidor web).

Buffer pool de InnoDB: la única gran perilla que suele pagar

Para hosts dedicados MySQL, un punto de partida común es 60–75% de la RAM para innodb_buffer_pool_size. Para hosts compartidos, depende de qué más compita por memoria.

Si tu working set no cabe, golpearás disco más seguido. El I/O de disco vuelve las consultas más lentas. Las consultas lentas mantienen conexiones más tiempo. Eso dispara “too many connections.” Es un bucle.

Timeouts: arregla la alineación, no el mito

wait_timeout es cuánto tiempo MySQL conserva una conexión inactiva no interactiva. Si es demasiado bajo y tu cliente reutiliza conexiones inactivas, obtendrás “gone away.” Si es demasiado alto y tienes miles de conexiones inactivas, desperdicias memoria. Escoge un valor realista y ajústalo a tu entorno.

Considera también los asesinos invisibles: firewalls y balanceadores. A menudo tienen timeouts por inactividad menores que los de MySQL. Si cortan conexiones silenciosamente, la primera lectura/escritura posterior dispara “gone away.”

max_allowed_packet: la perilla del “por qué esta subida mató mi BD”

Las subidas de WordPress no pasan directamente por MySQL, pero los plugins almacenan blobs serializados grandes, especialmente en options y post meta. Si ves errores en actualizaciones grandes, sube max_allowed_packet a algo sensato (64M o 128M según la carga) y luego encuentra el plugin que almacena basura.

No pierdas tiempo en esto durante un incidente

  • Consejos aleatorios sobre query cache: Si alguien te dice que habilites la antigua query cache de MySQL, está viajando en el tiempo. En MySQL moderno ya no está.
  • Aumentar buffers por hilo a tope: Esa es la forma de fabricar OOM kills con confianza.
  • Desactivar seguridad de flush de InnoDB: Puedes intercambiar durabilidad por velocidad, pero hazlo a propósito—no porque un comentario en un blog lo recomendó.

Patrones de fallo específicos de WordPress

Opciones autoloaded convirtiéndose en un impuesto de la base de datos

WordPress carga todas las opciones con autoload='yes' al inicio. Si un plugin vuelca datos grandes en opciones autoloaded, cada solicitud paga el coste. Eso hace las solicitudes más lentas, lo que incrementa el uso concurrente de BD.

Arreglo: audita wp_options, reduce la basura autoloaded y lleva caches voluminosos a cache de objetos (Redis/Memcached) o archivos.

Consultas a wp_postmeta sin índices adecuados

Las consultas meta son flexibles pero caras. Muchos plugins construyen características tipo búsqueda usando LIKE '%...%' en meta_value. Eso no es una consulta; es un grito de ayuda.

Arreglo: limita las consultas a meta, añade índices dirigidos (con cuidado) o mueve esa función a un motor de búsqueda.

WP-Cron y tormentas de admin-ajax

El alto tráfico puede disparar WP-Cron frecuentemente si se basa en tráfico. Mientras tanto, admin-ajax.php puede ser abusado por temas/plugins para sondeos constantes.

Arreglo: mueve cron a un cron real del sistema; limita la tasa de endpoints admin-ajax; cachea agresivamente para usuarios anónimos.

Broma corta #2: “Too many connections” es la forma de MySQL de decir que necesita menos reuniones en su calendario.

Tres micro-historias del mundo corporativo desde el frente

Micro-historia 1: El outage causado por una suposición equivocada

El equipo tenía un conjunto WordPress detrás de un balanceador: dos nodos web, un nodo MySQL. Habían sido estables por meses. Luego empezaron a aparecer intermitentes “server has gone away”—solo en algunas páginas, mayormente después de que usuarios navegaban un tiempo y luego hacían clic en algo.

La primera suposición fue predecible: “MySQL está haciendo timeout.” Alguien propuso subir wait_timeout a horas. Otra persona sugirió habilitar conexiones persistentes para “evitar el coste de reconexión.” Sonaba plausible, y estaba equivocado.

El problema real vivía en la capa de red. Un firewall entre la subred web y la subred DB tenía un timeout de TCP inactivo más corto que el de MySQL. Cortaba silenciosamente sesiones inactivas. PHP reutilizaba esas conexiones más tarde y recibía resets. Los logs de MySQL mostraban mensajes de “aborted connection”, pero MySQL no era quien estaba abortando.

La solución fue aburrida: alinear timeouts y habilitar keepalives TCP en los nodos web para que las conexiones inactivas se mantuvieran vivas, o mejor aún, deshabilitar la reutilización persistente donde no era necesaria. La lección quedó: cuando veas “gone away”, verifica quién realmente colgó primero.

Micro-historia 2: La optimización que salió mal

Otra compañía perseguía rendimiento. Redujeron el TTFB de la página añadiendo escalado agresivo de PHP-FPM: alto pm.max_children para que el sitio “soportara picos.” Lo hizo—soportó picos—creándolos.

Durante una campaña de marketing, el tráfico subió. PHP-FPM generó workers felices. Cada worker abrió una conexión MySQL y ejecutó un puñado de consultas. MySQL empezó a retrasarse, así que las solicitudes tardaban más. Más duración significó workers ocupados por más tiempo, así que el pool generó más. Las conexiones alcanzaron el techo. “Too many connections” explotó. Luego los reintentos del app y algunos proxies upstream hicieron lo suyo.

Habían optimizado para lo incorrecto: rendimiento en la capa web, no capacidad end-to-end. La solución correcta no fue “subir max_connections a 2000.” Fue limitar la concurrencia de PHP a lo que la BD podía servir con latencia aceptable, y cachear tráfico anónimo para que la mayoría de las solicitudes no necesitara MySQL.

Tras afinar, el sistema manejó la misma campaña con menos workers, menos conexiones a BD y más hits de cache. En un benchmark sintético parecía más lento en “cuántos workers puedo generar”. Para usuarios reales fue más rápido. Ese es el único metric que importa.

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

Un despliegue empresarial de WordPress corría sobre MySQL con réplicas. Nada fancy. La salsa secreta fue disciplina: cada cambio de esquema se programaba, cada actualización de plugin tenía soak en staging, y el slow query logging estaba activado en horas de negocio con umbrales sensatos.

Una tarde, una actualización de plugin introdujo un nuevo patrón de consulta en wp_postmeta. No fue catastrófico. Solo más lento. Pero lo suficiente para que el conteo de conexiones empezara a subir con tráfico normal. El on-call lo notó porque tenían alertas sobre “Threads_connected como porcentaje de max_connections” y sobre “percentil 95 del tiempo de consulta” en sus métricas DB.

No esperaron a un outage total. Revirtieron el plugin, eliminaron una opción autoloaded inflada que lo acompañaba y añadieron un índice dirigido en una ventana de mantenimiento. Los usuarios apenas lo notaron. El liderazgo no se enteró. Así es como se gana: prevención tan aburrida que no hace buenas diapositivas.

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

1) “Server has gone away” tras exactamente N segundos de inactividad

  • Síntoma: Errores aparecen cuando los usuarios vuelven al sitio tras estar inactivos; se reproduce con cronómetro.
  • Causa raíz: Desajuste de timeouts (wait_timeout vs timeout de firewall/LB vs reutilización cliente).
  • Solución: Alinear timeouts; subir wait_timeout moderadamente; deshabilitar conexiones persistentes si existen; configurar keepalives TCP donde corresponda.

2) “Too many connections” durante picos de tráfico, más alta iowait de CPU

  • Síntoma: BD se vuelve lenta, sube conteo de conexiones, iowait se dispara.
  • Causa raíz: Miss del buffer pool + almacenamiento lento; consultas esperan disco.
  • Solución: Incrementar InnoDB buffer pool (dentro de límites de RAM), mejorar almacenamiento, reducir coste de consultas, añadir caching para tráfico anónimo.

3) “Too many connections” justo después de subir PHP-FPM max_children

  • Síntoma: Más workers “mejoran” throughput por un día, luego empiezan incidentes.
  • Causa raíz: Concurrencia upstream excede capacidad DB; tormentas de conexiones.
  • Solución: Limitar PHP-FPM a capacidad DB; añadir encolamiento y backpressure; considerar réplica de lectura separada si la app lo soporta.

4) “Server has gone away” al guardar entradas o actualizar plugins

  • Síntoma: Acciones de admin fallan; los logs muestran errores de packet o conexiones abortadas.
  • Causa raíz: max_allowed_packet demasiado pequeño, o escrituras largas alcanzando timeouts.
  • Solución: Subir max_allowed_packet; inspeccionar qué datos se almacenan; impedir que plugins metan blobs gigantes en options.

5) “Error establishing a database connection” intermitente

  • Síntoma: Error genérico de conexión a BD en WordPress, no siempre “too many connections”.
  • Causa raíz: MySQL reinicia, fallos DNS, agotamiento de sockets, o throttling de autenticación.
  • Solución: Revisar uptime y logs de crash de MySQL; fijar host DB por IP si DNS es poco fiable; verificar límites de descriptores de archivos; auditar fallos de auth.

6) Subir max_connections hace caer el host

  • Síntoma: Menos errores “too many connections”, pero la latencia empeora; luego mysqld es matado por OOM.
  • Causa raíz: Memoria por conexión + overhead de scheduling; swapping u OOM.
  • Solución: Revertir a un max_connections más seguro; limitar PHP; reducir buffers por hilo; añadir caching; escalar verticalmente o mover BD a host dedicado.

Listas de verificación / plan paso a paso

Checklist de respuesta a incidentes (15–30 minutos)

  1. Confirmar salud de la BD: comprobar systemctl status mysql y uptime.
  2. Buscar OOM/disco: dmesg líneas OOM; df -h y df -ih.
  3. Medir saturación: Threads_connected vs max_connections.
  4. Encontrar culpables: processlist; usuarios/hosts principales; consultas largas.
  5. Aplicar backpressure: reducir concurrencia PHP-FPM (o drenar temporalmente un nodo web) antes de tocar perillas de BD.
  6. Activar slow log brevemente: capturar patrones de consulta; no dejarlo siempre con umbrales muy bajos.
  7. Estabilizar: confirmar que la tasa de errores baja; vigilar latencia; asegurar que MySQL no se reinicie de nuevo.

Plan de estabilización (día siguiente)

  1. Arreglar consultas lentas: índices, cambios de plugin, reescrituras de consultas.
  2. Auditar autoload de wp_options: eliminar bloat, deshabilitar culpables, mover caching fuera de la BD.
  3. Alinear timeouts: MySQL, PHP, servidor web, balanceador de carga, firewall.
  4. Dimensionar buffer pool correctamente: basado en RAM y carga, no en folklore.
  5. Añadir monitorización: utilización de conexiones, latencia de consultas, métricas de InnoDB, crecimiento de disco, eventos OOM.

Plan de hardening (sprint siguiente)

  1. Separar roles: poner MySQL en un host dedicado o servicio gestionado si puedes.
  2. Cachear tráfico anónimo: full-page cache + object cache para reducir carga en BD.
  3. Limitar concurrencia intencionalmente: fijar PHP-FPM pm.max_children para proteger MySQL.
  4. Higiene operativa: cambios de esquema programados, política de actualizaciones de plugins y un plan de rollback que no implique oración.

Preguntas frecuentes

1) ¿Debería simplemente aumentar max_connections para arreglar “Too many connections”?

Sólo como alivio temporal y después de comprobar el margen de memoria. La solución durable es reducir tiempo de consulta y la concurrencia upstream.

2) ¿Cuál es la forma más rápida de detener el outage ahora mismo?

Limitar la concurrencia en la capa web (PHP-FPM max_children o drenar un nodo web) para que MySQL pueda ponerse al día. Luego busca consultas lentas.

3) ¿Por qué veo “server has gone away” pero MySQL parece sano?

Porque la conexión puede ser cerrada por firewalls, balanceadores, gateways NAT o timeouts del cliente. Verifica timeouts y resets de red, no solo uptime de mysqld.

4) ¿Esto lo causa WordPress en sí o un plugin?

El core de WordPress normalmente no es el culpable directo. Los plugins suelen crear patrones de consulta patológicos (especialmente consultas meta) o inflar opciones autoloaded.

5) ¿Las conexiones persistentes a MySQL ayudan al rendimiento de WordPress?

A veces, pero también pueden amplificar errores “gone away” y acaparamiento de conexiones. En la mayoría de setups modernos, arreglar el coste de consultas y cachear rinde más.

6) ¿Qué valores de timeout debería usar?

No hay respuesta universal. Pero wait_timeout=60 frecuentemente es demasiado bajo para redes en capas. Empieza alrededor de 300–900 segundos y alínea con los timeouts de tus dispositivos de red.

7) ¿Por qué ocurre más durante acciones de administración?

Las acciones de admin disparan consultas más pesadas, escrituras y a veces cambios de esquema. También exponen límites de max_allowed_packet cuando se actualizan blobs grandes en options.

8) ¿Puede una réplica solucionar “too many connections”?

Sólo si tu aplicación realmente envía lecturas a ella. WordPress clásico no divide lecturas/escrituras automáticamente sin herramientas adicionales. El caching suele ser la primera victoria.

9) ¿Cómo sé si el I/O de disco es el verdadero cuello de botella?

Mira iowait alto, tiempos de consultas lentas que se correlacionan con actividad de disco, pendings de flush en InnoDB y tasas de acierto bajas del buffer pool. Si el almacenamiento es lento, todo lo demás es teatro.

10) ¿MariaDB es diferente en esto?

Los modos de fallo son similares: límites de conexiones, timeouts, coste de memoria/hilos y consultas lentas. Las variables y defaults concretos pueden diferir, así que verifica en tu versión.

Conclusión: próximos pasos prácticos

Estos errores de MySQL no son misterios. Son estados contables: te quedaste sin conexiones, tiempo, memoria o paciencia en alguna parte del stack.

  1. Hoy: ejecuta el playbook de diagnóstico rápido, limita la concurrencia de PHP, confirma que MySQL no se está reiniciando y captura consultas lentas.
  2. Esta semana: elimina o arregla los patrones de consulta peores (a menudo causados por plugins), reduce el bloat autoloaded en options y alinea timeouts entre MySQL y capas de red.
  3. Este sprint: añade caching, establece límites de capacidad explícitos y pon monitorización/alertas sobre utilización de conexiones y latencia de consultas para que veas el precipicio antes de caer.

Si haces un cambio cultural: deja de tratar max_connections como una perilla de rendimiento. Es un disyuntor. Sízalo con cuidado y diseña tu stack para que rara vez importe.

← Anterior
Errores SMTP 4xx temporales: causas principales y soluciones que realmente funcionan
Siguiente →
Generación de frames: ¿frames gratis o una trampa de latencia?

Deja un comentario