Ubuntu 24.04: MySQL “server has gone away” — arregle correctamente timeouts y límites de paquetes (caso #36)

¿Te fue útil?

“MySQL server has gone away” es el equivalente en bases de datos de que un compañero desaparezca a mitad de una reunión: la sala queda en silencio, todos miran su portátil y de algún modo ahora es tu problema.

En Ubuntu 24.04, normalmente esto no es “un bug de MySQL”. Es una descoordinación de timeouts, un límite de tamaño de paquete, un proxy que cree estar ayudando, o un reinicio de mysqld que no notaste porque la aplicación solo registra “SQLSTATE[HY000]”. Vamos a solucionarlo correctamente: encontrar el cuello de botella rápido, demostrar la causa raíz con comandos, y afinar la capa adecuada sin convertir tu base de datos en una piñata de memoria sin límites.

Qué significa realmente “server has gone away” (y qué no significa)

La frase es engañosamente amistosa. Implica que el servidor salió por un café y volverá enseguida. En la práctica significa: el cliente intentó usar una conexión MySQL y la otra parte ya no estaba, o el flujo del protocolo se rompió de una forma que el cliente no puede recuperar.

Formas comunes de la falla

  • Error 2006: MySQL server has gone away (el cliente nota que el socket está muerto o no puede enviar).
  • Error 2013: Lost connection to MySQL server during query (la consulta empezó y la conexión se rompió en medio).
  • Envoltorios SQLSTATE[HY000] de drivers de aplicación (PDO, mysqli, JDBC) que ocultan el número de error y el contexto.

Qué no es

No es automáticamente “la base de datos está sobrecargada” y no se soluciona “simplemente aumentando todos los timeouts”. Aumentar timeouts a ciegas suele ocultar fugas (conexiones perdidas, transacciones abiertas) hasta que un día te quedas sin memoria o sin hilos.

Sólo hay unas pocas categorías raíz:

  1. Conexión inactiva terminada por MySQL (wait_timeout) o por un proxy/LB/NAT intermedio.
  2. Paquete demasiado grande: MySQL lo rechaza (max_allowed_packet) o un proxy lo rechaza, o el cliente alcanza su propio límite.
  3. Reinicio/fallo del servidor: mysqld murió, fue OOM-kill, o systemd lo reinició.
  4. Red: resets TCP, problemas de MTU, timeouts de conntrack, desajustes de keepalive.
  5. Consulta excede timeouts (lado servidor net_read_timeout/net_write_timeout, timeouts de proxy, timeouts del cliente).

Una frase para llevar en el bolsillo: La esperanza no es una estrategia — comúnmente atribuida en círculos de operaciones; una idea que debes aplicar al solucionar problemas de bases de datos.

Broma #1: Si tu solución es “poner timeouts a 24 horas”, no resolviste el problema, solo lo programaste para el turno de mañana.

Hechos e historia interesantes que puedes usar

  • Hecho 1: “MySQL server has gone away” existe desde las primeras librerías cliente de MySQL; es un mensaje del lado cliente, no una línea del log del servidor.
  • Hecho 2: El clásico wait_timeout por defecto solía estar en 8 horas en muchas instalaciones; los entornos modernos con proxies a menudo necesitan timeouts mucho más cortos y un pool adecuado.
  • Hecho 3: El protocolo está basado en paquetes; sentencias grandes y filas voluminosas estresan tanto max_allowed_packet como las rutas de asignación de memoria.
  • Hecho 4: Gateways NAT y firewalls stateful frecuentemente tienen timeouts TCP inactivas muy por debajo de los valores por defecto de MySQL; cerrarán sockets “saludables” sin avisar educadamente a ninguno de los endpoints.
  • Hecho 5: Los keepalives TCP de Linux por defecto son conservadores (horas). En redes cloud eso básicamente significa “nunca”, lo que permite que conexiones muertas persistan hasta que la próxima escritura falle.
  • Hecho 6: Muchos drivers (especialmente versiones antiguas) no se reconectan automáticamente de forma segura porque reconectar a mitad de una transacción es una mina de errores de corrección.
  • Hecho 7: El max_allowed_packet de MySQL existe tanto en el servidor como en el cliente; aumentar solo un lado puede mantener la falla.
  • Hecho 8: Un culpable moderno común no es MySQL sino la pila de capa 7: un pool de conexiones en la app con vida “infinita” que mantiene sockets que los proxies eliminan.
  • Hecho 9: El error suele aparecer después de despliegues porque los despliegues cambian el comportamiento de conexión: más paralelismo, cargas útiles más grandes, lógica de reintento distinta.

Guion de diagnóstico rápido (primero/segundo/tercero)

Primero: determina si mysqld se reinició o falló

Si mysqld se reinició alrededor del momento de los errores, deja de perseguir límites de paquetes. Tus clientes perdieron conexiones porque el servidor desapareció. Averigua por qué se reinició.

  1. Revisa el journal de systemd por reinicios de mysqld y códigos de salida.
  2. Revisa el log de error de MySQL por marcas de crash y recuperación de InnoDB.
  3. Revisa logs del OOM killer y presión de memoria.

Segundo: decide si es timeouts por inactividad o caída durante la consulta

“Gone away” justo después de un periodo de inactividad apunta a wait_timeout o a timeouts de red/proxy. “Lost connection during query” apunta a consultas largas, timeouts de proxy, o timeouts net del servidor.

  1. Compara las marcas temporales de errores con los periodos de inactividad en los logs de la app.
  2. Revisa las tendencias de Aborted_clients y Aborted_connects en MySQL.
  3. Inspecciona los ajustes de timeout del proxy/LB si existen.

Tercero: prueba límites de paquetes (solo después de confirmar que no son reinicios)

Si los errores se correlacionan con inserts/updates grandes o tráfico BLOB, revisa max_allowed_packet en servidor y cliente, y comprueba si la app está enviando sentencias de varios megabytes.

  1. Revisa los valores actuales de max_allowed_packet (global y sesión).
  2. Reproduce con una prueba controlada de carga grande.
  3. Corrige subiendo límites con criterio o cambiando cómo se envían los datos (fragmentación, streaming, evitar mega-consultas).

Tareas prácticas: comandos, salidas, decisiones (haz esto en orden)

Estos no son “ideas”. Son los pasos que ejecutas en Ubuntu 24.04 y lo que concluyes de cada uno. Ejecútalos como usuario con privilegios sudo en el host de BD a menos que se indique lo contrario.

Tarea 1: Confirma qué MySQL estás ejecutando (MySQL vs MariaDB importa)

cr0x@server:~$ mysql --version
mysql  Ver 8.0.39-0ubuntu0.24.04.1 for Linux on x86_64 ((Ubuntu))

Qué significa: Estás en los paquetes Oracle MySQL 8.0. Las variables y rutas de log coinciden con las convenciones de MySQL 8.0.

Decisión: Usa la documentación/variables de MySQL 8.0; no copies perillas específicas de MariaDB.

Tarea 2: Revisa el estado del servicio y reinicios recientes (systemd es el detector de verdad)

cr0x@server:~$ systemctl status mysql --no-pager
● mysql.service - MySQL Community Server
     Loaded: loaded (/usr/lib/systemd/system/mysql.service; enabled; preset: enabled)
     Active: active (running) since Mon 2025-12-30 08:22:10 UTC; 2h 11min ago
   Main PID: 1234 (mysqld)
     Status: "Server is operational"
      Tasks: 44 (limit: 38316)
     Memory: 1.3G
        CPU: 18min 12.345s
     CGroup: /system.slice/mysql.service
             └─1234 /usr/sbin/mysqld

Qué significa: MySQL está activo ahora; también obtienes el tiempo de actividad.

Decisión: Si el tiempo de actividad es sospechosamente corto comparado con cuando empezaron los errores, enfócate en investigación de reinicios/fallos antes de ajustar timeouts.

Tarea 3: Extrae la última hora de logs de mysql.service (busca salidas, OOM, reinicios)

cr0x@server:~$ journalctl -u mysql --since "1 hour ago" --no-pager
Dec 30 09:54:01 server systemd[1]: mysql.service: Main process exited, code=killed, status=9/KILL
Dec 30 09:54:01 server systemd[1]: mysql.service: Failed with result 'signal'.
Dec 30 09:54:01 server systemd[1]: mysql.service: Scheduled restart job, restart counter is at 1.
Dec 30 09:54:04 server systemd[1]: Started mysql.service - MySQL Community Server.

Qué significa: mysqld recibió SIGKILL. Eso no es gracioso. Razones comunes: OOM killer, kill administrativo, watchdog o expulsión de contenedor.

Decisión: No toques max_allowed_packet todavía. Primero averigua quién lo mató.

Tarea 4: Comprueba evidencia del OOM killer (Ubuntu te lo dirá)

cr0x@server:~$ journalctl -k --since "2 hours ago" | grep -E "Out of memory|Killed process|oom-kill" | tail -n 20
Dec 30 09:53:59 server kernel: Out of memory: Killed process 1234 (mysqld) total-vm:9123456kB, anon-rss:6212340kB, file-rss:0kB, shmem-rss:0kB, UID:112 pgtables:15432kB oom_score_adj:0
Dec 30 09:53:59 server kernel: oom_reaper: reaped process 1234 (mysqld), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB

Qué significa: MySQL fue terminado por agotamiento de memoria. Tu app ve “server has gone away” porque efectivamente murió.

Decisión: Arregla el dimensionamiento de memoria, la concurrencia o las fugas; subir límites de paquetes puede empeorarlo.

Tarea 5: Localiza y sigue el log de errores de MySQL (captura recuperación de crash, errores de paquete)

cr0x@server:~$ sudo ls -l /var/log/mysql/
total 120
-rw-r----- 1 mysql adm 118432 Dec 30 10:32 error.log
cr0x@server:~$ sudo tail -n 60 /var/log/mysql/error.log
2025-12-30T09:54:04.123456Z 0 [System] [MY-010116] [Server] /usr/sbin/mysqld (mysqld 8.0.39) starting as process 5678
2025-12-30T09:54:05.234567Z 0 [Warning] [MY-010068] [Server] CA certificate ca.pem is self signed.
2025-12-30T09:54:06.345678Z 0 [System] [MY-010931] [Server] /usr/sbin/mysqld: ready for connections. Version: '8.0.39'  socket: '/var/run/mysqld/mysqld.sock'  port: 3306  MySQL Community Server - GPL.

Qué significa: Ves marcadores de reinicio y disponibilidad; si hubiera errores de paquete, a menudo verías “Got packet bigger than ‘max_allowed_packet’ bytes”.

Decisión: Si los logs muestran recuperación de crash o reinicios frecuentes, arregla la estabilidad primero. Si muestran errores de tamaño de paquete, pasa a la sección de paquetes.

Tarea 6: Revisa variables actuales de timeout y paquetes (la verdad global)

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

Qué significa: Paquetes de 64MB, timeout de inactividad de 8 horas y timeouts net bastante estándar.

Decisión: Si los errores ocurren después de ~8 horas de inactividad, probablemente sea wait_timeout. Si los errores ocurren alrededor de ~60 segundos durante grandes conjuntos de resultados, sospecha de net_write_timeout o de timeouts de proxy.

Tarea 7: Revisa variables de sesión desde la perspectiva de la app (el pooling puede diferir)

cr0x@server:~$ mysql -uroot -p -e "SHOW SESSION VARIABLES WHERE Variable_name IN ('wait_timeout','max_allowed_packet');"
+--------------------+----------+
| Variable_name      | Value    |
+--------------------+----------+
| max_allowed_packet | 67108864 |
| wait_timeout       | 28800    |
+--------------------+----------+

Qué significa: La sesión actual hereda los valores globales. Las aplicaciones pueden cambiar valores de sesión al conectar.

Decisión: Si la app usa una cuenta diferente con comandos init distintos, revisa esa cuenta o la configuración del pool de conexiones.

Tarea 8: Observa contadores de conexiones abortadas (detecta timeouts y caídas de red)

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

Qué significa: Aborted_clients cuenta conexiones abortadas (cliente desapareció, problema de red, timeout). No es perfectamente diagnóstico, pero las tendencias importan.

Decisión: Si Aborted_clients sube rápidamente durante incidentes, investiga caídas por inactividad (proxy/NAT) o sobrecarga del servidor que cause escrituras bloqueadas.

Tarea 9: Revisa conexiones actuales y qué están haciendo (inactivas vs atascadas)

cr0x@server:~$ mysql -uroot -p -e "SHOW PROCESSLIST;"
+-----+------+-----------------+------+---------+------+------------------------+------------------+
| Id  | User | Host            | db   | Command | Time | State                  | Info             |
+-----+------+-----------------+------+---------+------+------------------------+------------------+
| 101 | app  | 10.10.0.21:5332 | prod | Sleep   | 7200 |                        | NULL             |
| 102 | app  | 10.10.0.21:5333 | prod | Sleep   | 7199 |                        | NULL             |
| 201 | app  | 10.10.0.22:6121 | prod | Query   |   55 | Sending data           | SELECT ...       |
+-----+------+-----------------+------+---------+------+------------------------+------------------+

Qué significa: Tienes conexiones pooled durmiendo largo tiempo (2 horas). Eso está bien si tu red lo soporta. Es un problema si un proxy mata sockets inactivos a los 60 minutos.

Decisión: Si ves muchas conexiones Sleep muy antiguas y la app falla tras inactividad, alinea la vida máxima del pool con los timeouts del servidor/proxy.

Tarea 10: Revisa resets y retransmisiones a nivel TCP (¿la red miente?)

cr0x@server:~$ ss -tan sport = :3306 | head -n 20
State  Recv-Q Send-Q Local Address:Port Peer Address:Port  Process
ESTAB  0      0      10.0.0.10:3306   10.10.0.21:5332
ESTAB  0      0      10.0.0.10:3306   10.10.0.21:5333
ESTAB  0      0      10.0.0.10:3306   10.10.0.22:6121

Qué significa: Las conexiones existen y no están obviamente congestionadas. Para señales de red más profundas, usa nstat.

cr0x@server:~$ nstat -az | egrep "TcpRetransSegs|TcpExtTCPRcvCoalesce|TcpExtListenOverflows|TcpExtListenDrops" || true
TcpRetransSegs                124
TcpExtListenOverflows         0
TcpExtListenDrops             0

Decisión: Si las retransmisiones se disparan durante los errores, no es “un ajuste de MySQL”. Investiga la ruta de red, MTU, congestión o saturación de CPU del host.

Tarea 11: Valida settings TCP keepalive de Linux (a menudo muy laxos)

cr0x@server:~$ sysctl net.ipv4.tcp_keepalive_time net.ipv4.tcp_keepalive_intvl net.ipv4.tcp_keepalive_probes
net.ipv4.tcp_keepalive_time = 7200
net.ipv4.tcp_keepalive_intvl = 75
net.ipv4.tcp_keepalive_probes = 9

Qué significa: Los keepalives comienzan después de 2 horas. Si tu firewall corta inactividad a los 15 minutos, estos no ayudarán.

Decisión: Si debes mantener conexiones de larga vida, reduce el tiempo de keepalive (o mejor: establece la vida máxima del pool por debajo del timeout de la red).

Tarea 12: Busca errores de tamaño de paquete en los logs de MySQL

cr0x@server:~$ sudo grep -E "max_allowed_packet|packet bigger than" -n /var/log/mysql/error.log | tail -n 20
2025-12-30T10:01:12.123456Z 45 [Warning] [MY-000000] [Server] Got packet bigger than 'max_allowed_packet' bytes

Qué significa: Ese es tu indicio claro. El servidor rechazó un paquete entrante.

Decisión: Aumenta max_allowed_packet a un valor justificado y corrige el comportamiento de la app que generó paquetes gigantes si es posible.

Tarea 13: Reproduce el límite de paquete de forma segura (prueba controlada, no en pico de producción)

cr0x@server:~$ python3 - << 'PY'
import os
print(len(os.urandom(10*1024*1024)))
PY
10485760

Qué significa: Puedes generar un BLOB de 10MB. Ahora prueba un insert con binding de parámetros (preferible) usando tu stack de app, o una prueba local con mysql si es aceptable.

Tarea 14: Confirma las fuentes de configuración en Ubuntu (evita editar el archivo equivocado)

cr0x@server:~$ mysql -uroot -p -e "SHOW VARIABLES LIKE 'pid_file'; SHOW VARIABLES LIKE 'socket';"
+---------------+------------------------------+
| Variable_name | Value                        |
+---------------+------------------------------+
| pid_file      | /var/run/mysqld/mysqld.pid   |
+---------------+------------------------------+
+---------------+------------------------------+
| Variable_name | Value                        |
+---------------+------------------------------+
| socket        | /var/run/mysqld/mysqld.sock  |
+---------------+------------------------------+

Decisión: En el empaquetado de Ubuntu, normalmente ajustas settings de MySQL en /etc/mysql/mysql.conf.d/mysqld.cnf (o archivos drop-in). Verifica con mysqld --verbose --help si tienes dudas.

Tarea 15: Prueba presión de conexiones máximas (agotamiento de hilos puede parecer “gone away”)

cr0x@server:~$ mysql -uroot -p -e "SHOW GLOBAL STATUS LIKE 'Threads_connected'; SHOW GLOBAL STATUS LIKE 'Max_used_connections';"
+-------------------+-------+
| Variable_name     | Value |
+-------------------+-------+
| Threads_connected | 312   |
+-------------------+-------+
+---------------------+-------+
| Variable_name       | Value |
+---------------------+-------+
| Max_used_connections| 498   |
+---------------------+-------+

Qué significa: Estás cerca de max_connections (500). Cuando alcanzas el límite, las apps a menudo fallan al conectar y reportan mal el error.

Decisión: Si Max_used_connections se aproxima al límite, arregla el pooling y fugas de conexión antes de aumentar max_connections. Subirlo puede convertir “errores de conexión” en “OOM kill”.

Timeouts que importan: wait_timeout, net_*_timeout, proxies y keepalives TCP

Empieza con la línea temporal, no con los nombres de variables

La manera más rápida de resolver “gone away” es trazar el tiempo. No dashboards con cuarenta colores; una línea temporal simple:

  • ¿Cuándo abrió la aplicación la conexión?
  • ¿Cuánto tiempo estuvo inactiva?
  • ¿Cuándo ocurrió el error: en la primera consulta tras inactividad, o en medio de una consulta?
  • ¿Hay un proxy entre la app y MySQL?

Si los errores ocurren en la primera consulta tras una larga inactividad, miras un desajuste de timeout por inactividad. Si ocurren durante una consulta larga, miras timeouts de lectura/escritura de consulta o un reinicio/fallo del servidor.

wait_timeout e interactive_timeout: la guillotina de inactividad de MySQL

wait_timeout es cuánto tiempo el servidor permite que una sesión no interactiva esté inactiva antes de matarla. La mayoría de conexiones de app son no interactivas. interactive_timeout es para clientes interactivos (como un humano en terminal) cuando el cliente establece la bandera CLIENT_INTERACTIVE.

Lo que suele fallar en producción rara vez es “wait_timeout demasiado bajo”. Normalmente es:

  • Un pool de la app mantiene conexiones por horas.
  • Un firewall/NAT/proxy intermedio corta TCP inactivos a 5–60 minutos.
  • La app conserva el objeto de conexión y trata de reutilizarlo después.
  • La siguiente consulta choca con un socket muerto y obtienes “server has gone away”.

Así que la solución no siempre es “aumentar wait_timeout”. En muchos entornos, quieres reducir el timeout de inactividad del servidor y asegurarte de que el pool no mantiene zombis.

Guía práctica que funciona

  • Establece la vida máxima del pool por debajo del timeout de inactividad más corto en la ruta (NAT/proxy/firewall). Ejemplo: si el LB mata inactivos a 15 minutos, fija la vida máxima del pool en 10 minutos.
  • Prefiere validaciones al tomar la conexión (consulta de prueba) en lugar de pings periódicos si la carga es variable.
  • No pongas wait_timeout a días. Conexiones inactivas de larga vida no son “estabilidad”, son estado obsoleto con buen PR.

net_read_timeout y net_write_timeout: los timeouts “en medio de la consulta”

Estos son timeouts en el servidor para leer desde y escribir hacia un cliente. Se vuelven relevantes cuando:

  • El cliente envía una sentencia grande lentamente (red lenta, cliente sobrecargado), y el servidor decide que esperó suficiente tiempo.
  • El servidor está enviando un conjunto de resultados grande y el cliente no lo está leyendo (CPU del cliente al máximo, retropresión), así que el servidor se bloquea y caduca el timeout.

Si ves Lost connection during query y tienes resultados grandes o escrituras grandes, estas variables son sospechosas. Pero no las subas a ciegas. Pregunta por qué el emisor/receptor está lento. En producción, “cliente lento” suele ser un pool de hilos hambriento por GC, o un worker de PHP atascado haciendo otra cosa mientras mantiene el socket abierto.

Keepalives TCP de Linux: plomería como último recurso

Los keepalives son una forma de bajo nivel para mantener el estado NAT/firewall vivo y detectar peers muertos. Pueden ayudar, pero no sustituyen un pooling correcto.

Si controlas los servidores y debes mantener conexiones de larga vida a través de middleboxes inestables, afinar keepalives puede reducir eventos de “socket muerto sorpresivo”:

  • Reduce tcp_keepalive_time a algo como 300 segundos (5 minutos) si la ruta corta inactividad alrededor de 10–15 minutos.
  • Mantén intervalos razonables (tcp_keepalive_intvl 30–60 segundos) y probes modestos.

Ten cuidado: los keepalives crean tráfico. En flotas muy grandes no es gratis. Pero es más barato que despertar gente a las 3 AM.

Límites de paquetes: max_allowed_packet, filas grandes y consultas voluminosas

Los errores de paquete son directos cuando realmente los capturas. La parte difícil es que muchas pilas no registran la advertencia real de MySQL, así que solo ves “gone away”.

Qué cuenta como “paquete” en este contexto

El protocolo de MySQL transmite datos en paquetes, y max_allowed_packet limita el paquete más grande que el servidor aceptará (y el más grande que enviará). Esto interactúa con:

  • Enormes sentencias INSERT ... VALUES (...), (...), ...
  • Columnas BLOB/TEXT grandes
  • Grandes conjuntos de resultados (el servidor enviando mucho)
  • Replicación y eventos de binlog (sí, el tamaño de paquete también importa allí)

Cómo elegir un valor sin hacer cargo cult

Valores comunes: 16MB, 64MB, 256MB, 1GB. La tentación es poner 1GB “por si acaso”. No lo hagas.

¿Por qué? Porque paquetes más grandes pueden crear buffers por conexión y asignaciones de memoria más grandes. Una sola petición enorme puede presionar la memoria y disparar condiciones OOM, lo que se manifiesta como… adivinaste… “server has gone away”.

Un enfoque razonable:

  1. Mide el tamaño de payload: encuentra la mayor petición/fila realista que envía tu aplicación.
  2. Añade margen: 2–4x suele ser suficiente, no 100x.
  3. Prefiere cambiar la app para evitar mega-sentencias: procesa en lotes más pequeños, transmite blobs, guarda objetos grandes fuera, o al menos comprime.

Los límites del lado cliente también importan

Muchos clientes tienen su propio max_allowed_packet o equivalente. Si aumentas el servidor pero el cliente sigue negándose, seguirás fallando—a veces con errores específicos del driver y confusos.

Arreglarlo correctamente en Ubuntu (MySQL 8.0)

Quieres un cambio persistente en la configuración, no un SET GLOBAL temporal que desaparece al reiniciar.

Cambio de ejemplo (ilustrativo): establece max_allowed_packet=128M si tienes evidencia de que lo necesitas.

cr0x@server:~$ sudo grep -n "max_allowed_packet" /etc/mysql/mysql.conf.d/mysqld.cnf || true
cr0x@server:~$ sudo bash -lc 'printf "\n[mysqld]\nmax_allowed_packet=128M\n" >> /etc/mysql/mysql.conf.d/mysqld.cnf'
cr0x@server:~$ sudo systemctl restart mysql

Qué significa: Lo has establecido en el arranque del servidor. Ahora verifica.

cr0x@server:~$ mysql -uroot -p -e "SHOW GLOBAL VARIABLES LIKE 'max_allowed_packet';"
+--------------------+-----------+
| Variable_name      | Value     |
+--------------------+-----------+
| max_allowed_packet | 134217728 |
+--------------------+-----------+

Decisión: Si los errores cesan y la memoria permanece estable, mantenlo. Si la memoria sube o aumentan eventos OOM, tu “arreglo” es una granada con la anilla parcialmente sujeta.

Broma #2: Un max_allowed_packet de 1GB es como comprar un cubo de basura más grande en lugar de sacar la basura; funciona hasta que no, y entonces es espectacular.

La causa sigilosa: reinicios, fallos, OOM y systemd

En producción, “gone away” suele ser muy literal. mysqld murió, se reinició o fue reemplazado durante mantenimiento. Tus clientes no fueron consultados.

Por qué MySQL se reinicia en Ubuntu 24.04

  • OOM killer por presión de memoria (común cuando buffers están sobredimensionados o la carga cambió).
  • Eventos del kernel o hipervisor (reboot del host, problemas de migración en vivo, stalls de almacenamiento que activan watchdogs).
  • Actualizaciones de paquetes que reinician servicios.
  • Acciones humanas (reinicio por cambio de configuración) sin coordinar con pools y reintentos.
  • Crashes (existen bugs; pero asume recurso y configuración primero).

Cómo hacer que los reinicios duelan menos

Dos ángulos: reducir la frecuencia y reducir el radio de impacto.

  • Reducir la frecuencia: dimensiona memoria correctamente; evita consultas patológicas; asegúrate de que el almacenamiento esté sano; no ejecutes la máquina al 99% de RAM con swap deshabilitado y fe en la poesía.
  • Reducir el radio de impacto: usa reintentos en la app con idempotencia; usa pools que validen conexiones; establece vidas máximas cortas en el pool; considera MySQL Router/ProxySQL si procede (y entonces gestiona también sus timeouts).

Detectar bucles de reinicio rápidamente

Si ves reinicios frecuentes, necesitas un incidente de estabilidad, no un ejercicio de tuning.

cr0x@server:~$ systemctl show mysql -p NRestarts -p ExecMainStatus -p ExecMainCode
NRestarts=3
ExecMainStatus=0
ExecMainCode=0

Qué significa: El servicio se ha reiniciado varias veces desde el arranque.

Decisión: Extrae logs del journal en la ventana de reinicios; revisa OOM y logs de error; considera reducir temporalmente la concurrencia desde la app para estabilizar.

Proxies y balanceadores: los intermediarios de timeouts

Si hay un proxy entre tu app y MySQL (HAProxy, ProxySQL, balanceador cloud, sidecar de service mesh), ahora tienes al menos tres sistemas de timeout independientes:

  • Timeouts del servidor MySQL
  • Timeouts de sesión e inactividad del proxy
  • Timeouts del cliente/driver y ajustes del pool

La mayoría de los casos de “server has gone away después de inactividad” vienen por desajuste: el proxy mata la conexión primero, pero el pool cree que la puede reutilizar para siempre.

Cómo es “correcto”

  • El timeout de inactividad más corto gana. La vida máxima de tu pool debe ser más corta que ese.
  • Keepalive solo donde sea necesario. Si el proxy soporta keepalive TCP, habilítalo allí; no dependas solo de los valores por defecto del kernel.
  • Observa en el proxy. Si el proxy registra razones de desconexión, te ahorrará horas de adivinanza.

Cuando aumentar timeouts tiene sentido

A veces la app realmente necesita consultas de larga duración (análisis, migraciones, backfills). En ese caso:

  • Sube los timeouts de lectura/escritura del proxy/servidor para que excedan la consulta más larga legítima, más un buffer.
  • Prefiere mover trabajo de larga duración fuera del camino de la petición.
  • Usa límites y cancelación adecuados para evitar consultas zombis que nunca terminan.

Tres micro-historias corporativas desde el terreno

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

Tenían una suposición razonable: “Nuestra base y app están en la misma VPC, así que la red es estable.” Razonable no es lo mismo que correcto.

La aplicación usaba un pool con una vida máxima generosa. Las conexiones reposaban inactivas durante horas por la noche y se reutilizaban por la mañana. La tasa de error se disparaba exactamente en el primer pico de tráfico después de las 9am, y luego se estabilizaba lentamente.

El equipo subió wait_timeout porque parecía “desconexiones por inactividad”. Eso no solucionó nada. Lo subieron más. Aún nada. Mientras tanto, los logs del proxy (que nadie leía) mostraban que las sesiones inactivas eran eliminadas a los 60 minutos por una política interna del balanceador.

Una vez alinearon la vida del pool a 45 minutos y habilitaron una consulta de validación al tomar la conexión, el error desapareció. La lección no fue “pon timeouts más altos”. La lección fue “el timeout más corto en la ruta decide tu destino”.

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

Un equipo quiso reducir viajes de red. Cambiaron un camino de importación masiva para construir INSERTs multi-fila enormes. La CPU bajó, el throughput subió y todos se felicitaron.

Luego un cliente empezó a importar registros más grandes con largos blobs JSON. La primera falla no fue un “paquete demasiado grande” gracioso. Fue un “server has gone away” intermitente en la app, seguido de un reinicio de MySQL minutos después. On-call culpó a la red. Red culpó a la app. App culpó a MySQL.

La causa raíz fue fea pero simple: sentencias gigantes aumentaron el uso de memoria (parseo del servidor más buffers más overhead por conexión), y bajo concurrencia pico MySQL fue OOM-kill. La “optimización” incrementó el tamaño del peor caso y agudizó picos de memoria. Funcionó hasta que no.

La solución fue doble: limitar el tamaño de los lotes para mantener sentencias por debajo de un máximo sensato, y fijar max_allowed_packet a un valor basado en necesidades reales. Mantuvieron la mayor parte de la ganancia de rendimiento y la base dejó de morir.

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

Otra organización tenía una práctica que parecía aburrida en papel: tras cada cambio significativo, capturaban las variables MySQL actuales, el estado de systemd y un pequeño conjunto de parámetros de red del SO en el comentario del ticket. No un volcado gigante—solo lo que explica el comportamiento.

Una tarde empezaron a ver “lost connection during query” esporádico. La primera corazonada fue culpar una migración de esquema reciente. Pero sus “instantáneas aburridas” mostraron que net_write_timeout había sido reducido semanas antes durante un intento equivocado de “fallar rápido”, y el cambio sobrevivió porque vivía en un archivo drop-in que nadie recordaba.

Revirtieron el timeout y los errores desaparecieron. La migración era inocente. El valor de la práctica no fue la instantánea en sí; fue que les dio una comparación rápida entre suposiciones y realidad.

Ese tipo de corrección aburrida es lo que mantiene tus fines de semana intactos.

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

1) Errores justo después de largos periodos de inactividad

