连接池拦截:监控与统计

连接池用得好不好,光靠感觉不行。我在好几个线上项目里都吃过亏——连接池明明配了最大50个,结果一到高峰期就卡死,查了半天才发现是某个慢查询把连接全占满了。所以啊,监控必须做,而且得做到拦截层里去。

说白了,连接池拦截不仅仅是"拦截请求,拿连接还连接"。它还能顺手干一件大事:采集运行时数据。这些数据能告诉你池子是否健康、SQL是否拖慢系统、连接有没有泄露。你想想看,没有这些数字,你怎么优化?

监控哪些指标?

我个人习惯把指标分成三类:容量类、行为类、异常类。下面这张表列了最关键的几个:

分类 指标 说明
容量类 activeConnections 当前活跃连接数
容量类 idleConnections 当前空闲连接数
容量类 totalConnections 池中总连接数
行为类 connectionAcquireTime 获取连接的平均耗时
行为类 connectionUsageTime 连接被占用的平均时长
行为类 queueSize 等待队列长度
异常类 timeoutCount 获取连接超时的次数
异常类 connectionLeakCount 连接泄露的次数

这些指标,大部分主流连接池(HikariCP、Druid、Tomcat JDBC)都直接暴露了。但监控层要做的是:统一采集、统一聚合、统一暴露。我接手过一个老项目,用了三个不同来源的连接池,每个的监控API都不一样,维护起来真想骂人。所以后来我干脆在拦截层里包了一层,把所有指标都转成标准格式。

拦截器里怎么采集?

以HikariCP为例,它自带一个 HikariPoolMXBean 和 HikariConfigMXBean,可以通过JMX拿到大部分容量类数据。但行为类和异常类,比如连接占用时间、等待队列长度随时间的变化,JMX就力不从心了。

这时候就需要自定义拦截器。我的做法是:

  1. 在 DataSource.getConnection() 入口记录开始时间,并增加队列计数器。
  2. 实际连接的 close() 方法被代理,在关闭时计算占用时长,更新直方图。
  3. 所有异常(超时、获取失败)单独记录,用原子计数器累加。

下面是一个精简版的拦截器骨架:

public class MonitoredDataSource implements DataSource {
    private final DataSource delegate;
    private final AtomicInteger activeCount = new AtomicInteger(0);
    private final AtomicInteger timeoutCount = new AtomicInteger(0);
    private final SlidingWindow avgAcquireTime = new SlidingWindow(60);

    @Override
    public Connection getConnection() throws SQLException {
        long start = System.nanoTime();
        try {
            Connection conn = delegate.getConnection();
            activeCount.incrementAndGet();
            long elapsed = System.nanoTime() - start;
            avgAcquireTime.record(elapsed);
            return new MonitoredConnection(conn, this);
        } catch (SQLException e) {
            timeoutCount.incrementAndGet();
            throw e;
        }
    }

    // 其他 DataSource 方法委托...
}

小技巧:采集性能数据时,尽量用LongAdder或AtomicLong,别用synchronized。我曾经用一个synchronized的ArrayList记录耗时,结果压测时拦截器自己成了瓶颈——嗯,教训深刻。

统计数据的存储与展示

光采集不存等于白干。我推荐用环形缓冲区+定时落库的组合。为什么?因为连接池指标每秒都在变,全量存盘性能吃不消。环形缓冲区存最近1分钟的数据(精度100ms),每10秒批量写一次数据库,同时通过内存快照暴露给监控系统。

展示层面,常见的有两种方式:

  • Prometheus + Grafana:拦截器暴露/metrics端点,用Micrometer或自写Collector。
  • 自定义Dashboard:用WebSocket实时推给前端,适合内部运维平台。

我个人更倾向Prometheus方案,毕竟生态成熟,告警规则也好配。但如果你没有基础设施,直接用日志输出到ELK也能凑合用。

注意:监控采集本身不能拖慢主流程。所有统计操作必须是非阻塞的、O(1)的。我曾经见过一个团队在getConnection()里写了个Thread.sleep(10)来做采样——这直接导致连接池响应时间飙升10倍。别犯这种低级错误。

核心流程总览

把上面说的串起来,整体数据流是这样的:

连接池监控数据流 应用请求 连接池 HikariCP / Druid getConnection/close 拦截器 代理DataSource 采集耗时/计数 包裹Connection 统计模块 环形缓冲区 + 聚合 数据库 定时落盘 Grafana 告警 数据流说明 应用请求→连接池→拦截器采集指标→统计模块聚合→数据库/监控展示

这个流程图里,我特意加了一个"包裹Connection"的环节。实际项目中,如果只监控getConnection()而不监控连接使用时长,你根本不知道哪个业务模块在"占着茅坑不拉屎"。我曾经在排查一个连接泄露时,就是靠拦截器里记录了每条连接的调用栈,才找到那个忘记释放的线程。

实战建议

核心原则:监控代码必须对业务透明。你在拦截层加的指标采集,业务方不应该感知到。连接池的健康状态,应该通过独立的管理端口或监控系统查看,而不是改业务代码。

  • 告警阈值怎么设?我通常把activeConnections/maxPoolSize > 0.8持续30秒作为警告;超过95%持续10秒就告警。同时配合queueSize > 0持续5秒作为辅助指标。
  • 连接泄露怎么发现?在拦截器里给每个连接配一个WeakReference,定期扫描未正常关闭的连接,超过5分钟没归还就记录日志。
  • 性能开销能有多大?实测下来,纯原子操作+环形缓冲区写入,每10万次调用增加不到0.5ms延迟——比一次数据库查询的时间小两个数量级,放心用。

一个小坑:生产环境里,监控日志别打太多。我有个团队刚开始把每次连接获取的耗时都打一行info日志,结果日志量暴增,磁盘IO打满。后来改成每10秒聚合一次输出,或者只记录P99耗时,才解决问题。

好了,关于连接池拦截的监控与统计,核心就这些。数据采集、聚合、存储、展示,一条链路打通。下一项实战内容我们会聊到更具体的拦截模式——对数据源的统一代理改造。先把这个基础打牢,后面才不慌。