MySQL vs PostgreSQL: «timeouts aleatorios» — red, DNS y pooling culpables

¿Te fue útil?

“Timeouts aleatorios” es lo que la gente dice cuando el canal de incidentes avanza más rápido que los gráficos. Una petición se queda colgada 30 segundos, otra termina en 12 ms, y la base de datos recibe la culpa porque es la única dependencia compartida que todos saben nombrar.

La mayoría de las veces, la base de datos es inocente. O al menos: culpable de una manera más interesante que “Postgres es lento” o “MySQL perdió conexiones”. Los timeouts viven en las grietas—entre la app y la BD—donde las colas de red, el caché DNS, los balanceadores y los pools de conexiones cambian las reglas en silencio.

Qué significan realmente los “aleatorios”

“Aleatorio” típicamente quiere decir correlacionado, pero no con lo que estás mirando ahora. La misma consulta puede expirar para un usuario y no para otro porque no es solo la consulta. Es:

  • Qué instancia de la app alcanzaste (distinto caché DNS, distinto pool, distinto kernel del nodo, distinta ocupación de la tabla conntrack).
  • Qué endpoint de BD resolviste (DNS obsoleto, split-horizon, incompatibilidad happy-eyeballs IPv6/IPv4).
  • Qué ruta tomó el paquete (cambios en hash ECMP, un enlace congestionado, una VM vecina ruidosa).
  • Qué conexión reutilizaste (el pool te dio una sesión TCP medio muerta; la BD ya te olvidó).
  • Qué bloqueo esperaste (una transacción en una fila bloqueada es indistinguible de un atasco de red a menos que lo instrumentes).

Los timeouts también son acumulativos. Una app puede tener un timeout de petición de 2s, el driver un timeout de conexión de 5s, un pooler un server_login_retry de 30s y un balanceador un idle timeout de 60s. Cuando alguien dice “expira alrededor de 30 segundos”, eso no es una pista. Es una confesión: un valor por defecto hizo esto.

MySQL vs PostgreSQL: cómo suelen verse los timeouts

Dónde suelen aterrizar los “timeouts aleatorios” en sistemas MySQL

En entornos MySQL, los “timeouts aleatorios” son comúnmente efectos secundarios del churn de conexiones y límites en manejo de hilos/conexiones:

  • Tormentas de conexiones después de despliegues (o tras reiniciar un pooler) pueden saturar MySQL con sobrecarga de autenticación y creación de hilos.
  • wait_timeout e interactive_timeout pueden matar conexiones inactivas que el pool creía vivas, produciendo errores esporádicos de “server has gone away” o “lost connection”.
  • Búsquedas DNS inversas (cuando la resolución de nombres interviene para grants o logging) pueden añadir latencia inesperada al conectar cuando el DNS está enfermo.
  • Capas proxy (proxies conscientes de SQL, balanceadores L4) pueden introducir idle timeouts que parecen “MySQL es inestable”.

Dónde suelen aterrizar los “timeouts aleatorios” en sistemas PostgreSQL

En Postgres, los timeouts con frecuencia aparecen como esperas—en bloqueos, en slots de conexión o en recursos del servidor:

  • Saturación de max_connections crea un patrón muy específico: algunos clientes conectan al instante, otros cuelgan, otros fallan—dependiendo del comportamiento del pool y del backoff.
  • Locks y transacciones largas causan que consultas “aleatoriamente” se queden colgadas detrás de un mal actor.
  • statement_timeout e idle_in_transaction_session_timeout pueden convertir un camino lento en un error, lo cual es bueno—hasta que está mal ajustado y se vuelve ruido.
  • PgBouncer en modo transacción/statement puede amplificar rarezas si tu aplicación depende del estado de sesión.

Ambas bases sufren la misma física: TCP no es magia, DNS es una caché distribuida de mentiras, y los poolers son maravillosos hasta que dejan de serlo. Las diferencias están en el comportamiento por defecto ante fallos y en lo que la gente tiende a añadir alrededor.

Idea parafraseada de Werner Vogels: “Todo falla, todo el tiempo.” No es pesimismo; es un requisito de diseño.

Broma #1: Los “timeouts aleatorios” son simplemente fallos deterministas que aún no conocieron tus dashboards.

Datos interesantes y un poco de historia (que realmente ayuda)

  1. PostgreSQL desciende de POSTGRES (años 80), diseñado con extensibilidad y corrección en mente; eso se nota en su rigidez respecto a transacciones y comportamiento de bloqueo.
  2. La popularidad temprana de MySQL (finales 90s/2000s) vino por ser rápido y fácil para cargas web; muchos defaults operativos y supuestos del ecosistema aún reflejan “muchas consultas cortas”.
  3. MySQL históricamente se apoyó en motores no transaccionales (como MyISAM) antes de que InnoDB se volviera la norma; la sabiduría operacional sobre “MySQL es simple” a menudo ignora cómo se comporta InnoDB bajo contención.
  4. Postgres introdujo MVCC temprano y se sostuvo en ello; “las consultas no bloquean escrituras” es mayormente cierto—hasta que aparecen bloqueos y DDL.
  5. PgBouncer se hizo común porque las conexiones de Postgres no son baratas; una flota grande haciendo TLS + auth por petición puede parecer un DDoS que pagaste por accidente.
  6. Los proxies de MySQL (y balanceadores) se hicieron comunes con el escalado de lectura; componentes L4/L7 pueden inyectar idle timeouts que imitan inestabilidad del servidor.
  7. Los TTL de DNS existen porque los resolvers son cachés; los clientes discuten sobre respetar TTL, y algunas librerías cachean más tiempo del que crees—especialmente en procesos de larga vida.
  8. Conntrack de Linux (netfilter) puede ser un cuello de botella en setups con mucho NAT; no es específico de bases de datos, pero el tráfico DB es lo suficientemente constante para revelarlo.
  9. Los balanceadores de nube a menudo tienen valores por defecto de idle timeout relativamente fijos; las bases son conversadoras pero a veces están quietas, así que “idle” puede seguir siendo “saludable”.

