WEBKT

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. 埋点追踪连接获取和释放

光看连接池状态还不够,我们需要更精细的追踪,知道哪个业务模块在什么时间获取了连接,又在什么时间释放了连接。这就需要在代码中埋点。

实现思路:

  1. AOP 切面: 使用 AOP 切面拦截所有获取和释放连接的操作。
  2. ThreadLocal 存储: 在 ThreadLocal 中存储当前线程持有的连接信息,例如连接创建时间、业务模块名称等。
  3. 日志记录: 在连接获取和释放时,记录详细的日志,包括线程 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 的获取方式需要根据你的项目实际情况调整。
  • 日志级别可以根据实际情况调整,建议在问题排查阶段设置为 INFODEBUG
  • 避免在切面中执行耗时操作,以免影响性能。
  • 考虑使用 try-with-resources 语句,自动关闭连接,减少手动释放连接的遗漏。

3. 分析日志,定位泄露点

有了详细的日志,就可以开始分析了。你可以使用 ELK Stack (Elasticsearch, Logstash, Kibana) 等日志分析工具,将日志导入并进行分析。

分析思路:

  1. 按业务模块分组: 统计每个业务模块获取和释放连接的数量。
  2. 时间序列分析: 观察每个业务模块的连接使用情况随时间的变化趋势。
  3. 异常情况排查: 关注连接获取后长时间未释放的连接,以及连接释放失败的异常情况。

通过分析,你应该能够找到哪个业务模块存在连接泄露,并定位到具体的代码位置。

总结

解决 Too many connections 问题需要细致的排查和分析。通过监控连接池状态、埋点追踪连接获取和释放,以及分析日志,你可以逐步缩小问题范围,最终找到连接泄露的根源。希望这些方法能帮助你解决问题!

Debug侠 Java数据库连接池连接泄露

评论点评