Síntoma: La primera consulta tras inactividad falla con “server has gone away”, reintentos posteriores funcionan.

Causa raíz: Sesiones TCP inactivas son cerradas (proxy/NAT/firewall) o MySQL mata sesiones inactivas (wait_timeout).

Solución: Establece la vida máxima del pool por debajo del timeout más corto en la ruta; añade validación de conexión; opcionalmente ajusta keepalive TCP. Si el servidor está matando inactivos y realmente necesitas inactividad larga, aumenta wait_timeout—pero solo si la red puede mantenerlas.

2) Errores durante inserts/updates grandes

Síntoma: Fallos correlacionados con escrituras masivas, JSON grande, cargas BLOB o sentencias multi-fila grandes.

Causa raíz: Límite de paquete excedido (max_allowed_packet) en servidor o cliente; o presión de memoria por sentencias enormes.

Solución: Confirma el mensaje en logs; incrementa max_allowed_packet a un valor justificado; reduce tamaños de lotes; cambia a patrones de streaming/fragmentación.

3) “Lost connection during query” en endpoints lentos

Síntoma: Consulta larga corre y luego falla. O grandes conjuntos de resultados fallan cuando el cliente está ocupado.

Causa raíz: Timeout de lectura/escritura del proxy o net_read_timeout/net_write_timeout de MySQL demasiado bajos para el comportamiento real; cliente no lee lo suficientemente rápido.