Guía rápida de diagnóstico

Este es el orden que uso cuando alguien dice “timeouts en BD” y no hay tiempo para debatir arquitectura.

Primero: clasifica el timeout

  • Timeout de conexión (no se puede establecer TCP/TLS/auth). Sospechosos: DNS, enrutamiento, firewall, salud del balanceador, max connections, backlog SYN.
  • Timeout de lectura (conectado, luego atascado). Sospechosos: bloqueos, transacciones largas, CPU/IO del servidor, pérdida de paquetes/retransmisiones, pool que te da un socket muerto.
  • Timeout de escritura (a menudo parece timeout de lectura pero al hacer commit). Sospechosos: fsync/estancamientos de IO, presión de replicación, commit síncrono, latencia del almacenamiento, red hacia el almacenamiento.
  • Timeout de aplicación (tu propio deadline expiró). Sospechosos: todo lo anterior más “pusiste 500ms y esperabas”.

Segundo: encuentra el alcance

  • ¿Un nodo de app? Revisa caché DNS, kernel, conntrack, estado local del pool, vecino ruidoso.
  • ¿Una AZ/subred? Revisa cambios de enrutamiento, grupos de seguridad, desajuste MTU, pérdida de paquetes, brownouts.
  • ¿Todos los clientes? Revisa saturación de BD, estancamientos de almacenamiento, o un proxy/balanceador compartido.
  • ¿Solo conexiones nuevas? Revisa auth/DNS, handshake TLS, agotamiento de pool, max connections, backlog SYN.

Tercero: decide si estás esperando o descartando

  • Esperando: el servidor muestra sesiones atascadas en locks/IO/CPU; la red muestra baja pérdida pero alta latencia; las consultas aparecen “activas” pero bloqueadas.
  • Descartando: resets TCP, broken pipes, “server closed the connection”, picos en retransmisiones/timeouts, presión en la tabla NAT.

Cuarto: reduce la superficie del problema

  • Prueba un cliente, una conexión, directo a la IP de la BD (evita DNS + pooler) para separar capas.
  • Prueba desde otro nodo/subred para separar local de sistémico.
  • Reduce la concurrencia temporalmente para detener tormentas de conexión autoinfligidas mientras investigas.

Tareas prácticas: comandos, salidas y decisiones

Estas son tareas reales que puedes ejecutar durante un incidente. Cada una incluye qué significa la salida y qué decidir después. Elige las que coincidan con tu entorno (bare metal, VM, Kubernetes).

1) Confirma la respuesta DNS, TTL y si está fluctuando

cr0x@server:~$ dig +noall +answer +ttlid db.internal.example A
db.internal.example. 5 IN A 10.20.30.41

Qué significa: TTL=5 segundos. Eso es agresivo. Si el endpoint cambia o el resolver está lento, lo notarás.

Decisión: Si el TTL es muy bajo, revisa la salud del resolver y el comportamiento de caché del cliente. Considera aumentar el TTL para servicios estables o usar un VIP/proxy estable.

2) Comprueba si tu nodo usa una ruta de resolver defectuosa

cr0x@server:~$ resolvectl status | sed -n '1,80p'
Global
       Protocols: -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
resolv.conf mode: stub
Current DNS Server: 10.0.0.2
       DNS Servers: 10.0.0.2 10.0.0.3

Qué significa: Este host usa systemd-resolved con dos servidores DNS. Si 10.0.0.2 está enfermo, puedes ver demoras intermitentes en búsquedas.

Decisión: Si ves picos de latencia en búsquedas, prueba cada resolver directamente; considera eliminar un resolver no saludable o arreglar el enrutamiento/seguridad hacia él.

3) Mide la latencia DNS directamente (no adivines)

cr0x@server:~$ for i in {1..5}; do time dig +tries=1 +timeout=1 @10.0.0.2 db.internal.example A >/dev/null; done
real    0m0.012s
real    0m0.980s
real    0m0.011s
real    0m1.003s
real    0m0.010s

Qué significa: La mitad de las consultas casi alcanza el timeout de 1s. Eso no está “bien”. Es ruleta.

Decisión: Trata el DNS como parte de tu camino crítico. Arregla la carga del resolver, pérdida de paquetes o la recursión upstream. Mientras tanto, aumenta el caché DNS del cliente solo si entiendes las implicaciones de failover.

4) Confirma la ruta y MTU hacia la BD

cr0x@server:~$ ip route get 10.20.30.41
10.20.30.41 via 10.20.0.1 dev eth0 src 10.20.10.55 uid 1000
    cache

Qué significa: Tienes un camino enrutado vía 10.20.0.1. Si esto cambia durante incidentes, eso es una pista.

Decisión: Si el enrutamiento es inestable, involucra al equipo de red temprano. Rutas inestables producen “timeouts aleatorios” que ningún ajuste de BD arreglará.

