连接数失控排查:从数据库会话、应用线程池到连接池泄漏
应用接入数据库后,最常见的生产问题之一就是连接数失控。现象可能是接口突然变慢,应用日志出现获取连接超时,数据库提示连接数接近上限,甚至新请求完全进不来。
连接数失控通常不是单点问题。可能来自连接池配置过大,应用实例扩容后没有重新核算连接预算,SQL 执行太慢,事务长时间不提交,代码没有归还连接,也可能是某个报表任务突然拉满了数据库。本文用一套排查路径把这些原因串起来。 
@toc
一、典型故障现象
那当连接数真的失控的时候,你能看到些啥现象呢?通常来说就是这么几种:
- Spring Boot 日志出现
Connection is not available。 - 接口响应时间明显变长。
- 数据库新连接变慢或失败。
sys_stat_activity中会话数量快速上涨。- 大量会话处于空闲、活跃或等待状态。
- 应用重启后短暂恢复,过一段时间又复现。
这里我要特别提一下。要是你重启了一下应用,它临时恢复了,但是过一段时间又复现了。那这种情况,你就要重点怀疑了。怀疑啥呢?一个是连接泄漏,再一个是慢 SQL 堆积,还有可能就是定时任务在捣鬼。
说明一下哈,本文里的示例,我是用运行状态视图来观察会话的。那因为 KingbaseES 版本或者模式不一样的话,视图名和字段往往会有点差异。所以你实际去排查的时候,先用本机的产品手册查一查,或者用 \d 命令看看,再不行用图形化工具确认一下。确认好了,再去替换我给的示例 SQL。
二、先在数据库侧确认连接分布
咱们进了金仓数据库之后啊,第一步先看看当前的会话。按用户和状态给它分个布,看看是啥情况:
sql
SELECT usename, state, COUNT(*) AS cnt
FROM sys_stat_activity
GROUP BY usename, state
ORDER BY cnt DESC;

那要是你能看到客户端的地址和应用名的话,那就可以再进一步拆分细一点来看了:
sql
SELECT usename, application_name, client_addr, state, COUNT(*) AS cnt
FROM sys_stat_activity
GROUP BY usename, application_name, client_addr, state
ORDER BY cnt DESC;