Solución: Aumenta los timeouts relevantes para exceder tiempos realistas de consulta; arregla la app para hacer streaming de resultados; evita traer conjuntos masivos a memoria; añade paginación.

4) Errores que se agrupan con reinicios de MySQL

Síntoma: Errores de conexión se concentran alrededor de tiempos de reinicio del servicio; el uptime se resetea.

Causa raíz: mysqld crash, OOM kill, reinicio por paquete, reboot del host.

Solución: Diagnostica la razón del reinicio vía journal + log de errores; arregla dimensionamiento de memoria; reduce concurrencia; considera comportamiento de swap; limita consultas/paquetes de peor caso.

5) Errores que aparecen después de aumentar max_connections

Síntoma: Menos “too many connections”, pero más “gone away” y reinicios.

Causa raíz: Más conexiones concurrentes aumentaron uso de memoria y cambios de contexto; el servidor se vuelve inestable.

Solución: Reduce conexiones con pooling adecuado; añade un limitador de conexiones; dimensiona buffers correctamente; no escales a la fuerza bruta.

6) Errores solo en un entorno (prod, no staging)

Síntoma: Staging está bien, producción falla intermitentemente.

Causa raíz: Diferentes middleboxes/redes/timeouts, diferentes settings de pool, distintos tamaños de payload, distinta concurrencia o distinta configuración de MySQL.