5) Prueba rápida de conexión TCP (evitando drivers)

cr0x@server:~$ nc -vz -w 2 10.20.30.41 5432
Connection to 10.20.30.41 5432 port [tcp/postgresql] succeeded!

Qué significa: El handshake TCP fue exitoso. Esto no prueba autenticación o éxito de consulta, pero descarta “puerto bloqueado” en ese momento.

Decisión: Si TCP falla intermitentemente, revisa reglas de seguridad, backlog SYN, firewalls stateful, agotamiento de conntrack/NAT, y salud de checadores de LB.

6) Inspecciona retransmisiones y salud TCP desde el cliente

cr0x@server:~$ ss -ti dst 10.20.30.41 | sed -n '1,40p'
ESTAB 0 0 10.20.10.55:49822 10.20.30.41:5432
	 cubic wscale:7,7 rto:204 rtt:2.3/0.7 ato:40 mss:1448 pmtu:1500 rcvmss:1448 advmss:1448 cwnd:10 bytes_sent:21984 bytes_retrans:2896 segs_out:322 segs_in:310 send 50.4Mbps lastsnd:12 lastrcv:12 lastack:12 pacing_rate 100Mbps unacked:2 retrans:1/7

Qué significa: Hay retransmisiones (bytes_retrans y contadores retrans). Un poco es normal; picos se correlacionan fuertemente con estancamientos “aleatorios”.

Decisión: Si las retransmisiones aumentan durante incidentes, deja de debatir planes de consulta y empieza a mirar pérdida de paquetes, congestión, problemas MTU/PMTU o NICs defectuosas.

7) Comprueba presión en conntrack (entornos con mucho NAT)

cr0x@server:~$ sudo sysctl net.netfilter.nf_conntrack_count net.netfilter.nf_conntrack_max
net.netfilter.nf_conntrack_count = 248901
net.netfilter.nf_conntrack_max = 262144

Qué significa: Estás cerca del límite de la tabla conntrack. Cuando se llena, obtienes drops de paquetes que parecen “timeouts intermitentes”.

Decisión: Aumenta límites de conntrack (con conciencia de memoria), reduce churn de conexiones, evita NAT innecesario y considera pooling en el borde.

8) Verifica el comportamiento de idle timeout del balanceador/proxy con una pausa controlada

cr0x@server:~$ (echo "ping"; sleep 75; echo "ping") | nc 10.20.30.50 3306
ping

Qué significa: Si el segundo “ping” nunca obtiene respuesta o la conexión cae después de ~60s, algún middlebox está aplicando un idle timeout.

Decisión: Alinea settings de keepalive: TCP keepalives del kernel, keepalives del driver y idle timeouts del LB/proxy. O elimina el LB del camino de la BD si es la herramienta equivocada.

9) PostgreSQL: comprueba si clientes están atascados esperando bloqueos

cr0x@server:~$ psql -h 10.20.30.41 -U ops -d appdb -c "select pid, wait_event_type, wait_event, state, now()-query_start as age, left(query,80) from pg_stat_activity where state <> 'idle' order by age desc limit 10;"
 pid  | wait_event_type |  wait_event   | state  |   age   |                                      left
------+-----------------+---------------+--------+---------+--------------------------------------------------------------------------------
 8123 | Lock            | transactionid | active | 00:00:31| update orders set status='paid' where id=$1
 7991 | Client          | ClientRead    | active | 00:00:09| select * from orders where id=$1

Qué significa: Una consulta está esperando un lock desde hace 31 segundos. Eso no es un timeout de red. Es una transacción bloqueante.

Decisión: Encuentra al bloqueador (siguiente tarea), luego decide si terminarlo, ajustar el alcance de las transacciones en la app, o añadir índices/re-escrituras para reducir la duración del lock.

10) PostgreSQL: encuentra la consulta bloqueadora

cr0x@server:~$ psql -h 10.20.30.41 -U ops -d appdb -c "select blocked.pid as blocked_pid, blocker.pid as blocker_pid, now()-blocker.query_start as blocker_age, left(blocker.query,80) as blocker_query from pg_locks blocked_locks join pg_stat_activity blocked on blocked.pid=blocked_locks.pid join pg_locks blocker_locks on blocker_locks.locktype=blocked_locks.locktype and blocker_locks.database is not distinct from blocked_locks.database and blocker_locks.relation is not distinct from blocked_locks.relation and blocker_locks.page is not distinct from blocked_locks.page and blocker_locks.tuple is not distinct from blocked_locks.tuple and blocker_locks.virtualxid is not distinct from blocked_locks.virtualxid and blocker_locks.transactionid is not distinct from blocked_locks.transactionid and blocker_locks.classid is not distinct from blocked_locks.classid and blocker_locks.objid is not distinct from blocked_locks.objid and blocker_locks.objsubid is not distinct from blocked_locks.objsubid and blocker_locks.pid != blocked_locks.pid join pg_stat_activity blocker on blocker.pid=blocker_locks.pid where not blocked_locks.granted and blocker_locks.granted;"
 blocked_pid | blocker_pid | blocker_age |                         blocker_query
-------------+-------------+-------------+---------------------------------------------------------------
        8123 |        7701 | 00:02:14    | begin; select * from orders where customer_id=$1 for update;

Qué significa: El PID 7701 ha mantenido locks por más de 2 minutos. Tus “timeouts aleatorios” son tu app esperando educadamente.

