Monitoreo de HikariCP: Detección y Resolución de Fugas de Conexión

Habilitar el "caja negra" de Hikari (Reqeurido)

1. Configuración (application.yml)

spring:
  datasource:
    hikari:
      leak-detection-threshold: 5000   # Si no se devuelve en 5 segundos, se registra la pila de llamadas (mínimo 2000)
      register-mbeans: true            # Exponer JMX para ver el tiempo de retención

  • leak-detection-threshold: Si una conexión prestada no se devuelve después de N milisegundos, se imprime automáticamente la pila de llamadas completa al obtener la conexión, el nombre del hilo y el tiempo de retención.
  • Recomendación para producción: 5000~30000ms (un valor demasiado corto puede generar falsos positivos).

2. Ejemplo de registro

WARN com.zaxxer.hikari.pool.ProxyLeakTask
Detección de fuga de conexión activada para la conexión com.mysql.cj.jdbc.ConnectionImpl@6d8a00e7,
en el hilo http-nio-8080-exec-8, pila de llamadas a continuación:
java.lang.Exception: Se detectó una posible fuga de conexión
  at com.zaxxer.hikari.pool.ProxyConnection.<init>(ProxyConnection.java:152)
  at com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:181)
  at com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:147)
  at com.zaxxer.hikari.HikariDataSource.getConnection(HikariDataSource.java:100)
  at com.example.service.UserService.queryUser(UserService.java:42)  // 👈 Método + número de línea
  at com.example.controller.UserController.getUser(UserController.java:25)

Se obtienen los tres elementos clave:

  • Hilo: http-nio-8080-exec-8
  • Método: UserService.queryUser(línea 42)
  • Tiempo de retención: El registro indica que ha superado los 5 segundos.

Visualización en tiempo real de quién está reteniendo las conexiones (JMX + Código)

Hikari registra el tiempo de adquisición, la duración de retención y el hilo propietario para cada conexión, lo cual se puede acceder a través de HikariPoolMXBean.

1. Código para obtener y mostrar todas las conexiones activas

import com.zaxxer.hikari.HikariDataSource;
import com.zaxxer.hikari.HikariPoolMXBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.sql.DataSource;
import java.lang.management.ManagementFactory;
import java.util.concurrent.TimeUnit;

@RestController
public class HikariHolderController {

    @Autowired
    private DataSource dataSource;

    @GetMapping("/hikari/holders")
    public String listHolders() {
        HikariDataSource hikariDs = (HikariDataSource) dataSource;
        HikariPoolMXBean poolBean = hikariDs.getHikariPoolMXBean();

        String pid = ManagementFactory.getRuntimeMXBean().getName().split("@")[0];

        StringBuilder sb = new StringBuilder();
        sb.append("=== Conexiones activas de Hikari ===\n");
        sb.append("PID: ").append(pid).append("\n");
        sb.append("Conexiones activas: ").append(poolBean.getActiveConnections()).append("\n\n");

        try {
            Object pool = hikariDs.getPool();
            java.lang.reflect.Field activeField = pool.getClass().getDeclaredField("activeConnections");
            activeField.setAccessible(true);
            java.util.Set<?> activeConnections = (java.util.Set<?>) activeField.get(pool);

            for (Object conn : activeConnections) {
                java.lang.reflect.Field openTimeField = conn.getClass().getDeclaredField("openTime");
                java.lang.reflect.Field owningThreadField = conn.getClass().getDeclaredField("owningThread");
                java.lang.reflect.Field openStackTraceField = conn.getClass().getDeclaredField("openStackTrace");

                openTimeField.setAccessible(true);
                owningThreadField.setAccessible(true);
                openStackTraceField.setAccessible(true);

                long openTime = (Long) openTimeField.get(conn);
                Thread owningThread = (Thread) owningThreadField.get(conn);
                Exception stack = (Exception) openStackTraceField.get(conn);

                long holdMs = System.currentTimeMillis() - openTime;

                sb.append("----------------------------------------\n");
                sb.append("Hilo: ").append(owningThread.getName()).append("\n");
                sb.append("Tiempo de retención: ").append(holdMs).append("ms (").append(TimeUnit.SECONDS.convert(holdMs, TimeUnit.MILLISECONDS)).append("s)\n");
                sb.append("Pila de llamadas:\n");
                for (StackTraceElement element : stack.getStackTrace()) {
                    sb.append("  at ").append(element).append("\n");
                }
            }
        } catch (Exception e) {
            sb.append("Error al obtener detalles de la conexión: ").append(e.getMessage());
        }

        return sb.toString();
    }
}