Solución: Compara el timeout más corto en la ruta; revisa tamaños reales de payload; diff de variables MySQL; reproduce con volumen de datos parecido a producción.

Listas de verificación / plan paso a paso

Plan paso a paso: detén la hemorragia y luego arregla bien

  1. Confirma si MySQL se reinició. Usa systemctl status mysql y journalctl -u mysql. Si sí, la prioridad es estabilidad.
  2. Revisa por OOM kills. Usa journalctl -k filtrando mensajes OOM. Si hay OOM: reduce uso de memoria y concurrencia antes de subir límites.
  3. Sigue el log de errores de MySQL. Busca advertencias de paquete y marcadores de recuperación por crash.
  4. Clasifica el momento del error. Después de inactividad vs durante consulta cambia toda la ruta de investigación.
  5. Inventaría timeouts en capas. MySQL (wait_timeout, net_*), proxy, cliente/pool, keepalive del OS.
  6. Arregla el desajuste del timeout más corto. Ajusta vida del pool y validación; no intentes esperar más que un firewall con optimismo.
  7. Solo entonces ajusta variables de MySQL. Sube max_allowed_packet si hay evidencia; ajusta net_write_timeout/net_read_timeout si hay caídas mid-query.
  8. Vuelve a probar con una reproducción controlada. Usa un payload conocido, tiempo de consulta conocido y vigila logs y contadores.
  9. Bloquea cambios en gestión de configuración. Evita archivos drop-in misteriosos y SET globales “temporales” que desaparecen al reiniciar.
  10. Monitorea lo que importa. Uptime, reinicios, eventos OOM, clientes abortados, conexiones usadas y distribución de latencia.