Decisión: Si es una transacción runaway, termínala. Si es normal, cambia el código: mantén transacciones cortas, evita FOR UPDATE salvo que lo necesites, y indexa el predicado.

11) PostgreSQL: detecta agotamiento de slots de conexión

cr0x@server:~$ psql -h 10.20.30.41 -U ops -d appdb -c "select count(*) as total, sum(case when state='active' then 1 else 0 end) as active from pg_stat_activity;"
 total | active
-------+--------
  498  |   112

Qué significa: 498 sesiones están presentes. Si max_connections es 500, estás en el filo.

Decisión: Pon PgBouncer delante (con cuidado), reduce tamaños de pool de la app y reserva conexiones para mantenimiento. “Solo aumenta max_connections” suele ser un plan de memoria disfrazado de optimismo.

12) MySQL: comprueba si alcanzas límites de conexión o presión de hilos

cr0x@server:~$ mysql -h 10.20.30.42 -u ops -p -e "SHOW GLOBAL STATUS LIKE 'Threads_connected'; SHOW GLOBAL STATUS LIKE 'Threads_running'; SHOW VARIABLES LIKE 'max_connections';"
+-------------------+-------+
| Variable_name     | Value |
+-------------------+-------+
| Threads_connected | 942   |
+-------------------+-------+
+-----------------+-------+
| Variable_name   | Value |
+-----------------+-------+
| Threads_running | 87    |
+-----------------+-------+
+-----------------+-------+
| Variable_name   | Value |
+-----------------+-------+
| max_connections | 1000  |
+-----------------+-------+

Qué significa: Estás cerca de max connections. Incluso si la CPU está bien, el servidor puede sufrir thrashing en manejo de conexiones y cambios de contexto.

Decisión: Reduce cuentas de conexión vía pooling, establece límites sensatos por servicio y evita que cada microservicio piense que merece 200 conexiones “por si acaso”.

13) MySQL: identifica causas raíz de “server has gone away” (timeouts vs tamaño de paquete)

cr0x@server:~$ mysql -h 10.20.30.42 -u ops -p -e "SHOW VARIABLES LIKE 'wait_timeout'; SHOW VARIABLES LIKE 'interactive_timeout'; SHOW VARIABLES LIKE 'max_allowed_packet';"
+--------------------+-------+
| Variable_name      | Value |
+--------------------+-------+
| wait_timeout       | 60    |
+--------------------+-------+
+--------------------+-------+
| Variable_name      | Value |
+--------------------+-------+
| interactive_timeout| 60    |
+--------------------+-------+
+--------------------+----------+
| Variable_name      | Value    |
+--------------------+----------+
| max_allowed_packet | 67108864 |
+--------------------+----------+

Qué significa: Las conexiones inactivas mueren después de 60 segundos. Eso está bien para clientes de corta vida, terrible para pools que mantienen conexiones mucho más tiempo.

Decisión: O: aumenta timeouts y habilita keepalives; o: acorta la vida idle del pool para que el pool descarte conexiones antes que el servidor lo haga.

14) Valida timers TCP keepalive del kernel (lado cliente)

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: El primer keepalive se envía después de 2 horas. Si tu load balancer mata sesiones inactivas tras 60 segundos, keepalive no te salvará.

Decisión: Ajusta keepalives (con cuidado) para clientes DB detrás de LBs/NATs, o deja de poner bases de datos detrás de dispositivos que matan conexiones inactivas.

15) Valida saturación del pool de la aplicación a nivel OS

cr0x@server:~$ ss -s
Total: 1632 (kernel 0)
TCP:   902 (estab 611, closed 221, orphaned 0, synrecv 0, timewait 221/0), ports 0

Transport Total     IP        IPv6
RAW	  0         0         0
UDP	  12        10        2
TCP	  681       643       38
INET	  693       653       40
FRAG	  0         0         0

Qué significa: 611 conexiones TCP establecidas. Si tu app “debería” tener 50, tienes una fuga de pool o una tormenta de conexiones.

Decisión: Limita el pool, añade backpressure y arregla la fuga. Escalar la BD para que coincida con un pool defectuoso es cómo se gastan presupuestos.

16) Vista rápida del servidor: ¿estás ligado por CPU o IO?

cr0x@server:~$ iostat -x 1 3
avg-cpu:  %user   %nice %system %iowait  %steal   %idle
          18.21    0.00    6.10   24.77    0.00   50.92

Device            r/s     rkB/s   rrqm/s  %rrqm r_await rareq-sz     w/s     wkB/s   w_await aqu-sz  %util
nvme0n1         120.0   40960.0     0.0   0.00    8.10   341.3   220.0   53248.0   21.40   4.90   96.80

Qué significa: El disco está ~97% utilizado; write await es 21 ms. Esa es latencia de commit esperando el almacenamiento, no un “bug del driver”.

Decisión: Si IO está al máximo, revisa checkpoints, comportamiento de fsync, settings de replicación, throttling del almacenamiento y vecinos ruidosos.

DNS: el saboteador silencioso

El DNS rara vez es la causa raíz de timeouts en bases de datos. Suele ser el amplificador: un pequeño problema de DNS convierte la lógica rutinaria de reconnect en una falla sincronizada.

