MySQL连接泄漏,应用长连接过多耗尽连接池排查
MySQL 连接池被耗尽,最常见的表现是应用报 Too many connections 或接口突然大面积超时。
这个问题多半不是单条 SQL 慢,而是应用侧把连接借出去之后没有正确归还,时间一长,连接数只涨不降。
本文会从查连接数、定位源头、调整参数到验证恢复,带你按顺序把问题找出来并处理掉。
先确认连接数真的满了
登录 MySQL 后,先看当前连接总数和上限:
SHOW STATUS LIKE 'Threads_connected';
SHOW VARIABLES LIKE 'max_connections';
如果 Threads_connected 长时间接近 max_connections,说明连接池确实被占满。
再用下面这条 SQL,按用户和主机维度看谁占用的连接最多:
SELECT user, host, db, COUNT(*) AS cnt
FROM information_schema.processlist
GROUP BY user, host, db
ORDER BY cnt DESC;
也可以直接看 processlist 里每个连接的 Command 和 State,快速判断是空闲连接还是正在跑查询:
SELECT id, user, host, db, command, time, state, info
FROM information_schema.processlist
ORDER BY time DESC;
如果看到大量 Command = Sleep 且 Time 很大的连接,基本可以判断连接拿出去后没有及时归还,也就是典型的连接泄漏。
从应用侧定位泄漏源头
数据库只负责展示连接状态,真正要修的是应用代码。
先在应用服务器上用 ss 或 netstat 看当前到 MySQL 3306 端口的连接数:
ss -ant | grep ':3306' | awk '{print $5}' | cut -d: -f1 | sort | uniq -c
如果某个应用实例连接数特别高,就重点查这个实例的日志和连接池监控。
常见原因有三类:
- 使用
DataSource时没走try-with-resources,异常路径下connection.close()没执行。 - 事务操作中手动创建了 Connection,但事务提交或回滚后忘记释放。
- 连接池的最大连接数设置过大,超过了 MySQL 的
max_connections,比如 MySQL 上限 500,连接池却配了 600。
如果是 Java 应用,可以开启连接池的 leakDetectionThreshold(HikariCP)或 removeAbandonedTimeout(Druid),让连接池自动检测并回收超时未归还的连接。
下面是一个 HikariCP 的配置片段:
spring:
datasource:
hikari:
maximum-pool-size: 50
minimum-idle: 10
connection-timeout: 30000
idle-timeout: 600000
leak-detection-threshold: 60000
leak-detection-threshold: 60000 表示连接借出超过 60 秒未归还就打印告警日志,但不会强制回收,主要用于发现问题。
Druid 则可以用 removeAbandoned=true 强制回收。
调低连接池上限并设置连接生命周期
连接池不是越大越好。
如果并发不高,把 maximum-pool-size 调小反而能保护数据库。
同时加上连接最大存活时间和空闲回收,避免长连接长期占用:
spring:
datasource:
hikari:
maximum-pool-size: 30
minimum-idle: 5
max-lifetime: 1800000
idle-timeout: 600000
max-lifetime 建议小于 MySQL 的 wait_timeout。
可以在 MySQL 里查看:
SHOW VARIABLES LIKE 'wait_timeout';
如果 wait_timeout 是 28800(8 小时),
应用侧 max-lifetime 建议设为 1800000(30 分钟),
这样即使有连接没关闭,
MySQL 也会在超时后主动断掉一部分空闲连接。
如果应用和 MySQL 之间有防火墙或代理(比如 ProxySQL、
云数据库代理),
还要检查代理层的空闲超时,
有时连接是被代理切断了,
但应用不知道,
继续使用就会报错,
这类错误会被误认为是连接泄漏。
处理已经堆积的连接
在应用修复并重新发布前,可以先临时清理空闲连接,让服务恢复可用。
MySQL 中不能直接 kill 所有 Sleep 连接,但可以写一条拼接 SQL:
SELECT CONCAT('KILL ', id, ';')
FROM information_schema.processlist
WHERE command = 'Sleep' AND time > 300;
把查询结果复制出来执行,或者用存储过程循环执行。
注意这只是一个应急手段,如果代码不修,新连接很快又会堆积。
更好的做法是重启应用,让连接池重新建立连接。
如果应用无法立即重启,只能靠调低 wait_timeout 让 MySQL 更快回收空闲连接,例如临时设置为 300 秒:
SET GLOBAL wait_timeout = 300;
这个设置会在 MySQL 重启后失效,不能当作长期方案。
验证问题是否解决
按照前面的步骤操作后,重点观察以下几点:
- 用
SHOW STATUS LIKE 'Threads_connected';查看连接数是否回落到正常水位,且不再持续上涨。 - 用
SHOW PROCESSLIST;观察Sleep连接是否维持在低位,没有大量长时间空闲连接。 - 查看应用日志,确认没有新的
Too many connections或连接获取超时错误。
如果你给连接池加了泄漏检测,日志中也不再出现 Connection leak detection 相关警告,说明泄漏点已经处理掉。
还有一点很容易忽略:连接池的选择和参数配置要匹配实际框架版本。
Druid、HikariCP、dbcp 的默认参数不同,调整前先确认你用的到底是哪种连接池,再查对应版本的官方文档,不要照搬别人的配置。
如果你用的云数据库或自建 MySQL 版本不同,information_schema.processlist 的字段基本一致,但个别版本可能没有 db 字段,以实际输出为准。
排查 MySQL 连接泄漏的核心思路就是三个字:看、杀、修。
先看连接哪里来,再杀长时间空闲连接应急,最后在应用代码或连接池配置里把归还逻辑修好。
按照本文的步骤操作,大多数连接池耗尽问题都能在半小时内定位到方向,剩下的就是改代码和发布验证了。