Lista: reglas seguras para tuning de timeouts

  • No pongas timeouts de inactividad del servidor más largos de lo que la red puede mantener.
  • Vida máxima del pool < timeout de inactividad más corto de la red/proxy.
  • Validación al tomar conexión supera a pings periódicos en tráfico con picos.
  • Aumenta read/write timeouts solo tras confirmar consultas largas legítimas o clientes lentos.

Lista: reglas seguras para tuning de paquetes

  • Encuentra primero el payload legítimo más grande.
  • Aumenta en pasos (por ejemplo, 64M → 128M), no saltos a 1G.
  • Limita tamaños de lote; evita construir mega-consultas.
  • Observa la memoria tras el cambio; tamaño de paquete y OOM son amigos cercanos.

Preguntas frecuentes

1) ¿“MySQL server has gone away” es siempre un timeout?

No. Puede ser un timeout, un límite de paquete, un reinicio del servidor o un reset de red. Tu primer trabajo es determinar en qué categoría estás.

2) ¿Debería simplemente aumentar wait_timeout para detener desconexiones por inactividad?

Sólo si MySQL es la capa que mata inactivos y realmente necesitas conexiones inactivas de larga duración. En muchos entornos un proxy/NAT corta la sesión TCP primero, así que aumentar wait_timeout no ayuda. Arregla la vida del pool y la validación primero.