Cómo el DNS causa “timeouts” aleatorios en BD

  • Búsquedas lentas durante la creación de conexiones: cada reconnect espera al DNS, así que tu pool de conexiones se convierte en un benchmark de DNS.
  • Respuestas obsoletas: los clientes siguen usando una IP antigua tras un failover; la BD está bien en la nueva IP, y tú machacas la antigua como si te debiera dinero.
  • Caché negativo: un NXDOMAIN o SERVFAIL transitorio se cachea, y ahora “aleatoriamente” no puedes resolver la BD por un rato.
  • Desajustes split-horizon: DNS interno/externo dan respuestas distintas; la mitad de tus pods resuelven a direcciones inalcanzables.
  • Quirks de Happy Eyeballs: respuestas AAAA IPv6 llevan a intentos sobre una ruta v6 rota antes de que v4 funcione.

MySQL vs Postgres: sorpresas relacionadas con DNS

Ambos se ven afectados en tiempo de conexión porque el cliente resuelve el host antes de establecer TCP. Las diferencias sorpresa suelen venir de lo que hay alrededor:

  • Despliegues MySQL suelen incluir proxies conscientes de SQL o balanceadores L4 para separación de lectura/escritura. Eso añade otro hostname, otra resolución y otra capa de caché.
  • Despliegues Postgres suelen incluir PgBouncer. PgBouncer resuelve a su vez nombres upstream y tiene su propio comportamiento sobre reconnects y reintentos.

Operativamente: no dejes que cada instancia de app haga su propia gimnasia DNS durante un incidente. Pon un endpoint estable delante de la BD (VIP, proxy o endpoint gestionado) y prueba intencionalmente el modo de fallo.

Realidades de red: retransmisiones, MTU y “no se cae, se encola”

Las redes no suelen fallar apagándose por completo. Fallan volviéndose lo bastante poco fiables como para que tus timeouts sean estadísticamente interesantes.

Retransmisiones: el impuesto oculto

Una sola retransmisión puede costar decenas o cientos de milisegundos dependiendo de RTO y control de congestión. Unos pocos por ciento de pérdida de paquetes pueden convertir un protocolo conversacional de base de datos en una máquina de timeouts, especialmente con TLS encima.

MTU y PMTUD: el clásico “funciona hasta que no”

El desajuste de MTU puede manifestarse como:

  • Consultas pequeñas funcionan, respuestas grandes se quedan estancadas.
  • La conexión se establece, el primer resultado grande se cuelga.
  • Algunas rutas funcionan (misma rack), otras fallan (cross-AZ).

Si ICMP “fragmentation needed” está bloqueado en algún punto, Path MTU Discovery se rompe y obtienes comportamiento de agujero negro. Parece un timeout de BD porque la BD es lo primero que envía paquetes grandes con consistencia.

Colas y bufferbloat

No toda latencia es pérdida. Si tienes enlaces congestionados, tus paquetes pueden estar esperando en una cola. TCP eventualmente los entregará, pero tu aplicación expirará antes. Por eso “sin pérdida de paquetes” no significa “la red está bien”.

Broma #2: La red es como la impresora de la oficina: funciona perfectamente hasta que alguien la mira.

Culpables del pooling: PgBouncer, proxies y valores por defecto “útiles”

El pooling existe porque crear una conexión a BD es caro—CPU, memoria, handshakes TLS, autenticación, bookkeeping del servidor. Pero el pooling también es donde la realidad se abstrae en algo que tus desarrolladores malentienden.

Los tres tipos de pool (y por qué importan)

  • Pools en cliente (en la app): lo más simple, pero puede multiplicar conexiones por número de instancias. Geniales para crear tormentas de conexiones.
  • Poolers externos (PgBouncer, ProxySQL): pueden limitar conexiones al servidor y absorber churn del cliente. También añaden un salto y otra superficie de configuración de timeouts.
  • Proxies gestionados (proxies DB en la nube): convenientes, pero su comportamiento durante failover y sus timeouts idle por defecto suelen ser donde nacen los “aleatorios”.

Modos de fallo del pooler que parecen timeouts de BD

  • Saturación del pool: las peticiones se encolan esperando una conexión; la app expira; la BD está aburrida.
  • Conexiones muertas reutilizadas: el pool entrega un socket que un middlebox mató; la primera consulta se queda o falla.
  • Tormentas de reintentos: el pooler reintenta agresivamente; añade carga cuando la BD ya está descontenta.
  • Suposiciones de estado de sesión: el pooling por transacción rompe apps que dependen de variables de sesión, tablas temporales, locks de advisory, prepared statements o SET LOCAL (según BD y modo).

MySQL vs Postgres: trampas del pooling

Postgres es especialmente sensible al conteo de conexiones porque cada conexión mapea a un proceso backend (en la arquitectura clásica). Por eso PgBouncer es tan común. Pero los modos de PgBouncer importan:

  • Pooling por sesión: lo más seguro; menos sorpresas; menos efectivo reduciendo conexiones servidor bajo ráfagas.
  • Pooling por transacción: efectivo; rompe todo lo que necesita afinidad de sesión a menos que esté diseñado con cuidado.
  • Pooling por statement: cuchillo afilado; raramente vale la pena para apps generales.

MySQL típicamente maneja muchas conexiones de manera diferente (thread-per-connection a menos que haya thread pool, dependiendo de distro/edición). El conteo de conexiones sigue siendo un problema, pero la firma del fallo suele parecer sobrecarga de CPU y churn del scheduler más que “sin slots” inmediatos.

Modos de fallo específicos de MySQL

Desajuste de idle timeout (wait_timeout vs vida del pool)

Una de las fallas “aleatorias” más comunes: el servidor expira conexiones inactivas, el pool las mantiene, y tu siguiente consulta descubre el cadáver.