Al acceder a http://localhost:8080/hikari/holders, se obtiene:

  • Nombre del hilo
  • Duración de retención (en milisegundos/segundos)
  • Pila de llamadas completa (clase+método+número de línea)

Uso de jstack y registros para localizar problemas (Resolución de emergencia en producción)

1. Obtener el PID del proceso

jps | grep jar
# o
ps -ef | grep java

2. Tomar un volcado de hilos (varias veces, con intervalos de 1 segundo)

jstack 12345 > thread_dump1.txt
sleep 1
jstack 12345 > thread_dump2.txt

3. Combinar con registros de Hikari para filtrar

  • Encontrar el nombre del hilo que mantiene la conexión en los registros de Hikari: http-nio-8080-exec-8
  • Buscar este nombre de hilo en el volcado de hilos para ver el método que está ejecutando:
"http-nio-8080-exec-8" #28 daemon prio=5 os_prio=0 tid=0x00007f8b1c228800 nid=0x3456 runnable [0x00007f8b0d7f9000]
   java.lang.Thread.State: RUNNABLE
        at com.example.service.UserService.queryUser(UserService.java:42)  // Coincide con el registro
        at com.example.controller.UserController.getUser(UserController.java:25)

Conclusión: El hilo exec-8 está bloqueado en UserService.queryUser(línea 42) y mantiene la conexión durante mucho tiempo.

Uso de Arthas para la resolución de problemas (Más conveniente, recomendado)

1. Instalar y ejecutar

curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar

2. Comandos principales

# 1. Ver todos los hilos, ordenados por uso de CPU
thread -n 10

# 2. Buscar hilos que mantienen conexiones (palabras clave: Hikari, Connection)
thread | grep -i hikari

# 3. Ver la pila de llamadas de un hilo específico (por ejemplo, exec-8)
thread http-nio-8080-exec-8

# 4. Ver el Bean de Spring (HikariDataSource)
bean com.zaxxer.hikari.HikariDataSource

# 5. Ver detalles de las conexiones activas (igual que el código anterior)
ognl '#ds=@hikariDataSource, #pool=#ds.getPool(), #active=#pool.activeConnections, #active'

Arthas permite ver en tiempo real el estado de los hilos, los métodos en ejecución y la duración de retención de las conexiones, sin necesidad de modificar el código.

Causas comunes de fugas de conexión

  1. No cerrar la conexión: conn.close() no se ejecuta (excepción no manejada, falta de finally).
  2. Transacciones sin confirmar/desactiavr: Métodos @Transactional bloqueados, bucles infinitos.
  3. Consultas lentas: Conslutas que toman varios minutos, ocupando la conexión.
  4. Tareas asíncronas: Hilos nuevos que obtienen conexiones, pero no las devuelven cuando el hilo principal termina.
  5. Fugas de conexión: Uso de new Connection() o conexiones de terceros no gestionadas.

Resumen (3 pasos para una localización rápida)

  1. Habilitar leak-detection-threshold=5000: Registros automáticos de hilo, método y tiempo de retención.
  2. Acceder a /hikari/holders: Visualización en tiempo real de los tres elementos clave.
  3. Usar jstack/Arthas: Para inspeccionar la pila de llamadas actual de los hilos en situaciones de emergencia.

Etiquetas: HikariCP Spring Boot JMX Arthas Leak Detection

Publicado el 8-14 18:36