3) ¿Cuál es un buen valor para max_allowed_packet?

Uno basado en tamaños de payload medidos más un margen. 64MB o 128MB suele funcionar para apps que ocasionalmente mueven blobs medianos. Si necesitas cientos de MB, reevalúa el diseño (fragmentación, almacenamiento de objetos, streaming).

4) ¿Por qué lo veo sólo después del despliegue?

Los despliegues cambian el comportamiento de conexión: más procesos worker, defaults de pooling diferentes, peticiones más grandes, lógica de reintento distinta o un nuevo salto de proxy. Además, upgrades de paquetes pueden reiniciar mysql.service.

5) ¿Cuál es la diferencia entre el error 2006 y 2013?

2006 suele indicar que la conexión ya estaba muerta al intentar usarla. 2013 usualmente indica que la conexión murió durante una consulta activa o transferencia de datos. Señalan sospechosos distintos.

6) ¿Puede max_connections causar “gone away”?

Indirectamente. Alcanzar max_connections provoca fallos de conexión; dependiendo del driver, la app puede reportarlo mal. Aumentarlo también puede incrementar presión de memoria y provocar crashes, que luego generan “gone away” reales.

7) ¿La aplicación debería auto-reconectarse?

Auto-reconectar es peligroso si puedes estar dentro de una transacción. El enfoque más seguro es: usa pooling con validación, maneja reintentos a un nivel superior con operaciones idempotentes, y falla rápido en escrituras no idempotentes.

