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
- No cerrar la conexión:
conn.close()no se ejecuta (excepción no manejada, falta de finally). - Transacciones sin confirmar/desactiavr: Métodos
@Transactionalbloqueados, bucles infinitos. - Consultas lentas: Conslutas que toman varios minutos, ocupando la conexión.
- Tareas asíncronas: Hilos nuevos que obtienen conexiones, pero no las devuelven cuando el hilo principal termina.
- Fugas de conexión: Uso de
new Connection()o conexiones de terceros no gestionadas.
Resumen (3 pasos para una localización rápida)
- Habilitar
leak-detection-threshold=5000: Registros automáticos de hilo, método y tiempo de retención. - Acceder a
/hikari/holders: Visualización en tiempo real de los tres elementos clave. - Usar jstack/Arthas: Para inspeccionar la pila de llamadas actual de los hilos en situaciones de emergencia.