Qué hacer: Haz que la vida máxima del pool y el idle timeout sean más cortos que wait_timeout, o aumenta wait_timeout y habilita TCP keepalive. Elige uno; no lo dejes derivar.

Lentitud en autenticación y búsqueda DNS inversa

Si MySQL está configurado en formas que requieren resolución de hostname (o el entorno hace búsquedas inversas para logging/control de acceso), un tropiezo de DNS se vuelve un estancamiento en tiempo de conexión. No es que MySQL “no pueda manejarlo”; es que tu camino de auth es ahora un sistema distribuido.

Demasiadas conexiones: “funciona hasta el deploy”

MySQL puede parecer estable a 500 conexiones y luego desmoronarse a 900—no porque las consultas cambiasen, sino por la sobrecarga. Hilos, memoria por conexión, contención de mutex y churn de caché aparecen como timeouts en la app.

Opinión fuerte: Si tienes cientos de instancias aplicativas cada una permitiendo abrir docenas de conexiones, no tienes un problema de base de datos. Tienes un problema de coordinación.

Modos de fallo específicos de PostgreSQL

Bloqueos y transacciones largas

Postgres es excelente en concurrencia—hasta que mantienes locks más tiempo del que crees. El culpable usual es una transacción que comienza temprano, hace algo de “lógica de negocio” y luego actualiza filas al final. Bajo carga, esos locks se acumulan y todo “aleatoriamente” espera.

Qué hacer: Mantén las transacciones cortas, reduce el alcance de los locks, indexa las filas exactas que tocas y usa timeouts que fallen lo bastante rápido para proteger el sistema.

Agotamiento de slots de conexión y el mito de “solo aumenta max_connections”

Cada conexión de Postgres consume memoria y overhead de gestión. Aumentar max_connections sin replantear work_mem, paralelismo y RAM total es cómo conviertes un incidente de timeouts en un incidente OOM.

Qué hacer: Pon PgBouncer en modo sesión o transacción, y luego dimensiona correctamente los pools de la app. Reserva margen para conexiones administrativas.

Configuraciones de timeout que faltan o se usan como arma

Postgres te da buenos controles: statement_timeout, lock_timeout, idle_in_transaction_session_timeout. La trampa es fijarlos globalmente a valores que encajen con la expectativa de un único endpoint.

Qué hacer: Establece timeouts por rol o por servicio. Diferentes cargas merecen diferentes modos de fallo.

Tres micro-historias corporativas desde el campo

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

La compañía tenía un servicio de checkout activo y un Postgres primario con un hot standby. Durante una prueba de failover planificada, movieron el rol primario. El nombre del endpoint DB debía seguir al primario, TTL bajo, todo “moderno”.

Tras el failover, un tercio de las peticiones empezaron a expirar. No errores—timeouts. Los gráficos de la BD se veían bien. CPU normal. Disco bien. Replicación al día. Todos miraron a Postgres, porque eso es lo que se hace cuando tienes miedo.

La suposición equivocada fue simple: “Nuestras apps Java respetan el TTL de DNS.” No lo hacían—no de forma consistente. Algunas instancias cacheaban la IP resuelta mucho más tiempo debido al caching a nivel JVM combinado con la forma en que la librería cliente resolvía nombres. Esas instancias seguían intentando la IP del antiguo primario, que ahora rechazaba escrituras o directamente no era alcanzable por reglas de seguridad.

La solución no fue heroica. Pusieron el endpoint DB estable detrás de un proxy que no cambiaba IP en failover, y configuraron explícitamente el caché DNS del JVM para respetar TTL en ese entorno. También añadieron un runbook: en failover, medir resolución DNS y la IP destino real desde una muestra de pods. No más redes basadas en fe.

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

Un equipo que corría MySQL para una API multi-tenant quiso reducir overhead de conexiones. Subieron los tamaños de pool mucho “para reutilizar conexiones” y redujeron timeouts “para fallar rápido”. El resultado se veía bien en staging: menos connects por segundo, menor latencia mediana.

En producción, una ráfaga semanal de tráfico golpeó. El servicio escaló rápido. Cada nueva instancia vino con un pool grande y todas intentaron calentarlo a la vez. MySQL aceptó conexiones hasta que dejó de hacerlo. Los hilos explotaron. CPU subió no por consultas, sino por manejar conexiones y cambio de contexto. Luego vinieron los reintentos.

La política de reintentos del cliente, combinada con un timeout de conexión corto, creó una tormenta de reintentos. Conexiones que habrían tenido éxito en 300 ms ahora fallaban a 100 ms y se reintentaban inmediatamente. La “optimización” volvió el sistema menos paciente justo cuando necesitaba paciencia.

Revirtieron el aumento del pool y lo reemplazaron por presupuestos globales estrictos de conexiones por servicio y una estrategia de arranque lento en startup. También hicieron los reintentos exponenciales con jitter y añadieron circuit breakers alrededor de la conectividad DB. La latencia mediana aumentó ligeramente. Los incidentes disminuyeron drásticamente. Los sistemas de producción prefieren aburrido sobre ingenioso.

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

Otra organización ejecutaba Postgres con PgBouncer en modo sesión. Nada fancy. Su equipo SRE tenía un hábito que parecía soso: cada trimestre, hacían drills de fallo controlados—matar un nodo app, reiniciar PgBouncer, simular lentitud DNS, introducir pequeña pérdida de paquetes en un entorno de prueba que replicaba la topología de producción.