8) ¿Los keepalives TCP solucionan esto de forma fiable?

Ayudan con timeouts de NAT/firewall inactivos y detección de peers muertos, pero no sustituyen pools y timeouts correctamente configurados. Úsalos como plomería, no como arquitectura.

9) ¿Y si el log de error de MySQL no muestra nada?

Entonces o estás mirando en el lugar equivocado, el logging está mal configurado, o la desconexión ocurre fuera de MySQL (proxy/red). Confirma rutas de log, revisa el journal de systemd y revisa logs del proxy si hay.

10) Subí max_allowed_packet y sigue fallando. ¿Ahora qué?

Confirma el límite del lado cliente, confirma que la configuración se aplicó realmente (global vs sesión) y confirma el tamaño del payload. También revisa límites en proxies y eventos OOM causados por asignaciones mayores.

Conclusión: próximos pasos que puedes hacer hoy

Si tomas un solo hábito operativo de esto: deja de tratar “server has gone away” como un solo bug. Es una clase de síntomas. Catégorizalo rápido.

  1. Revisa reinicios primero (systemctl status, journalctl, logs OOM). Si mysqld murió, arregla la estabilidad antes de tocar perillas.
  2. Decide inactividad vs mid-query por timing y tipo de error (2006 vs 2013). Eso te dirá si centrarte en wait_timeout/pooling o en timeouts de lectura/escritura/consulta/proxy.
  3. Demuestra problemas de paquete con evidencia de logs y una reproducción controlada. Aumenta max_allowed_packet a un número justificado y luego ajusta batching para no crear acantilados de memoria.
  4. Alinea timeouts entre capas (MySQL, proxy/LB, pool, keepalive TCP). El más corto gana; actúa en consecuencia.

Haz eso y “server has gone away” dejará de ser un fantasma intermitente para convertirse en un incidente normal con causa raíz y un postmortem corto. Tu yo futuro seguirá estando cansado, pero al menos estará cansado por razones interesantes.

← Anterior
Errores de checksum en ZFS: qué significan y qué hacer primero
Siguiente →
ZFS acltype: ACLs POSIX vs NFSv4 sin confusión

Deja un comentario