Java 应用 "Too many connections" 问题排查:实时追踪连接泄露
50
0
0
0
线上 Java 应用 "Too many connections" 疑云:实时追踪连接泄露
最近线上环境频繁出现 Too many connections 错误,让人头大。数据库明明配置了足够大的最大连接数,而且 SHOW PROCESSLIST 显示的连接数也远未达到上限。直觉告诉我,这可能是应用层面的连接泄露在作祟!但问题是,如何才能实时追踪到哪个业务模块在哪个时间点耗尽了连接资源呢?
别慌,这里分享几个实用技巧,希望能帮你拨开云雾:
1. 监控连接池状态
首先,你需要监控你的数据库连接池的状态。无论是使用 Druid、HikariCP 还是 Tomcat JDBC Pool,它们都提供了监控接口。你需要关注以下几个指标:
- Active Connections: 当前活跃的连接数。
- Idle Connections: 当前空闲的连接数。
- Connections Created: 创建的连接总数。
- Connections Destroyed: 销毁的连接总数。
- Wait Count: 等待连接的线程数。
通过监控这些指标,你可以大致了解连接池的健康状况。如果 Active Connections 持续增长,Idle Connections 持续减少,而 Wait Count 较高,那么很可能存在连接泄露。
具体操作:
- Druid: Druid 提供了 Web 监控页面,可以直接查看连接池状态。也可以通过编程方式获取监控数据。
- HikariCP: HikariCP 提供了 JMX 接口,可以使用 JConsole 或 VisualVM 等工具监控连接池状态。
- Tomcat JDBC Pool: Tomcat JDBC Pool 也提供了 JMX 接口,监控方法类似 HikariCP。
2. 埋点追踪连接获取和释放
光看连接池状态还不够,我们需要更精细的追踪,知道哪个业务模块在什么时间获取了连接,又在什么时间释放了连接。这就需要在代码中埋点。
实现思路:
- AOP 切面: 使用 AOP 切面拦截所有获取和释放连接的操作。
- ThreadLocal 存储: 在 ThreadLocal 中存储当前线程持有的连接信息,例如连接创建时间、业务模块名称等。
- 日志记录: 在连接获取和释放时,记录详细的日志,包括线程 ID、业务模块名称、连接信息、时间戳等。
示例代码 (简化版):
@Aspect
@Component
public class ConnectionMonitorAspect {
private static final Logger logger = LoggerFactory.getLogger(ConnectionMonitorAspect.class);
private static final ThreadLocal<Map<Connection, String>> connectionContext = new ThreadLocal<>();
@Around("execution(* javax.sql.DataSource.getConnection())")
public Object aroundGetConnection(ProceedingJoinPoint joinPoint) throws Throwable {
Connection connection = (Connection) joinPoint.proceed();
String moduleName = // 获取当前业务模块名称,例如从请求上下文中获取
if (moduleName == null) {
moduleName = "Unknown";
}
Map<Connection, String> connections = connectionContext.get();
if (connections == null) {
connections = new HashMap<>();
connectionContext.set(connections);
}
connections.put(connection, moduleName);
logger.info("Thread: {}, Module: {}, Connection acquired", Thread.currentThread().getId(), moduleName);
return connection;
}
@After("execution(* java.sql.Connection.close())")
public void afterCloseConnection(JoinPoint joinPoint) {
Connection connection = (Connection) joinPoint.getTarget();
Map<Connection, String> connections = connectionContext.get();
if (connections != null && connections.containsKey(connection)) {
String moduleName = connections.remove(connection);
logger.info("Thread: {}, Module: {}, Connection released", Thread.currentThread().getId(), moduleName);
} else {
logger.warn("Thread: {}, Connection released but not tracked", Thread.currentThread().getId());
}
}
@AfterThrowing(pointcut = "execution(* java.sql.Connection.close())", throwing = "e")
public void afterThrowingCloseConnection(JoinPoint joinPoint, Throwable e) {
// 记录异常信息,例如连接未关闭的原因
logger.error("Thread: {}, Connection close failed", Thread.currentThread().getId(), e);
}
}
注意事项:
- 确保
getConnection()和close()方法都被切面拦截到。 moduleName的获取方式需要根据你的项目实际情况调整。- 日志级别可以根据实际情况调整,建议在问题排查阶段设置为
INFO或DEBUG。 - 避免在切面中执行耗时操作,以免影响性能。
- 考虑使用
try-with-resources语句,自动关闭连接,减少手动释放连接的遗漏。
3. 分析日志,定位泄露点
有了详细的日志,就可以开始分析了。你可以使用 ELK Stack (Elasticsearch, Logstash, Kibana) 等日志分析工具,将日志导入并进行分析。
分析思路:
- 按业务模块分组: 统计每个业务模块获取和释放连接的数量。
- 时间序列分析: 观察每个业务模块的连接使用情况随时间的变化趋势。
- 异常情况排查: 关注连接获取后长时间未释放的连接,以及连接释放失败的异常情况。
通过分析,你应该能够找到哪个业务模块存在连接泄露,并定位到具体的代码位置。
总结
解决 Too many connections 问题需要细致的排查和分析。通过监控连接池状态、埋点追踪连接获取和释放,以及分析日志,你可以逐步缩小问题范围,最终找到连接泄露的根源。希望这些方法能帮助你解决问题!