← Casos
SaaS hospitalario (POS multi-sucursal) · 2024

Resolviendo un HikariPool exhaustion de 7 horas en producción

Anatomía de una caída: 23K errores, la cascada que ocultó la causa raíz, y la observabilidad que quedó como resultado.

Rol Solutions Architect & DevOps Lead
~7h 25min
Duración de la caída
23,500+
Errores registrados
137/h → 21/h (-85%)
Connection leak
15+
Alarmas agregadas
AWSECS / FargateRDS MySQLJava / Spring BootHikariCPCloudWatch

Contexto

Un SaaS POS multi-sucursal corriendo en ECS Fargate (Java / Spring Boot) con RDS MySQL. Tres servicios comparten la base de datos a través de pools HikariCP: gateway, auth, cash-register.

Una tarde, el servicio cash-register dejó de aceptar requests. El patrón en logs: HikariPool-1 - Connection is not available, request timed out. Para cuando identificamos la causa, 23K errores se habían acumulado y el servicio había estado degradado por ~7 horas.

La cascada que ocultó la causa raíz

La base de datos en sí operaba normal. Las métricas RDS de CloudWatch mostraban CPU en niveles normales, conexiones muy por debajo de max_connections, y query latency sin cambios.

La capa faltante eran las métricas a nivel de connection pool. La visibilidad disponible cubría el lado de la base de datos, no el estado interno del pool de cada servicio. El síntoma (timeouts) parecía relacionado con la base de datos, pero la causa real (leaks dentro de la aplicación) no era visible desde métricas RDS.

La causa, identificada leyendo thread dumps:

  • 55 servicios en el codebase usaban métodos transaccionales, varios con OSIV (Open Session In View) habilitado — el patrón de Spring que mantiene la Hibernate session abierta durante todo el request, incluyendo el render de vista.
  • Varios controllers llamaban APIs externas dentro de transacciones, reteniendo conexiones por segundos durante la llamada HTTP.
  • Bajo carga, el pool se drenaba más rápido de lo que las conexiones regresaban. Una vez agotado, cada nuevo request se encolaba en la wait queue hasta timeout.
lo que mostraban los dashboards (verde) lo que en realidad pasaba RDS MySQL · métricas CPU ok · max_conn lejos · latencia flat → "la base está bien" no había métrica de lo que pasaba dentro del pool de cada servicio ↓ gateway HikariCP pool: 300 auth HikariCP pool: 150 cash ⚠ leaked path del leak :: 55 servicios auditados @Transactional API externa conn retenida segundos durante llamada HTTP + OSIV manteniendo sesión abierta hasta render Pool agotado · timeouts "HikariPool-1 - Connection is not available" 7h 25min de caída hasta identificar el patrón 23,500+ errores apilados en wait queue 137/h → 21/h leak rate post-fix · 48h post-deploy

El fix

  1. OSIV deshabilitado a nivel aplicación — las sesiones viven solo dentro de fronteras @Transactional.
  2. 55 servicios auditados para scope transaccional; llamadas externas movidas fuera del bloque transaccional.
  3. Sizing de HikariCP rebalanceado entre los tres servicios basado en perfiles reales de concurrencia (gateway 300, auth 150, worker 50 — total 500 de 2,730 max, ~18% headroom de utilización).
  4. Connection leak detection activado en config Hikari (leakDetectionThreshold) para que futuros leaks loguen stack trace inmediatamente.

Resultado: connection leaks bajaron de 137/h a 21/h (-85%) en 48 horas post-deploy.

Mejoras de observabilidad

Para prevenir recurrencia y detectar patrones similares antes de que lleguen a producción, agregué:

  • Metric filters de CloudWatch sobre líneas de log matcheando HikariPool.*timed out, Connection is not available, y pool stats: — alarma configurada con rate > 5/min.
  • CloudWatch Insights queries guardadas como entradas de runbook nombradas (pool-saturation, connection-leaks, slow-transactions).
  • 15+ nuevas CloudWatch Alarms organizadas por categoría: memory pressure, connection saturation, task health, RDS replication lag — cada una con un runbook de una línea adjunto en la descripción de la alarma.
  • Container Insights habilitado en ECS Fargate para visibilidad de CPU, memoria y conexiones en tiempo real por task.

Cuando un patrón similar apareció en sandbox cinco días después durante el rollout de OSIV en otro servicio, la alarma se disparó en menos de 2 minutos y el problema se contuvo antes de tocar tráfico productivo.

Resultado

El incidente fue resuelto y los patrones que lo causaron se atendieron en las capas de aplicación y observabilidad. La visibilidad a nivel de pool existe donde no existía antes, con alarmas y runbooks en su lugar para las categorías de falla observadas durante el incidente.