线上接口开始变慢。
你第一反应是查数据库。
慢SQL列表几乎是空的。
大部分SQL都只有几十毫秒。
数据库CPU也没有明显打满。
可连接池监控却越来越奇怪:
Active Connection一路上涨。
Idle越来越少。
Pending Thread开始出现。
最后很多请求不是卡在SQL执行,而是卡在:
getConnection()
最让人困惑的是:
SQL明明都很快,为什么数据库连接还是迟迟不归还?
如果这时候直接让 ChatGPT、Codex:
“帮我优化SQL。”
很可能方向就错了。
因为连接池真正关心的,不只是:
SQL执行了多久。
而是:
一条连接从借出去,到最终归还,一共被占用了多久。
一、先给核心判断:SQL快,不等于连接占用时间短
很多人会把这两个时间混在一起。
比如一条SQL执行:
30ms。
看起来很快。
但如果整个事务是:
拿连接。
执行SQL。
调用HTTP接口。
处理业务逻辑。
再执行第二条SQL。
最后Commit。
整个过程可能持续:
2秒。
甚至5秒。
这意味着:
虽然真正跑SQL只用了几十毫秒,
这条数据库连接却可能被这个请求占了几秒。
所以真正应该看的不是:
SQL Duration
而是:
Connection Hold Time——连接持有时间。
二、最常见的现场:事务里夹了远程调用
比如代码逻辑大概是:
先查订单。
然后调用支付接口。
再更新状态。
最后提交事务。
如果整个方法被@Transactional包住,
那数据库连接可能从第一次访问数据库开始,就一直被占着。
中间调用支付接口花了1.5秒。
连接也跟着等了1.5秒。
如果并发一上来:
20个线程。
20个连接。
大家全都拿着连接等HTTP。
这时候数据库本身可能非常闲。
但连接池已经满了。
于是后面的请求全部卡在:
等连接。
这就是为什么:
Database不忙,不代表Connection Pool不忙。
三、所以第一步不要先查慢SQL,先看连接池四个指标
我通常先看:
- Active Connection
- Idle Connection
- Pending / Waiting Thread
- Connection Acquire Time
如果出现:
Active长期接近Max。
Idle接近0。
Pending持续上涨。
Acquire Time越来越高。
那问题已经很明确:
应用拿连接的速度,大于归还连接的速度。
这时候继续盯数据库CPU,很容易浪费时间。
四、为什么单条SQL几十毫秒,连接池还是能被拖满?
假设连接池大小:
20。
每个请求真正SQL执行:
50ms。
如果连接拿到以后立即执行、立即归还,
吞吐其实很高。
但如果每个请求拿着连接平均2秒:
20个连接最多只能同时服务20个请求。
第21个请求开始:
只能等。
如果请求继续进来,
Pending就会越来越多。
所以连接池容量真正受影响的是:
每个请求占用连接的总时间。
而不是SQL自己执行多久。
五、第二个高频原因:事务边界太大
很多项目里会出现这种写法:
一个Service大方法整个加:
@Transactional
然后这个方法里面做很多事情:
查数据库。
调用RPC。
计算。
写日志。
发消息。
再更新数据库。
从业务角度看:
“这是一整个流程。”
但从资源角度看:
这可能意味着:
一条数据库连接被整个流程长期占用。
事务越大,
锁持有时间通常越长。
连接持有时间也越长。
这时候连接池压力就会明显放大。
所以排查连接池时,一定要问:
Transaction到底从哪里开始,到哪里结束?
不要只看SQL本身。
六、真实边界场景:连接泄漏
还有一种情况就更直接。
连接拿出来了。
但某条异常路径没有正确关闭。
或者:
ResultSet。
Statement。
Streaming Cursor
没有释放。
于是这些连接一直回不到池里。
这种问题的特点通常是:
Active Connection缓慢上涨。
Idle越来越少。
但即使流量下降以后:
Active也不明显回落。
这时候就要开始怀疑:
Connection Leak。
很多连接池都有Leak Detection能力。
例如可以监控:
一条连接被借出超过多少秒还没有归还。
如果这种日志持续出现,
就很值得顺着调用栈查。
七、Streaming Result也很容易让连接“看起来没问题,其实一直占着”
有些查询结果很多。
为了避免一次性加载全部数据,会使用:
流式读取。
Cursor。
边读边处理。
这种方式本身没问题。
但只要数据还没有读取完,
连接往往就不能归还。
如果你一边读数据库,一边:
做复杂计算。
调用外部接口。
写文件。
那这条连接可能会被长时间占用。
所以有时候:
SQL执行本身已经开始返回数据。
但Connection仍然没有结束生命周期。
这类场景特别容易被“没有慢SQL”误导。
八、还有一种情况:线程池比连接池大得多
比如:
业务线程池有100个线程。
数据库连接池只有20个连接。
一旦100个任务同时进来:
20个拿到连接。
剩下80个开始等。
如果这些任务本身还包含远程调用、长事务,
等待会越来越严重。
这时候你可能以为:
连接池太小。
直接改成:
20 → 50。
但问题可能只是被暂时推迟。
因为数据库本身能不能承受50个并发连接,
还要另外判断。
所以:
Thread Pool和DB Pool必须一起看。
不能各调各的。
九、为什么“把连接池调大”经常治标不治本?
这是最常见的第一反应。
Active满20。
那就改50。
如果问题只是:
连接池确实偏小,
当然可能有效。
但如果真正原因是:
长事务。
连接泄漏。
HTTP调用放在事务里。
Streaming Result占用太久。
那Pool变大以后只是:
原来20个请求一起卡。
现在50个请求一起卡。
甚至数据库压力变得更大。
最终可能出现:
连接池不排队了。
数据库开始排队。
所以调大Pool之前,一定要先回答:
为什么连接迟迟不归还?
十、最快排查顺序,我一般按这7步
遇到“没有慢SQL,但连接池越来越满”,我建议直接按这个顺序查。
第一步:看Pool状态
确认:
Active、Idle、Pending、Max。
第二步:看Connection Acquire Time
判断请求是不是已经开始大量等连接。
第三步:看事务耗时
不是SQL耗时。
是完整Transaction Duration。
第四步:看Connection Hold Time
连接拿到以后到底多久才归还。
第五步:检查事务里有没有远程调用
HTTP。
RPC。
消息。
文件IO。
第六步:检查Leak
看连接是否存在长时间不归还的异常路径。
第七步:再看线程池和数据库容量
最后才决定Pool到底应该配多大。
这个顺序比:
“先看慢SQL → 没有 → 调大连接池”
要稳很多。
十一、具体怎么修?先看根因是哪一类
如果是:
事务太大
缩小事务边界。
把非数据库操作尽量移出事务。
HTTP / RPC夹在事务里
尽量先完成远程交互,再进入真正需要原子性的数据库阶段。
Connection Leak
补全关闭逻辑。
检查异常路径。
开启Leak Detection。
Streaming Result占用过久
缩短处理时间。
分页。
批次读取。
不要边占连接边做大量非数据库工作。
Pool确实偏小
在确认数据库承载能力以后,再合理调整。
不是盲目翻倍。
十二、还有一个很实用的判断:流量下降以后,Active会不会降下来?
这能快速区分很多问题。
如果高峰过去以后:
Active很快从20降到3。
Idle恢复。
那更像:
瞬时并发 + 容量不足。
如果高峰过去以后:
Active还长期维持19、20。
那就要重点怀疑:
长事务。
连接泄漏。
卡死调用。
未关闭资源。
这个现象非常有用。
十三、修完以后怎么验证?
不要只看:
“接口现在快了。”
更应该重新制造原来的并发。
然后观察:
Active是否会在峰值后回落。
Pending是否减少。
Acquire Time是否下降。
Transaction P95 / P99是否下降。
连接泄漏日志是否消失。
数据库CPU、IO是否仍然健康。
最重要的是:
Completed Request Rate能不能稳定高于进入速率。
否则Queue和Pending迟早还会再涨。
十四、一个典型修复前后的差异
修复前:
连接池Max = 20。
Active长期20。
Pending = 80。
SQL平均30ms。
Transaction平均2.3s。
修复以后:
把事务里的HTTP调用移出去。
Transaction从2.3s降到180ms。
结果即使连接池仍然是20:
Active峰值可能只到12。
Pending直接接近0。
这就说明:
真正的问题从来不是:
连接数量太少。
而是:
每条连接占用得太久。
十五、这种连接池排查,Plus和Pro怎么判断?
如果你平时只是偶尔让 ChatGPT、Codex:
分析连接池监控。
看看事务边界。
检查一段@Transactional代码。
辅助判断是不是Leak。
这种任务范围比较集中,Plus通常已经够用。
关键是把Evidence给完整:
Pool指标。
事务耗时。
线程池大小。
DB Pool大小。
代码调用链。
远程调用位置。
这些信息比只给一句:
“连接池满了”
有用得多。
如果你的日常已经变成:
持续排查数据库连接、线程池、事务、Redis、HTTP依赖。
一个问题需要结合:
Trace。
Thread Dump。
连接池指标。
数据库监控。
应用代码。
还要让Codex修改事务边界、补压测、做回归,
而且这种多轮工程任务每天都在发生,
那Pro会更适合。
所以Plus还是Pro,不看:
“连接池问题高级不高级。”
而是看:
ChatGPT、Codex是不是已经长期参与完整的工程排障链路。
偶发排查、短任务:
Plus通常够用。
高频性能治理、多轮分析验证:
再考虑Pro。
最后
没有慢SQL,
为什么数据库连接池还是越来越满?
因为真正决定连接池压力的,不只是:
SQL执行多久。
而是:
每条连接到底被占用了多久。
一个30ms的SQL,
完全可能因为:
长事务。
HTTP调用。
Streaming Result。
连接泄漏
让连接被占2秒、5秒甚至更久。
所以以后看到:
Active越来越高。
Pending越来越多。
不要第一反应只去查慢SQL。
先把:
Connection Acquire Time → Transaction Duration → Connection Hold Time → 外部调用 → Leak
这一条链还原出来。
很多“数据库不忙,但连接池爆了”的问题,真正根因往往就在这里。
持续分享 Codex、大模型开发与 AI 编程实战内容。
长期深度使用各类代码大模型,也整理了稳定的Plus/Pro会员订阅渠道,有需要可自取。