Son las 02:13. Tu API está “up” pero los usuarios ven spinners como si fuera una app de meditación. Entonces los logs traen el remate: ERROR 1040 (08004): Too many connections. MySQL no se cayó. Simplemente dejó de aceptar más conexiones.
La solución tentadora es subir max_connections a un número heroico y dar por terminado el problema. Así es como conviertes un problema de conexiones en uno de memoria, luego en paginación, y después en una reflexión profesional. Arreglémoslo bien en Ubuntu 24.04: encuentra el verdadero cuello de botella, detén la hemorragia y aumenta la concurrencia solo donde sea seguro—sin hacer la base de datos más lenta.
Qué significa realmente “too many connections” (y qué no)
MySQL lanza “too many connections” cuando no puede aceptar una nueva conexión de cliente porque alcanzó un techo interno. Más comúnmente, ese techo es max_connections. A veces es otra pared: límites de descriptores de archivos del SO, límites del servicio systemd, o agotamiento de recursos que hace que MySQL en la práctica no pueda crear más sesiones.
Aquí está lo que la gente no ve: “too many connections” rara vez es causado por “demasiado tráfico” en el sentido simplista. Usualmente es causado por conexiones que no desaparecen lo suficientemente rápido. Eso puede ser queries lentas, transacciones bloqueadas, CPU sobrecargada, I/O saturado, un buffer pool mal dimensionado que provoca lecturas constantes desde disco, o código de aplicación que fuga conexiones. La base de datos te está diciendo que se está ahogando en concurrencia. Tu trabajo es averiguar si el agua viene de demasiados grifos abiertos o de un desagüe atascado.
Una opinión que te ahorrará tiempo: aumentar max_connections tiene sentido solo después de tener evidencia de que el servidor tiene margen (memoria, CPU, I/O) y que la aplicación usa las conexiones responsablemente (pooling, timeouts, ámbito de transacción razonable). Si lo subes a ciegas, a menudo conviertes un error agudo en una miseria lenta.
Broma #1 (corta, pertinente): Una fuga de conexiones MySQL es como un goteo en la cocina: a nadie le importa hasta que la factura del agua empieza a llamarte.
Guía de diagnóstico rápido: primeras/segundas/terceras comprobaciones
Esta es la secuencia “estoy de guardia, mi café está frío, dame señales”. Hazla en orden. Cada paso reduce rápidamente la clase de cuello de botella.
Primero: ¿es una fuga de conexiones o un cuello de botella de rendimiento?
- Comprueba threads y estados actuales. Si la mayoría están en
Sleep, probablemente tengas problemas de pooling/fugas en la app o timeouts de inactividad demasiado largos. Si la mayoría están enQueryoLocked, es throughput/bloqueos. - Comprueba si las conexiones están subiendo. Un ascenso sostenido sugiere fuga. Un pico brusco sugiere ráfaga de tráfico o tormenta de reintentos.
Segundo: ¿qué está bloqueando el progreso—CPU, I/O, locks o memoria?
- CPU al máximo + muchas consultas en ejecución: demasiada paralelización, planes malos o índices faltantes.
- I/O de disco saturado + latencia de lectura alta: buffer pool demasiado pequeño, I/O aleatorio por índices pobres o almacenamiento lento.
- Locks/waits dominantes: transacciones largas, filas “calientes” o diseño de esquema que causa contención.
- Presión de memoria/swap: MySQL permitió buffers por conexión demasiado grandes, demasiadas conexiones, o el SO está reclamando agresivamente.
Tercero: ¿estás alcanzando un límite que no es max_connections?
- Descriptores de archivos demasiado bajos: no puede abrir más sockets o tablas.
- Límites de systemd: el servicio
LimitNOFILEsobrescribe lo que crees haber configurado. - Backlog de red: desbordamiento de la cola SYN bajo ráfagas (menos común, pero aparece como timeouts de conexión en lugar de 1040).
Al final de este playbook deberías poder decir una frase que empiece por: “Las conexiones se acumulan porque…”. Si no puedes, estás adivinando. Y adivinar es cómo los “arreglos rápidos” se vuelven arquitectura.
Hechos y contexto interesantes (por qué vuelve a pasar)
- Hecho 1: Históricamente MySQL usaba un modelo “un hilo por conexión” por defecto, lo que hacía que
max_connectionsfuera un proxy directo del recuento de hilos y la presión de memoria. - Hecho 2: Muchos buffers por conexión (
sort_buffer_size,join_buffer_size,read_buffer_size) se asignan por sesión y pueden hacer explotar la memoria cuando la concurrencia sube en picos. - Hecho 3: El
wait_timeoutpor defecto suele ser lo bastante largo como para que las sesiones inactivas estén horas en sistemas con pooling deficiente. - Hecho 4: El efecto “thundering herd”—muchos clientes reintentando a la vez—convierte una pequeña perturbación en una tormenta de conexiones. Suele aparecer con middleware HTTP de reintento agresivo.
- Hecho 5: El buffer pool de InnoDB fue introducido para reducir I/O de disco cachéando páginas; un buffer pool subdimensionado a menudo parece “too many connections” porque las consultas se atascan y las sesiones se acumulan.
- Hecho 6:
thread_cache_sizeimporta más de lo que la gente piensa: crear/destruir hilos en cargas con picos añade latencia y CPU extra. - Hecho 7: Los límites de Linux (ulimits, descriptores de archivos) son tan antiguos como Unix multiusuario; siguen mordiendo a bases de datos modernas porque los valores por defecto son conservadores.
- Hecho 8: En distribuciones con systemd (incluyendo Ubuntu 24.04), los límites a nivel de servicio pueden sobrescribir silenciosamente los
ulimitde shell, confundiendo incluso a operadores experimentados. - Hecho 9: “Aumentar max connections” es un antipatrón famoso porque puede aumentar la contención de locks y reducir las tasas de aciertos de caché, haciendo cada consulta más lenta.
Una idea para enmarcar, para tener presente, atribuida a John Ousterhout: idea parafraseada: la complejidad es el impuesto que pagas después; los mejores sistemas mantienen el camino rápido simple.
El modelo de concurrencia: conexiones, hilos y por qué “más” puede ser más lento
Piensa en un servidor MySQL como un restaurante que sienta clientes (conexiones) y asigna un camarero (hilo). Si añades más mesas sin aumentar la capacidad de cocina, no obtienes más comida—solo más gente esperando, más ruido y peor servicio.
Cuando subes max_connections, no solo dejas entrar más clientes. Permites que se intente más trabajo concurrente. Eso cambia:
- Huella de memoria: cada sesión consume memoria base, más buffers por conexión, tablas temporales y estructuras de locks de metadatos.
- Planificación de CPU: más hilos significa más cambio de contexto. En una CPU saturada, eso puede reducir trabajo útil.
- Contención de locks: más transacciones concurrentes aumentan el tiempo esperando locks, especialmente en tablas/filas “calientes”.
- Profundidad de la cola de I/O: más lecturas/escrituras pendientes pueden superar lo que tu almacenamiento maneja, aumentando latencia para todos.
El objetivo no es “aceptar conexiones infinitas”. El objetivo es “mantener tiempos de respuesta estables en picos”. Eso a menudo significa controlar la concurrencia: usar pooling en la app, limitar el máximo en el pool, usar transacciones cortas y afinar MySQL para que aguante la carga real.
Broma #2 (corta, pertinente): La forma más fácil de manejar 10.000 conexiones MySQL es no tener 10.000 conexiones MySQL.
Tareas prácticas (comandos, salida esperada y decisiones)
Estos son chequeos de grado producción. Ejecútalos durante un incidente o en una ventana de mantenimiento tranquila. Cada tarea incluye: comando, qué significa una salida típica y la decisión que tomas.
Task 1: Confirma la tasa exacta de errores y de dónde vienen
cr0x@server:~$ sudo journalctl -u mysql --since "30 min ago" | tail -n 30
Dec 29 02:08:11 db1 mysqld[1327]: [Warning] Aborted connection 18933 to db: 'app' user: 'appuser' host: '10.0.2.41' (Got an error reading communication packets)
Dec 29 02:08:13 db1 mysqld[1327]: [Note] Too many connections
Dec 29 02:08:14 db1 mysqld[1327]: [Note] Too many connections
Qué significa: MySQL está registrando presión de conexiones, además de conexiones abortadas que pueden indicar problemas de red, timeouts de cliente o sobrecarga del servidor.
Decisión: Si ves muchas líneas “Too many connections”, procede a medir sesiones actuales y sus estados. Si ves mayormente “Aborted connection”, revisa la red, timeouts de cliente y saturación de CPU/I/O del servidor.
Task 2: Verifica los límites de conexión configurados y efectivos
cr0x@server:~$ sudo mysql -e "SHOW VARIABLES LIKE 'max_connections'; SHOW STATUS LIKE 'Max_used_connections';"
+-----------------+-------+
| Variable_name | Value |
+-----------------+-------+
| max_connections | 300 |
+-----------------+-------+
+----------------------+-------+
| Variable_name | Value |
+----------------------+-------+
| Max_used_connections | 298 |
+----------------------+-------+
Qué significa: Estás tocando el techo. Esto no es teórico; lo estás alcanzando.
Decisión: No subas el techo todavía. Primero encuentra por qué las sesiones no terminan. Si Max_used_connections está muy por debajo de max_connections, tu error puede ser otro límite (FDs/systemd) o una capa proxy.
Task 3: Ve qué están haciendo las conexiones ahora mismo
cr0x@server:~$ sudo mysql -e "SHOW FULL PROCESSLIST;" | head -n 25
Id User Host db Command Time State Info
19401 appuser 10.0.2.41:53312 app Sleep 412 NULL
19408 appuser 10.0.2.45:58921 app Query 18 Sending data SELECT ...
19412 appuser 10.0.2.44:51220 app Query 18 Waiting for table metadata lock ALTER TABLE ...
19420 appuser 10.0.2.47:60011 app Sleep 399 NULL
Qué significa: Tienes mezcla: muchas sesiones en Sleep de larga duración (pooling o fugas de la app) y algunas sesiones activas atascadas en locks de metadatos.
Decisión: Si la mayoría están en Sleep con tiempos largos, reduce timeouts de inactividad y arregla el pooling. Si muchas esperan locks de metadatos, tienes un cambio de esquema o una transacción larga bloqueando a los demás—ataca el bloqueador.
Task 4: Cuenta sesiones por estado (señal rápida)
cr0x@server:~$ sudo mysql -NBe "SELECT COMMAND, STATE, COUNT(*) c FROM information_schema.PROCESSLIST GROUP BY COMMAND, STATE ORDER BY c DESC LIMIT 15;"
Sleep 221
Query Sending data 36
Query Waiting for table metadata lock 18
Query Sorting result 6
Qué significa: Tu “problema de conexiones” es mayormente sesiones inactivas más una situación real de contención de locks.
Decisión: Ataca la acumulación de sesiones en Sleep (timeouts + tamaño de pool) y desbloquea el lock de metadatos (termina/mata DDL, evita DDL en picos).
Task 5: Identifica el bloqueador del lock de metadatos
cr0x@server:~$ sudo mysql -e "SELECT OBJECT_SCHEMA, OBJECT_NAME, LOCK_TYPE, LOCK_STATUS, THREAD_ID FROM performance_schema.metadata_locks WHERE LOCK_STATUS='PENDING' LIMIT 10;"
+---------------+-------------+-----------+-------------+----------+
| OBJECT_SCHEMA | OBJECT_NAME | LOCK_TYPE | LOCK_STATUS | THREAD_ID|
+---------------+-------------+-----------+-------------+----------+
| app | orders | EXCLUSIVE | PENDING | 8421 |
+---------------+-------------+-----------+-------------+----------+
cr0x@server:~$ sudo mysql -e "SELECT * FROM performance_schema.threads WHERE THREAD_ID=8421\G" | head -n 20
*************************** 1. row ***************************
THREAD_ID: 8421
NAME: thread/sql/one_connection
PROCESSLIST_ID: 19412
PROCESSLIST_USER: appuser
PROCESSLIST_HOST: 10.0.2.44:51220
PROCESSLIST_DB: app
PROCESSLIST_COMMAND: Query
PROCESSLIST_TIME: 18
Qué significa: Tienes un DDL que necesita un lock exclusivo de metadatos, y otras sesiones pueden estar manteniendo locks en conflicto.
Decisión: Encuentra quién tiene el lock concedido (granted locks en el mismo objeto) y decide si esperar, matar al bloqueador o posponer el DDL. En producción, “posponer el DDL” suele ser la decisión madura.
Task 6: Revisa transacciones largas que mantienen locks activos
cr0x@server:~$ sudo mysql -e "SELECT trx_id, trx_started, trx_mysql_thread_id, trx_query FROM information_schema.innodb_trx ORDER BY trx_started LIMIT 5\G" | sed -n '1,40p'
*************************** 1. row ***************************
trx_id: 42119291
trx_started: 2025-12-29 01:58:03
trx_mysql_thread_id: 19321
trx_query: UPDATE orders SET status='paid' WHERE ...
Qué significa: Una transacción lleva abierta ~10 minutos. Eso es una eternidad en OLTP. Puede mantener locks de fila y prevenir el purge, causando estancamientos en cascada.
Decisión: Si es accidental (worker atascado, path de código malo), mátala y corrige el alcance de la transacción en la app. Si es intencional (job por lotes), prográmalo fuera de pico o refactorízalo en commits por bloques.
Task 7: Comprueba si estás limitado por CPU
cr0x@server:~$ mpstat -P ALL 1 5
Linux 6.8.0-xx-generic (db1) 12/29/2025 _x86_64_ (16 CPU)
02:12:10 PM CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle
02:12:11 PM all 78.2 0.0 9.1 1.3 0.0 0.8 0.0 0.0 0.0 10.6
Qué significa: La CPU está muy utilizada. Si se mantiene así, añadir conexiones normalmente aumentará la latencia, no el rendimiento.
Decisión: Si estás CPU-bound, prioriza la optimización de consultas y el límite de concurrencia (cap en pools) sobre subir max_connections.
Task 8: Revisa presión de disco y latencia
cr0x@server:~$ iostat -xz 1 3
avg-cpu: %user %nice %system %iowait %steal %idle
62.11 0.00 7.40 7.55 0.00 22.94
Device r/s rkB/s rrqm/s %rrqm r_await rareq-sz w/s wkB/s w_await aqu-sz %util
nvme0n1 920.0 48200.0 12.0 1.3 9.80 52.4 240.0 19320.0 14.20 6.12 96.3
Qué significa: El almacenamiento está cerca de la saturación (%util ~96%), con tiempos de espera notables. Las consultas se atascarán y las conexiones se acumularán.
Decisión: Si estás I/O-bound, aumenta buffer pool (si la memoria lo permite), reduce lecturas aleatorias mediante índices y evita aumentar la concurrencia hasta que la latencia mejore.
Task 9: Comprueba si hay swapping (el asesino silencioso)
cr0x@server:~$ free -h
total used free shared buff/cache available
Mem: 64Gi 58Gi 1.2Gi 612Mi 4.8Gi 2.1Gi
Swap: 8.0Gi 2.9Gi 5.1Gi
cr0x@server:~$ vmstat 1 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa
12 3 3024896 128992 98112 3621120 64 120 980 410 9200 18000 72 8 10 10
Qué significa: Estás haciendo swap (si/so), lo que disparará la latencia de consultas y causará acumulación de conexiones.
Decisión: Reduce el uso de memoria (a menudo bajando buffers por conexión y/o max_connections), y dimensiona el buffer pool inteligentemente. Swapping junto con “subir max_connections” es un auto-sabotaje.
Task 10: Verifica límites de descriptores de archivos del SO para MySQL (systemd importa)
cr0x@server:~$ sudo systemctl show mysql -p LimitNOFILE -p LimitNPROC
LimitNOFILE=1048576
LimitNPROC=15238
cr0x@server:~$ sudo cat /proc/$(pidof mysqld)/limits | egrep "Max open files|Max processes"
Max open files 1048576 1048576 files
Max processes 15238 15238 processes
Qué significa: Tu servicio tiene límites de FD altos. Si esto fuera bajo (como 1024/4096), podría enmascararse como un problema de conexiones o causar fallos extraños bajo carga.
Decisión: Si está bajo, crea un override de systemd para mysql y súbelo. Si ya está alto, no culpes a ulimit; sigue adelante.
Task 11: Revisa la presión del cache de tablas (puede inducir latencia y bloqueos)
cr0x@server:~$ sudo mysql -e "SHOW VARIABLES LIKE 'table_open_cache'; SHOW STATUS LIKE 'Opened_tables'; SHOW STATUS LIKE 'Open_tables';"
+------------------+--------+
| Variable_name | Value |
+------------------+--------+
| table_open_cache | 4000 |
+------------------+--------+
+---------------+--------+
| Variable_name | Value |
+---------------+--------+
| Opened_tables | 812349 |
+---------------+--------+
+-------------+------+
| Variable_name | Value |
+-------------+------+
| Open_tables | 3998 |
+-------------+------+
Qué significa: Opened_tables es enorme y sigue subiendo, mientras Open_tables está cerca del tamaño del cache. MySQL está abriendo tablas constantemente—sobrehead extra bajo carga.
Decisión: Aumenta table_open_cache si la memoria lo permite, y verifica que tienes suficientes descriptores de archivos. Esto es un problema de “pequeños cortes que se convierten en hemorragia”.
Task 12: Encuentra los patrones de consulta más nocivos que mantienen conexiones abiertas
cr0x@server:~$ sudo mysql -e "SELECT DIGEST_TEXT, COUNT_STAR, SUM_TIMER_WAIT/1000000000000 AS total_s, AVG_TIMER_WAIT/1000000000000 AS avg_s FROM performance_schema.events_statements_summary_by_digest ORDER BY SUM_TIMER_WAIT DESC LIMIT 5\G" | sed -n '1,80p'
*************************** 1. row ***************************
DIGEST_TEXT: SELECT * FROM orders WHERE user_id = ? ORDER BY created_at DESC LIMIT ?
COUNT_STAR: 1203912
total_s: 84231.1134
avg_s: 0.0699
Qué significa: Tienes mucho tiempo total atado a unos pocos patrones de sentencia. Aunque la latencia media sea “aceptable”, el tiempo total indica carga pesada y ocupación de conexiones.
Decisión: Optimiza las sentencias más calientes (índices, índices covering, reducir columnas seleccionadas, mejores patrones de paginación). Esto reduce tiempo por conexión y soluciona la causa raíz.
Task 13: Revisa la efectividad del thread cache (victoria barata para cargas con picos)
cr0x@server:~$ sudo mysql -e "SHOW VARIABLES LIKE 'thread_cache_size'; SHOW STATUS LIKE 'Threads_created'; SHOW STATUS LIKE 'Connections';"
+-------------------+-------+
| Variable_name | Value |
+-------------------+-------+
| thread_cache_size | 8 |
+-------------------+-------+
+-----------------+--------+
| Variable_name | Value |
+-----------------+--------+
| Threads_created | 238912 |
+-----------------+--------+
+---------------+---------+
| Variable_name | Value |
+---------------+---------+
| Connections | 9823812 |
+---------------+---------+
Qué significa: Cache baja y muchos hilos creados sugiere overhead por churn de hilos.
Decisión: Aumenta thread_cache_size hasta que Threads_created deje de subir rápido respecto a Connections. No lo pongas en 10.000 “por si acaso”. Cache suficiente para los picos.
Task 14: Confirma el tamaño del buffer pool y señales de aciertos
cr0x@server:~$ sudo mysql -e "SHOW VARIABLES LIKE 'innodb_buffer_pool_size'; SHOW STATUS LIKE 'Innodb_buffer_pool_reads'; SHOW STATUS LIKE 'Innodb_buffer_pool_read_requests';"
+-------------------------+------------+
| Variable_name | Value |
+-------------------------+------------+
| innodb_buffer_pool_size | 8589934592 |
+-------------------------+------------+
+-------------------------+-----------+
| Variable_name | Value |
+-------------------------+-----------+
| Innodb_buffer_pool_reads| 832912311 |
+-------------------------+-----------+
+----------------------------------+------------+
| Variable_name | Value |
+----------------------------------+------------+
| Innodb_buffer_pool_read_requests | 3412981123 |
+----------------------------------+------------+
Qué significa: Muchas lecturas físicas respecto a las solicitudes sugiere una caché que no está al día (señal cruda; no adores una sola proporción).
Decisión: Si tienes memoria libre y estás I/O-bound, incrementa el buffer pool. Si estás haciendo swap, haz lo contrario: reduce primero otros consumidores de memoria.
Soluciones que no ralentizan la BD
Aquí está la jerarquía práctica: arregla el comportamiento de conexiones de la aplicación, arregla el tiempo de las consultas, arregla el comportamiento de locks, y luego dimensiona límites de MySQL para que coincidan con la realidad. Si lo haces al revés, estás enmascarando el problema hasta que vuelva más fuerte.
1) Arregla el acaparamiento de conexiones: pooling, topes y timeouts
Haz: Usa un pool de conexiones por instancia de app, pon un tope duro y define timeouts sensatos.
- Tamaño del pool: empieza más pequeño de lo que crees. Si tienes 50 instancias de app y cada pool es 50, eso son 2500 conexiones potenciales antes de que la base de datos tenga algo que decir.
- Timeout de adquisición: fuerza fallos rápidos en la app en lugar de esperas infinitas. Esperar infinitamente solo mantiene más sockets abiertos.
- Timeout de inactividad: cierra conexiones inactivas para no gastar tu presupuesto de conexiones en sesiones durmientes.
Evita: “Una conexión por petición” sin pooling. Es una máquina de churn de hilos y convierte picos en tormentas.
En MySQL, dos perillas ayudan a reducir zombis durmientes:
wait_timeout(sesiones no interactivas)interactive_timeout(sesiones interactivas)
Guía con opinión: En entornos típicos de apps web, un wait_timeout de 60–300 segundos es razonable si el pool de la app está sano. Si tu app no lo tolera, la app es el bug, no la base de datos.
2) Desatascar acumulaciones de locks: mantiene transacciones cortas y programa DDL
Los errores de conexión a menudo siguen a atasques de locks. Las sesiones esperan; llegan nuevas sesiones; eventualmente alcanzas el techo. Arreglar atascos de locks es una de las pocas maneras de resolver “too many connections” sin añadir capacidad.
- Mantén transacciones cortas: nada de tiempo de reflexión del usuario dentro de una transacción, no bucles largos, no “leer 10k filas y luego decidir”.
- Disciplina con DDL: no ejecutes cambios de esquema en pico, especialmente si pueden tomar locks de metadatos por mucho tiempo.
- Chunking en jobs por lotes: comitea con frecuencia, usa batches acotados y retrocede cuando el sistema esté caliente.
3) Reduce el tiempo de consulta: porque el tiempo es lo que mantiene conexiones abiertas
El conteo de conexiones es básicamente “tasa de llegada × tiempo en el sistema”. No siempre puedes controlar la tasa de llegada. Puedes reducir el tiempo en el sistema.
- Indexa los predicados y ordenaciones reales usados por los digests calientes.
- Prefiere índices covering para endpoints de listas comunes para evitar lecturas extra de páginas.
- Deja de seleccionar columnas que no usas. Sí, incluso si “es solo JSON”. Es memoria, CPU y red.
- Mata la paginación con OFFSET a escala. Se vuelve más lenta conforme el offset crece. Usa paginación por keyset cuando sea posible.
4) Haz de max_connections una decisión calculada, no un deseo
Si has verificado que no estás bloqueado, no estás haciendo swap y no estás I/O-saturado, aumentar max_connections puede ser apropiado. Pero ponle números y salvaguardas.
Regla práctica (úsala con juicio):
- Memoria base: buffer pool de InnoDB + buffers globales de MySQL + overhead del SO.
- Memoria por conexión: puede variar mucho según tus settings y carga. La parte peligrosa es que muchos buffers por hilo se asignan bajo demanda; el peor caso es feo.
Enfoque práctico:
- Mide uso de memoria en picos actuales (RSS de mysqld).
- Estima memoria adicional por cada 50–100 conexiones extra a partir de deltas observados, no de suposiciones.
- Aumenta en pasos pequeños (p. ej., +20%); observa swap, latencia y locks.
5) Ajusta buffers por conexión de forma conservadora
Así evitas “subimos max_connections y la máquina empezó a swappear”. Muchos defaults de MySQL no son una locura, pero la gente copia/pega configs “de rendimiento” que ponen buffers grandes, olvidando que se multiplican por sesiones activas.
Settings a tratar con escepticismo en sistemas de alta concurrencia:
sort_buffer_sizejoin_buffer_sizeread_buffer_sizeread_rnd_buffer_sizetmp_table_size/max_heap_table_size(afecta tablas temporales en memoria)
Guía con opinión: Mantén estos relativamente pequeños hasta tener evidencia de que una carga particular se beneficia. La mayoría de cargas OLTP ganan más con índices y buffer pool que con buffers por hilo gigantescos.
6) Thread cache: reduce overhead, especialmente durante churn
Las cargas con picos crean churn de conexiones. El churn crea overhead de creación/destrucción de hilos. Un thread_cache_size decente ayuda a amortiguar picos sin desperdiciar CPU.
Qué hacer: Aumenta thread_cache_size hasta que Threads_created deje de crecer rápidamente comparado con Connections. No lo pongas a 10.000 “por si acaso”. Cachea lo suficiente para los picos.
7) Usa pooling en la capa correcta (app primero, proxy segundo)
El pooling en la app suele ser lo mejor porque preserva contexto de petición y backpressure. Pero hay casos donde necesitas un proxy de pooling dedicado:
- Clientes de corta vida (serverless, ráfagas de cron, jobs de CI).
- Apps legadas que abren demasiadas conexiones y no pueden arreglarse rápido.
- Entornos multi-tenant donde necesitas gobernanza estricta.
Cuidado: el pooling a nivel de transacción puede romper supuestos de sesión (tablas temporales, variables de sesión). Si tu app usa eso, necesitas pooling de sesión o cambios en el código.
8) Específico de Ubuntu 24.04: overrides de systemd y cómo hacerlos persistentes
En Ubuntu 24.04, la historia más común de “pusimos ulimit pero no funcionó” es systemd. La unidad de servicio tiene sus propios límites.
Para fijar un límite persistente de FD para MySQL:
cr0x@server:~$ sudo systemctl edit mysql
# (opens an editor)
cr0x@server:~$ sudo cat /etc/systemd/system/mysql.service.d/override.conf
[Service]
LimitNOFILE=1048576
cr0x@server:~$ sudo systemctl daemon-reload
cr0x@server:~$ sudo systemctl restart mysql
Qué significa: MySQL ahora heredará el límite de FD al iniciar el servicio.
Decisión: Haz esto solo si confirmaste que los límites de FD son parte del problema. Elevar límites no arregla consultas lentas; solo retrasa el fallo hasta más tarde.
9) Añade retropresión: falla rápido upstream en vez de derretir downstream
Si la BD es el recurso crítico compartido, tu app debe protegerla. Estrategias de retropresión que mantienen sistemas responsivos:
- Capar el tamaño del pool por instancia de app.
- Timeout corto de adquisición (p. ej., 100–500ms según SLO).
- Encolar peticiones en la app con colas acotadas; rechazar más allá de eso.
- Desactivar reintentos agresivos en fallos de conexión a BD; usar backoff exponencial con jitter.
10) Escala lo correcto: réplicas de lectura, shardear o simplemente una máquina más grande
Si hiciste la higiene y sigues golpeando techos de conexión porque la carga es real, escala de forma deliberada:
- Réplicas de lectura para cargas de solo lectura (mueve endpoints de reporting/listas, no escrituras críticas).
- Máquina más grande si estás limitado por memoria/I/O y puedes escalar verticalmente rápido.
- Sharding si el escalado de escrituras es el problema y el modelo de datos lo permite—esto no es un “arreglo de martes por la tarde”.
Errores comunes: síntoma → causa raíz → arreglo
1) Síntoma: muchas sesiones en “Sleep”, conexiones al máximo, pero la CPU no está alta
Causa raíz: pools de aplicación demasiado grandes, conexiones filtradas o wait_timeout demasiado largo manteniendo sesiones inactivas.
Arreglo: capar tamaños de pool, asegurar que las conexiones se devuelven, reducir wait_timeout a unos minutos y establecer timeouts de inactividad en el cliente.
2) Síntoma: muchas sesiones “Waiting for table metadata lock”
Causa raíz: cambio de esquema en curso/DDL online o transacción larga bloqueando locks de metadatos.
Arreglo: programa DDL fuera de pico, usa herramientas de cambio de esquema online apropiadas y evita transacciones largas que mantengan locks. Identifica al bloqueador vía performance_schema y actúa.
3) Síntoma: “too many connections” aparece junto con actividad de swap
Causa raíz: sobrecompromiso de memoria, a menudo debido a buffers por conexión y concurrencia excesiva.
Arreglo: reduce buffers por conexión, reduce topes de conexión, dimensiona correctamente el buffer pool y detén el swapping antes de subir max_connections.
4) Síntoma: las conexiones se disparan durante una caída, luego no se recuperan bien
Causa raíz: tormentas de reintentos, clientes reconectando agresivamente o checks de salud del balanceador abriendo sesiones.
Arreglo: backoff exponencial con jitter, circuit breakers, limitar reconexiones y asegurar que los health checks no se autentiquen repetidamente en MySQL.
5) Síntoma: “Aborted connection… error reading communication packets” crece en pico
Causa raíz: clientes timeout por lentitud del servidor, pérdida de paquetes o stack de red sobrecargado.
Arreglo: arregla la latencia subyacente (CPU/I/O/locks), valida la estabilidad de la red y alinea timeouts cliente/servidor para que los clientes no hagan churn innecesario.
6) Síntoma: subir max_connections “arregla” el error pero la latencia se duplica
Causa raíz: aumentaste contención y presión de memoria; no aumentaste el throughput.
Arreglo: revierte o reduce, implementa topes de pooling, optimiza las consultas principales y escala CPU/I/O si es necesario.
7) Síntoma: los errores persisten aunque max_connections parece alto
Causa raíz: estás golpeando límites del SO/servicio (FDs), o un proxy tiene su propio tope de conexiones.
Arreglo: revisa LimitNOFILE en systemd, verifica límites efectivos de mysqld vía /proc y audita capas intermedias en busca de topes.
Tres micro-historias corporativas desde el campo
Micro-historia 1: El incidente causado por una suposición equivocada
Tenían un modelo mental ordenado: “Ejecutamos 20 pods de app, así que 20 conexiones.” Limpio. Reconfortante. Incorrecto.
La configuración real incluía un despliegue de workers en background, un colector de métricas que hacía checks periódicos, una UI de administración y un job de migración que “se ejecuta a veces”. Cada componente tenía sus propios defaults de pool. Los pods web por sí solos estaban bien. Los workers, sin embargo, se escalaron durante eventos de backlog—justo cuando la base de datos ya estaba ocupada.
El primer incidente de “too many connections” ocurrió durante una campaña de marketing. Aumentaron max_connections, reiniciaron MySQL y vieron desaparecer el error. Por una hora. Luego la BD se volvió lenta, los timeouts aumentaron y la app reintentó más fuerte. Las conexiones subieron otra vez, ahora con más waits de locks y más swapping.
La lección posterior al incidente no fue “sube max_connections”. Fue “cuenta las conexiones de toda la flota, incluyendo lo que nadie piensa”. Lo arreglaron limitando cada pool, añadiendo timeout de adquisición y enseñando al sistema de workers a retroceder cuando la latencia de BD sube.
Micro-historia 2: La optimización que salió mal
Un equipo quería reducir latencia de consultas. Alguien aumentó join_buffer_size y sort_buffer_size agresivamente porque un blog decía que ayuda. Lo hizo—en una máquina de staging que ejecutaba una consulta a la vez.
En producción, la concurrencia era el asunto. En pico, cientos de sesiones estaban activas. Esos buffers más grandes no siempre se asignaban, pero cuando ciertos endpoints tocaban queries complejas, el uso de memoria subió rápido. El SO empezó a hacer swap bajo tráfico en ráfagas. Una vez que empezó el swap, las consultas se ralentizaron. Al ralentizarse las consultas, las conexiones se mantuvieron abiertas más tiempo. Al mantenerse abiertas, el servidor se quedó sin conexiones. ¿El síntoma original? “Too many connections”.
Parecía un problema de límite de conexiones, pero era una amplificación de memoria por conexión. La solución fue aburrida: revertir buffers por hilo sobredimensionados, añadir los índices correctos y aumentar el buffer pool dentro de memoria segura. Su latencia mejoró, no empeoró, y el límite de conexiones dejó de ser un drama diario.
Micro-historia 3: La práctica aburrida pero correcta que salvó el día
Otra organización tenía una costumbre no glamorosa: cada servicio tenía un “presupuesto DB” documentado. Tamaño máximo de pool por instancia. Número máximo de instancias permitido antes de una revisión de escalado. Timeout de adquisición y política de reintentos estándar.
Durante una prueba de failover regional, el tráfico se desplazó abruptamente. La carga se duplicó. La base de datos se calentó pero no colapsó. En vez de abrir conexiones infinitas, las apps pusieron un poco en cola, rechazaron peticiones en exceso rápidamente y se recuperaron cuando pasó el pico inicial.
Siguieron viendo CPU de BD elevada. Tuvieron que ajustar un par de índices. Pero no tuvieron el catastrófico outage de “too many connections” que suele provocar cambios caóticos a medianoche.
Lo mejor: el informe del incidente fue corto. Los sistemas aburridos son sistemas fiables, y los sistemas fiables son con los que puedes dormir.
Listas de verificación / plan paso a paso
Paso a paso: detener la hemorragia durante un incidente (15–30 minutos)
- Verifica qué está saturado: ejecuta Task 7 (CPU), Task 8 (I/O), Task 9 (swap).
- Identifica estados de conexión: Task 4 para ver Sleep vs Query vs Locked.
- Si predominan waits por locks: Task 5 y Task 6 para encontrar bloqueadores. Decide: esperar, matar o posponer DDL/batch.
- Si domina Sleep: reduce tamaño de pool en la app (palanca más rápida), luego considera bajar
wait_timeouttras verificar impacto. - Si estás I/O bound: deja de aumentar la concurrencia. Reduce carga (rate limit, desactiva endpoints caros), luego ajusta buffer pool/índices.
- Si hay swapping: reduce uso de memoria inmediatamente (baja concurrencia/pools, revierte buffers por hilo sobredimensionados si aplica). Swapping es señal de “deja todo”.
- Solo entonces, si tienes margen y lo necesitas, aumenta modestamente
max_connectionscomo mitigación temporal.
Paso a paso: arreglo permanente (un sprint, no un pánico)
- Inventaría todos los clientes: web, workers, cron, admin tools, métricas, ETL. Documenta el máximo esperado de conexiones por componente.
- Implementa pooling correctamente: capa pools, establece timeouts de adquisición y asegura que las conexiones se devuelven.
- Arregla las consultas calientes: usa performance_schema para digests (Task 12), luego añade/ajusta índices y reduce datos traídos.
- Arregla comportamiento de locks: acorta transacciones, divide jobs por lotes y programa DDL.
- Calibra límites de MySQL: fija
max_connectionsbasado en margen medido; ajustathread_cache_size,table_open_cache. - Valida límites OS/servicio: asegúrate que límites de systemd concuerdan con tu diseño (Task 10).
- Prueba carga con concurrencia: no solo RPS. Verifica latencia y tasas de error bajo patrones de picos.
- Pon alertas preventivas: alerta sobre Threads_connected en alza, lock waits, swap y conexiones abortadas en aumento.
Qué evitar (porque lo lamentarás)
- Poner
max_connectionsa un número enorme “para que nunca pase de nuevo.” Volverá a pasar, solo más lento y caro. - Copiar/pegar my.cnf “de alto rendimiento” con buffers por hilo gigantes.
- Ejecutar cambios de esquema en pico y luego sorprenderse por locks de metadatos.
- Permitir que los clientes reintenten instantáneamente e infinitamente. Eso no es resiliencia; es una función de denegación de servicio.
FAQ
1) ¿Simplemente debería aumentar max_connections?
Sólo si confirmaste que tienes margen de memoria (sin swapping), y el servidor no está ya saturado de CPU/I/O. Si no, cambiarás errores duros por un colapso lento.
2) ¿Por qué la base de datos se pone más lenta cuando permito más conexiones?
Más sesiones significan más contención, más cambios de contexto, más churn de buffers y potencialmente más I/O de disco. El throughput no escala linealmente con conexiones.
3) ¿Cuál es un valor bueno para wait_timeout?
Para apps web típicas con pooling, 60–300 segundos es un rango de inicio sensato. Si necesitas horas, tu app está usando MySQL como almacenamiento de sesión, y eso es otra conversación.
4) ¿Cómo sé si es fuga de conexiones en la aplicación?
Si Threads_connected sube de forma sostenida y la mayoría de sesiones están en Sleep con tiempos grandes, eso es fuga/hoarding clásico. Confírmalo correlacionando con el conteo de instancias de app y la configuración de pools.
5) ¿Y si veo muchas sesiones “Waiting for table metadata lock”?
Deja de ejecutar DDL en pico, encuentra el bloqueador (a menudo una transacción larga) y decide si matarlo. Luego pon DDL detrás de una ventana de cambios y un proceso.
6) ¿Las réplicas de lectura arreglan “too many connections”?
Pueden, si la presión viene de tráfico de lectura y realmente rediriges lecturas allí. No arreglan contención de escrituras, transacciones largas o pooling malo.
7) ¿Los límites de descriptores del SO pueden causar errores de conexión?
Sí. Si mysqld no puede abrir más sockets/archivos, verás fallos extraños bajo carga. En Ubuntu 24.04, revisa LimitNOFILE de systemd y límites efectivos de mysqld.
8) ¿Bajar buffers por conexión siempre es seguro?
Normalmente es más seguro que subirlos a ciegas. Buffers más bajos pueden aumentar uso de disco temporal o hacer más lentas ciertas consultas, así que valida contra la carga real. Pero buffers sobredimensionados bajo alta concurrencia son un gatillo común de incidentes.
9) ¿Cómo elijo entre arreglar consultas y añadir hardware?
Si estás saturado en CPU/I/O con queries conocidas malas, arregla las queries primero. Si ya estás optimizado y sigues saturado, añade hardware o escala horizontal. Mide antes de gastar.
10) ¿Qué métricas debo alertar para detectar esto temprano?
Threads_connected, Max_used_connections, indicadores de lock wait, tasa de conexiones abortadas, uso de swap, await/utilización de disco y latencia p95/p99 de consultas (no solo la media).
Próximos pasos que no dañarán más adelante
Si ves “too many connections” en Ubuntu 24.04, trátalo como un síntoma, no como un ajuste. La solución estable más rápida casi siempre es reducir la ocupación de conexiones: acorta consultas, acorta transacciones y evita que la app acapare sesiones.
Haz esto a continuación, en orden:
- Ejecuta la guía de diagnóstico rápido y captura evidencia (estados de proceso, CPU/I/O/swap).
- Arregla el mayor impulsor: hoards de sleeping o atascos de locks. No debatas; mide.
- Capar y afinar pools de aplicación. Añade timeouts de adquisición y backoff de reintentos sensato.
- Optimiza los digests de sentencias principales. Esto reduce tiempo en sistema y facilita todo lo demás.
- Luego—y solo entonces—ajusta
max_connectionsy caches de MySQL según margen observado.
Cuando lo arreglas bien, la base de datos se hace más rápida bajo carga, no más lenta. Ese es el punto. El error desaparece porque el sistema está más sano, no porque le enseñaste a tolerar más dolor.