Una tarde, una degradación de red en la nube golpeó su región. La pérdida de paquetes fue baja pero no nula; la latencia subió intermitentemente. Los servicios empezaron a reportar “timeouts de base de datos”. El on-call siguió el playbook del drill: primero comprobó retransmisiones desde un nodo cliente, luego la profundidad de cola de PgBouncer, luego los waits de Postgres. En diez minutos supieron que era degradación de red más amplificación por reintentos, no una regresión de la base de datos.

Porque lo habían ensayado, tenían una mitigación segura lista: reducir concurrencia bajando dinámicamente tamaños de pool de la app, extender ligeramente ciertos timeouts cliente para evitar tormentas de reintentos, y desactivar temporalmente un job en background que producía conjuntos de resultados grandes. Postgres se mantuvo sano. El incidente fue un bache en vez de una maratón de pager.

No fue glamuroso. Fue el equivalente operacional de usar hilo dental. Poco cool, hasta que te ahorra miles en cuidados dentales.

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

1) Timeouts se agrupan exactamente alrededor de 60 segundos

Síntoma: fallos a ~60s, sin importar la complejidad de la consulta.

Causa raíz: idle timeout de load balancer / proxy, o timeout del servidor matando conexiones pooled.

Solución: alinea idle timeouts y keepalives; establece vida máxima del pool menor que el idle timeout de BD; evita LBs en el camino DB a menos que conozcas su comportamiento.

2) Solo las conexiones nuevas expiran; las existentes funcionan

Síntoma: tráfico establecido bien, pero reconnects fallan durante el incidente.

Causa raíz: lentitud DNS, cuello en handshake TLS, presión en backlog SYN, o cuello en auth.

Solución: mide latencia DNS; revisa backlog SYN y drops de firewall; limita tasa de creación de conexiones; usa poolers/proxies para reducir churn de handshakes.

3) Consultas “aleatoriamente” se cuelgan, luego se reanudan

Síntoma: una consulta pausa segundos, luego completa; CPU e IO se ven normales.

Causa raíz: esperas por locks (especialmente en Postgres), o retransmisiones/encolamiento de red.

Solución: revisa eventos de espera por locks; encuentra bloqueadores; reduce alcance de transacciones; revisa retransmisiones cliente y latencia de red.

4) Un nodo de app está maldito

Síntoma: timeouts solo desde un host/pod/nodo específico.

Causa raíz: problemas de caché DNS local, agotamiento de conntrack, errores de NIC, pool mal dimensionado, o entrada de tabla de rutas errónea.

Solución: compara comportamiento de resolvers y retransmisiones entre nodos; drena y reemplaza el nodo; arregla drift de configuración; establece reglas de cuarentena de nodos basadas en SLO.

5) “Server has gone away” / “broken pipe” después de periodos inactivos

Síntoma: la primera consulta tras inactividad falla; el reintento tiene éxito.

Causa raíz: idle timeout del servidor, idle timeout del LB, timeout de NAT matando estado.

Solución: reduce la vida idle del pool; habilita keepalive; ajusta timeouts DB; evita middleboxes stateful entre app y BD.

6) Aumentaste max connections y ahora los timeouts empeoraron

Síntoma: menos errores de “demasiadas conexiones”, más latencia/timeouts.

Causa raíz: contención de recursos por demasiados backends/hilos concurrentes; presión de memoria; overhead de cambio de contexto.

Solución: añade pooling; implementa presupuestos de conexión; reduce concurrencia; escala vertical solo después de controlar comportamiento de conexiones.

7) Timeouts pican después de despliegues o reinicios

Síntoma: ventanas cortas de incidente justo después del rollout.

Causa raíz: tormentas de warmup de conexiones, reintentos sincronizados, cold starts de caché, estampida DNS.

Solución: creación de conexiones en slow-start; añade jitter; despliegues escalonados; precalienta con cuidado; limita reintentos.

8) Lecturas expiran solo para respuestas “grandes”

Síntoma: consultas pequeñas bien, conjuntos de resultados grandes se cuelgan.

Causa raíz: agujeros MTU/PMTUD, bufferbloat, o un proxy que maneja mal cargas grandes.

Solución: valida MTU end-to-end; permite ICMP necesario; prueba con tamaños de paquete controlados; evita proxies innecesarios en el camino.

Listas de verificación / plan paso a paso

Checklist A: Cuando empiezan los timeouts (primeros 10 minutos)

  1. Clasifica el timeout: conexión vs lectura vs deadline de app. Extrae una muestra de errores con timestamps.
  2. Revisa latencia DNS desde al menos dos nodos cliente (tarea 3). Si es inestable, para y arregla DNS antes de hacer algo complicado.
  3. Revisa retransmisiones desde un nodo cliente (tarea 6). La pérdida explica lo “aleatorio”.
  4. Revisa saturación del pool: métricas de app o conexiones OS (tarea 15). Si hay encolamiento en el pool, la tunning de BD no ayudará.
  5. Revisa esperas en BD:
    • Postgres: eventos de espera en pg_stat_activity (tarea 9).
    • MySQL: threads connected/running (tarea 12) y métricas de consultas lentas / locks si están disponibles.
  6. Aplica un throttle seguro: reduce concurrencia y tasas de reintento; detén el job en background más ruidoso. Estabiliza primero, optimiza después.