这里你得重点关注几个点:
- 是哪个账号占用连接最多。
- 是哪个客户端 IP 占用连接最多。
- 那这些连接主要是活跃的呢,还是空闲的,或者是等待状态。
- 有没有那种大量的连接,全是从同一个应用实例过来的情况。
要是你没配置应用名的话,那数据库这边就只能看到账号和客户端地址了。这样定位起来就会变慢。所以我建议大家,后续最好在连接串或者连接池里面,把应用的标识给补上,这样好认。
三、看应用连接池是否到顶
咱们用 Spring Boot 的话,通常会用到 HikariCP。那如果连接池被耗尽了,日志里往往就会出现类似这样的信息:
text
Connection is not available, request timed out after 3000ms
这说明啥问题呢?说明请求去连接池里拿连接的时候,等的时间太长,超时了。那为什么会这样呢?原因通常来说有两个方向:
- 连接池太小了,正常并发下根本不够用。
- 连接被占用得太久,释放不回来了。
这里提醒一下,千万别一看到这个报错,就直接去把 maximum-pool-size 调大。你得先确认一下,连接到底是被谁给占住了?为啥它不释放出来?搞清楚这个再说。
四、识别慢 SQL 堆积
咱们可以查一下,当前正在执行的 SQL 里面,哪些是跑了很久的:
sql
SELECT pid,
usename,
client_addr,
state,
query_start,
now() - query_start AS running_time,
query
FROM sys_stat_activity
WHERE state = 'active'
ORDER BY running_time DESC;
要是你发现大量的 SQL 执行时间都特别长。那么连接数失控的根子,可能就不在连接池身上了。是什么呢?是 SQL 太慢了。这是一个问题。那连接池里的连接都被这些慢 SQL 给占住了,后来的请求自然就拿不到连接了。
那处理的思路是怎么样的呢?
- 先定位出最耗时的 SQL。
- 接着去看看它的执行计划。
- 检查一下索引啊、统计信息啊,还有返回的数据量。
- 一定要控制住接口一次查询的数据范围,别一次拉太多。
你光去增加连接数,顶多也就缓解那么一小段时间。到最后,肯定还是会被慢 SQL 给拖垮的。所以根本的出路,还是要去优化你的 SQL。
五、识别长事务
长事务这个事儿也很头疼。它会让连接长时间不归还。而且往往还会造成锁等待,把资源都占住。咱们可以查一下事务的启动时间:
sql
SELECT pid,
usename,
client_addr,
state,
xact_start,
now() - xact_start AS xact_age,
query
FROM sys_stat_activity
WHERE xact_start IS NOT NULL
ORDER BY xact_age DESC;
要是你看到某些连接的事务已经持续了很久了。那你就得回到应用代码里面去检查一下了。检查啥呢?
- 是不是方法上加了过大的事务范围。
- 或者事务里面是不是调用了外面的接口。
- 再看看事务里面是不是做了大量的循环处理。
- 还有一种情况,就是异常分支里面,没有把事务正确地结束掉。
其实应用里的事务,咱们应该尽量让它短一点。千万别把什么文件处理啊、远程调用啊、复杂计算这些东西,全都包在数据库事务里头。那样早晚会出事。
六、识别连接泄漏
连接泄漏这个东西,它典型的表现是啥呢?
- 应用一启动,连接数就不断往上涨。
- 请求结束之后呢,连接也不见下降。
- 到最后连接池就被耗尽了。
- 你重启一下应用,它又临时恢复了。
那么在开发和测试环境里,我建议大家把 HikariCP 的泄漏检测给打开:
yaml
spring:
datasource:
hikari:
leak-detection-threshold: 10000
这里的话,如果某个连接被借出去超过了 10 秒钟还没还回来。那日志就会把相关的调用栈给打出来。常见的代码问题大概有这么几种:
- 自己手写 JDBC 的时候,没把
Connection关掉。 - 异常分支提前返回了,资源没释放。
- 结果集做流式处理的时候忘了关。
- 再就是自己绕开框架,直接去管理连接了。
所以我推荐大家用 try-with-resources 这种写法:
java
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(sql);
ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
// process row
}
}
那如果你用的是 Spring 的 JdbcTemplate,或者说 MyBatis、事务管理器这些。也别在业务代码里面随随便便就去缓存连接对象,这点要注意。
七、检查应用实例数量
其实连接数失控,经常是在应用扩容之后发生的。那原因也很简单。啥原因呢?连接池的配置,它是每个实例都有一份的。
咱们假设一下:
- 每个实例的
maximum-pool-size=30 - 应用实例数从 2 个扩到了 8 个
那么理论上的最大连接数,就从 60 变成 240 了。这个增长量是很吓人的。数据库那边要是没跟着同步评估一下,那很容易就被打满了。
所以上线或者扩容之前,你必须得算一算这个账:
text
总连接预算 = 单实例最大连接数 * 应用实例数 + 后台任务连接 + 报表连接 + 运维工具连接
算出来的这个值,必须得比数据库可用的连接上限小。而且还得留点管理连接的空间出来,别算得太死。
八、检查定时任务和批处理
其实很多时候,连接数的高峰根本不是用户请求弄出来的。那是啥弄出来的?是定时任务弄出来的。比如:
- 每天 0 点跑个批量同步。
- 每 5 分钟扫一次全表。
- 报表任务一次性拉取大量数据。
- 还有多个任务同时启动,各自都在占着连接。
那排查的方法是怎么样的呢?
- 对照一下连接数上涨的时间和定时任务执行的时间。
- 看看应用日志里,任务开始和结束的时间。
- 给批处理任务单独设个线程池,限制一下并发。
- 大任务一定要拆分成分页处理,别一次拉满。
批处理和在线接口,最好不要无限制地共用同一个连接池。不然的话,夜里跑的任务,很可能就把白天的服务给影响了。
九、故障期间是否可以杀连接
那要是故障期间,你确认了某些会话在异常占用资源。这时候你可以考虑把会话给终止掉。但是大家注意,这属于应急的动作。做之前,一定要先确认好影响。
先查一下目标会话是啥:
sql
SELECT pid, usename, client_addr, state, query
FROM sys_stat_activity
WHERE usename = 'app_user'
ORDER BY query_start;
确认了目标会话之后,你可以选下面这两种方式来处理:
sql
-- 仅取消正在执行的 SQL,保留会话连接(优先考虑)
SELECT sys_cancel_backend(目标pid);
-- 终止整个会话并断开连接,正在执行的事务会回滚
SELECT sys_terminate_backend(目标pid);
执行之前,你必须得确认几个事儿:
- 这个连接是不是正在执行关键交易。
- 终止了之后,应用那边会不会自己重试。
- 会不会造成业务上只成功了一部分的情况。
通常来说,优先用 sys_cancel_backend。这个命令它只把 SQL 执行给中断了,会话连接还在。应用那边一般能正常抓到错误然后重试。那 sys_terminate_backend 呢,它会直接强制把连接断开。应用那边就会拿到连接异常了。这个适合那种连接已经彻底卡死的情况。最后强调一句,生产环境里面,千万别批量盲杀连接。一定要先定位,再处理。
十、连接数失控排查清单
| 排查点 | 观察方式 | 可能结论 |
|---|---|---|
| 数据库会话总数 | sys_stat_activity |
是不是快到上限了 |
| 账号分布 | 按 usename 分组 |
哪个应用占得最多 |
| 客户端来源 | 按 client_addr 分组 |
哪台机器不正常 |
| 活跃 SQL | state='active' |
有没有慢 SQL 堆积 |
| 长事务 | xact_start |
事务是不是没结束 |
| 连接池日志 | HikariCP 日志 | 是不是获取连接超时了 |
| 泄漏检测 | leak-detection-threshold |
连接是不是没归还 |
| 实例数量 | 部署平台 | 扩容后是不是放大了连接 |
| 定时任务 | 应用日志 | 是不是批处理给拉满了 |
十一、小结
所以你看,连接数失控这个问题,你不能光靠着调大连接池来解决。正确的路径是啥呢?先从数据库那边确认会话分布。然后再回到应用这边,一个一个去排查。查连接池、查线程池、查事务范围、查慢 SQL、查定时任务,还有查连接泄漏。等你把连接长期占用的根因给找到了,系统才能真正稳下来。
那下一篇呢,咱们接着看事务边界和批量写入的事儿。重点解决一下长事务、锁等待,还有批处理把数据库拖慢的这些问题。大家到时候看。