连接池拦截:监控与统计
连接池用得好不好,光靠感觉不行。我在好几个线上项目里都吃过亏——连接池明明配了最大50个,结果一到高峰期就卡死,查了半天才发现是某个慢查询把连接全占满了。所以啊,监控必须做,而且得做到拦截层里去。
说白了,连接池拦截不仅仅是"拦截请求,拿连接还连接"。它还能顺手干一件大事:采集运行时数据。这些数据能告诉你池子是否健康、SQL是否拖慢系统、连接有没有泄露。你想想看,没有这些数字,你怎么优化?
监控哪些指标?
我个人习惯把指标分成三类:容量类、行为类、异常类。下面这张表列了最关键的几个:
| 分类 | 指标 | 说明 |
|---|---|---|
| 容量类 | activeConnections | 当前活跃连接数 |
| 容量类 | idleConnections | 当前空闲连接数 |
| 容量类 | totalConnections | 池中总连接数 |
| 行为类 | connectionAcquireTime | 获取连接的平均耗时 |
| 行为类 | connectionUsageTime | 连接被占用的平均时长 |
| 行为类 | queueSize | 等待队列长度 |
| 异常类 | timeoutCount | 获取连接超时的次数 |
| 异常类 | connectionLeakCount | 连接泄露的次数 |
这些指标,大部分主流连接池(HikariCP、Druid、Tomcat JDBC)都直接暴露了。但监控层要做的是:统一采集、统一聚合、统一暴露。我接手过一个老项目,用了三个不同来源的连接池,每个的监控API都不一样,维护起来真想骂人。所以后来我干脆在拦截层里包了一层,把所有指标都转成标准格式。
拦截器里怎么采集?
以HikariCP为例,它自带一个 HikariPoolMXBean 和 HikariConfigMXBean,可以通过JMX拿到大部分容量类数据。但行为类和异常类,比如连接占用时间、等待队列长度随时间的变化,JMX就力不从心了。
这时候就需要自定义拦截器。我的做法是:
- 在
DataSource.getConnection()入口记录开始时间,并增加队列计数器。 - 实际连接的
close()方法被代理,在关闭时计算占用时长,更新直方图。 - 所有异常(超时、获取失败)单独记录,用原子计数器累加。
下面是一个精简版的拦截器骨架:
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倍。别犯这种低级错误。
核心流程总览
把上面说的串起来,整体数据流是这样的:
这个流程图里,我特意加了一个"包裹Connection"的环节。实际项目中,如果只监控getConnection()而不监控连接使用时长,你根本不知道哪个业务模块在"占着茅坑不拉屎"。我曾经在排查一个连接泄露时,就是靠拦截器里记录了每条连接的调用栈,才找到那个忘记释放的线程。
实战建议
核心原则:监控代码必须对业务透明。你在拦截层加的指标采集,业务方不应该感知到。连接池的健康状态,应该通过独立的管理端口或监控系统查看,而不是改业务代码。
- 告警阈值怎么设?我通常把
activeConnections/maxPoolSize > 0.8持续30秒作为警告;超过95%持续10秒就告警。同时配合queueSize > 0持续5秒作为辅助指标。 - 连接泄露怎么发现?在拦截器里给每个连接配一个
WeakReference,定期扫描未正常关闭的连接,超过5分钟没归还就记录日志。 - 性能开销能有多大?实测下来,纯原子操作+环形缓冲区写入,每10万次调用增加不到0.5ms延迟——比一次数据库查询的时间小两个数量级,放心用。
一个小坑:生产环境里,监控日志别打太多。我有个团队刚开始把每次连接获取的耗时都打一行info日志,结果日志量暴增,磁盘IO打满。后来改成每10秒聚合一次输出,或者只记录P99耗时,才解决问题。
好了,关于连接池拦截的监控与统计,核心就这些。数据采集、聚合、存储、展示,一条链路打通。下一项实战内容我们会聊到更具体的拦截模式——对数据源的统一代理改造。先把这个基础打牢,后面才不慌。