Checklist B: Prevenir incidentes repetidos (el siguiente día hábil)

  1. Estandariza timeouts entre app, driver, pooler y dispositivos de red. Documenta los valores elegidos y por qué.
  2. Define presupuestos de conexión por servicio. Hazlos cumplir en configuración, no en memoria tribal.
  3. Implementa backoff con jitter para reintentos; añade circuit breakers en fallos de conexión.
  4. Introduce un endpoint DB estable (proxy/VIP/endpoint gestionado) para que los cambios DNS no provoquen caos cliente-lado.
  5. Añade reglas de higiene sobre locks/transacciones (especialmente para Postgres): alcance de transacción, timeouts de statement por rol, alertas en transacciones largas.
  6. Realiza drills de fallo: simula lentitud DNS y pérdida modest de paquetes; verifica que tu sistema degrade de forma predecible.

Checklist C: Elegir MySQL vs Postgres para predictibilidad operativa

  • Si tu organización no puede hacer cumplir disciplina de conexiones, planea un pooler/proxy desde el día uno, sea cual sea la BD.
  • Si tu carga es sensible a locks y la lógica tiende a mantener transacciones abiertas, Postgres te mostrará la verdad—dolorosa pero arreglable.
  • Si tu patrón es de churn de conexiones (serverless, flotas con ráfagas), la arquitectura importa más que el motor. Pon una capa de pooling estable adelante y mide DNS/red como dependencia de primera clase.

Preguntas frecuentes

1) ¿Por qué los timeouts se sienten aleatorios incluso cuando la causa raíz es determinista?

Porque la distribución oculta patrones. Diferentes clientes toman rutas distintas, usan cachés DNS distintos, alcanzan distintos estados de pool y compiten por locks distintos. “Aleatorio” suele ser “shardeado”.

2) ¿Cómo sé si el timeout está en el pool y no en la base de datos?

Mira tiempo en cola en las métricas del pool, o infiere: si CPU/IO de BD está bajo pero latencias de app suben y cuentas de conexión están altas, el pool probablemente está saturado o en churn. Los contadores OS con ss también ayudan.

3) ¿Debo poner un load balancer delante de MySQL o Postgres?

Sólo si sabes exactamente qué hace con conexiones TCP de larga duración y con timeouts idle, y has probado comportamiento de failover. Los LBs son geniales para HTTP; las bases de datos no son HTTP.

4) ¿DNS TTL=5 segundos es bueno para failover?

Pue-de ser, pero sólo si tus clientes respetan realmente el TTL y tus resolvers son rápidos y fiables bajo carga. TTL bajo sin corrección en clientes convierte el failover en lotería.

5) Para Postgres, ¿PgBouncer es siempre la respuesta?

A menudo, sí—pero el modo importa. El pooling por sesión es lo menos sorprendente. El pooling por transacción es potente pero requiere disciplina de la app alrededor del estado de sesión y prepared statements.

6) Para MySQL, ¿“server has gone away” es siempre un problema de red?

No. Puede ser timeouts inactivos (wait_timeout), límites de tamaño de paquete (max_allowed_packet), reinicios de servidor, o middleboxes cerrando sesiones inactivas. Empieza correlacionando errores con periodos de inactividad y reuso de conexiones.

7) ¿Por qué los reintentos empeoran las cosas?

Los reintentos convierten latencia en carga. Si el cuello de botella es compartido (BD, DNS, proxy, conntrack), los reintentos sincronizan clientes y amplifican la falla. Usa backoff exponencial con jitter y un presupuesto de reintentos.

8) ¿Cuál es la ganancia más rápida para reducir “timeouts aleatorios”?

Controla el comportamiento de conexiones. Limita pools, detén tormentas de conexiones y alinea timeouts entre capas. Luego mide latencia DNS y retransmisiones TCP para dejar de depurar fantasmas.

9) ¿Los waits por locks en Postgres son un problema de BD o de app?

Normalmente es comportamiento de la aplicación expresado a través de la BD. Postgres sólo es honesto al esperar. Arregla alcance de transacciones, índices y patrones de acceso; luego considera niveles de aislamiento y timeouts.

10) ¿Cómo evito el ping-pong de culpas entre equipos de app, BD y red?

Recoge tres artefactos temprano: muestras de latencia DNS, evidencia de retransmisiones TCP y snapshots de waits/locks de BD. Con eso, la discusión se convierte en un plan.

Siguientes pasos (los prácticos)

Si quieres menos “timeouts aleatorios”, deja de tratarlos como rasgos de personalidad de la BD. Trátalos como interfaces que fallan bajo presión: DNS, TCP, pooling y control de concurrencia.

  1. Adopta la guía rápida de diagnóstico y conviértela en memoria muscular.
  2. Elige una estrategia estable de endpoint DB y prueba failover con clientes reales, no solo un cambio DNS teórico.
  3. Establece presupuestos explícitos de conexión y límites de pool por servicio. Hazlos cumplir.
  4. Alinea timeouts entre app, driver, pooler, keepalive del kernel y cualquier middlebox. Escríbelo.
  5. Instrumenta las esperas: waits por locks en Postgres, presión de hilos en MySQL y retransmisiones de red.

Haz eso, y la próxima vez que alguien diga “timeouts aleatorios”, tendrás el incómodo lujo de responder: “Genial. ¿Qué capa?”

← Anterior
Fallo de segmentación en producción: por qué un solo fallo puede arruinar un trimestre
Siguiente →
Escaneo de vulnerabilidades en Docker: qué confiar y qué es ruido

Deja un comentario