侧边栏壁纸
  • 累计撰写 149 篇文章
  • 累计收到 2 条评论

MySQL 突发 Too many connections 报错连不上?排查连接池泄露、Sleep 堆积与参数调优我踩过的几个坑

2026-10-2 / 0 评论 / 14 阅读
AI 摘要由 AI 生成

文章分析了MySQL“Too many connections”错误,指出原因可能是连接池泄露、Sleep堆积或参数调优不当。文章详细介绍了排查方法,包括如何进入数据库进行紧急排查、配置独立管理接口、利用gdb动态调整连接数上限等,以应对数据库连接数过多的问题。

周二上午业务高峰期,告警群突然报警,所有微服务和前台接口大面积超时报错。
查看后端应用日志,控制台密密麻麻全是清一色的红字:
java.sql.SQLException: null, message from server: "ERROR 1040 (08004): Too many connections"。
紧接着,后台系统和报表定时任务也跟着全线失败。
这时候想通过终端敲 mysql -u root -p 登进数据库排查现场,终端却直接甩出来同一句报错:
ERROR 1040 (08004): Too many connections。

数据库连接彻底满了,连管理员 root 都被挡在外面,连执行 SHOW PROCESSLIST 看一眼现场的权限都拿不到。

遇到这种情况,很多人第一反应就是登录服务器直接重启 MySQL,或者网上搜个命令把 max_connections 从默认的 151 改到 2000。
但如果没找到连接暴增的真实原因,改完配置跑不了几天,或者下次流量稍微抬升,连接数照样会被打满,甚至还会因为连接数调得过高直接把整机内存吃崩。

把排查过程里的几个关键节点和调优细节理顺,线上再遇到连接报警就能直接定界。


坑一:连 root 都被挡在门外,怎么进数据库做紧急排查?

MySQL 抛出 Too many connections 时,说明当前活跃连接总数已经达到了 max_connections 的上限。

很多人会有疑问:MySQL 不是默认给具有管理员权限的账号保留了一个额外的专属连接名额吗?
MySQL 确实设计了这种机制,允许具备高权限的账号在连接数达到 max_connections 时再多连一次,理论上限是 max_connections + 1。

但这个保留名额在线上常常起不到保命效果。常见的原因有两个:

  1. 应用程序在配置数据源时,有些项目为了图省事直接使用了 root 账号。这会导致业务代码在发狂创建连接时,把这个唯一的保留名额也一同耗光。
  2. 多个开发或者运维人员同时尝试 SSH 登录数据库,第一个人连进去之后没退出,其他人再次尝试就会被彻底拒之门外。

当 root 彻底登不进去的时候,切忌直接用 kill -9 强杀 mysqld 进程。非正常关闭 MySQL 会触发 InnoDB 引擎崩溃恢复,大数据量场景下仅重放 redo log 就要耗费半小时甚至更久。

怎么留下真正的独立管理后门?

从 MySQL 8.0.14 开始,官方引入了独立管理接口:admin_address 与 admin_port。
在配置文件 /etc/my.cnf 中增加:

[mysqld]
# 常规业务端口
port = 3306

# 独立管理端口与监听地址
admin_address = 127.0.0.1
admin_port = 33062
create_admin_listener_thread = 1

这个接口拥有独立的网络监听线程与通道,完全不受 max_connections 限制。具备 SERVICE_CONNECTION_ADMIN 权限的账号,即使 3306 端口被全部打满,也能在服务器本地通过 33062 端口顺利进入:

mysql -u root -p -h 127.0.0.1 -P 33062

没配独立端口时,当前的应急抢救手段

如果运行的是旧版本或者线上事先没配管理端口,还有两招可以快速破局:

  1. 利用 gdb 动态调大内存中的 max_connections:
    只要拥有操作系统的 root 权限,可以在不重启 MySQL 的前提下,通过 gdb 挂载到 mysqld 进程修改变量值:
# 获取 mysqld 进程 PID
PID=$(pidof mysqld)

# 动态扩容最大连接数配额
gdb -p $PID -batch -ex "set max_connections=2000"

命令执行完毕后,MySQL 内存里的连接数上限会被立即放宽,此时 root 就可以正常通过 3306 端口连进去了。登录后应先执行 SET GLOBAL max_connections = 2000; 确保运行状态同步,排查完毕再调回合理区间。

  1. 从网络协议栈定位异常来源:
    即便进不去数据库内部,也可以直接在 Linux 终端查看 TCP 连接分布:
# 统计连接到 3306 端口的前 10 个客户端 IP
ss -ant '( sport = :3306 )' | awk '{print $5}' | cut -d: -f1 | sort | uniq -c | sort -rn | head -n 10

观察哪个 IP 占用了几百上千个连接。如果是某台应用节点或者批处理任务在疯狂建立连接,直接在对应节点把该服务临时停掉,连接瞬间释放,管理员就能立刻登入。


坑二:盲目调大 max_connections,把内存直接吃崩

登入数据库后,不少人的操作就是直接把 max_connections 设到 3000 甚至 5000。

这很容易踩入第二个误区:MySQL 的每个连接都要消耗独立的内存空间。

MySQL 的内存分配主要分为两块:

  • 全局共享内存:如 innodb_buffer_pool_size、key_buffer_size,服务启动时便已预先分配好。
  • 线程私有内存:每个连接建立后,在执行排序、关联、临时表计算等操作时,内核会为每个会话单独申请缓冲区。常见的包括 sort_buffer_size、join_buffer_size、read_buffer_size、read_rnd_buffer_size、thread_stack 等。

算一笔账:如果单个会话的私有缓冲区配置在 8MB 到 16MB 之间,当活跃连接数冲到 3000 时,线程私有内存就会消耗 24GB 到 48GB。
如果物理机总共只有 32GB 内存,加上占了大头的 InnoDB 缓冲池,整机可用内存会被迅速吃干。
接下来就会触发 Linux 内核的 OOM Killer,mysqld 进程被直接击杀,造成更严重的故障停机。

合理的参数设定逻辑

单台 MySQL 实例承载的连接数,通常建议控制在 800 到 1500 以内。
如果业务端并发量确实很大,应当在应用层收敛连接池配置,或者在前面部署 ProxySQL 进行连接复用,避免无节制推高数据库底层限制。

检查历史最高连接数峰值:

SHOW GLOBAL STATUS LIKE 'Max_used_connections';
SHOW GLOBAL VARIABLES LIKE 'max_connections';

如果 Max_used_connections 长期处于 max_connections 的 85% 左右,说明需要扩容或优化;如果平时只有几十个连接,短时间内瞬间打满,根因通常是连接泄露或者阻塞,调大参数起不到治本作用。


坑三:8 小时默认超时,大量 Sleep 连接占着茅坑不拉屎

登录数据库后执行 SHOW FULL PROCESSLIST;,排查时常常会看到这样的景象:上千个连接里,绝大部分连接的 Command 都是 Sleep,且 Time 字段高达几百甚至几千秒。

为什么会有这么多空闲连接长期滞留?

原因在于 MySQL 默认的空闲超时时间过长:

  • wait_timeout:非交互式连接(Java、Go、Python 等后端连接池建立的 TCP 连接)空闲多久后自动断开。
  • interactive_timeout:交互式连接(如终端客户端)空闲多久后自动断开。

MySQL 默认将这两个值设置为 28800 秒(整整 8 个小时)。

如果应用服务或者脚本打开了连接却没有调用关闭,或者网络发生闪断,MySQL 服务端会一直维持着这些 TCP socket 与内部线程,直到满 8 个小时才会主动回收。

调整连接超时时间

在生产环境中,建议主动缩短 wait_timeout,一般推荐调整为 300 到 600 秒(5 到 10 分钟):

SET GLOBAL wait_timeout = 600;
SET GLOBAL interactive_timeout = 600;

同时写入配置文件持久化(/etc/my.cnf):

[mysqld]
wait_timeout = 600
interactive_timeout = 600

配置后,客户端如果闲置超过 10 分钟无数据交互,服务端便会自动切断该连接,释放线程资源。

批量清理现场已有的堆积连接

修改参数只对后续新建连接生效,当前已经堆积在库里的空闲连接仍需手工释放。
可以通过查询拼接 SQL 批量清理:

-- 抓取空闲超过 300 秒的 Sleep 连接并生成 kill 指令
SELECT CONCAT('KILL ', id, ';') 
FROM information_schema.processlist 
WHERE command = 'Sleep' AND time > 300;

在 Linux 终端也可以通过管道直接批量执行:

mysql -u root -p -e "SELECT CONCAT('KILL ', id, ';') FROM information_schema.processlist WHERE command = 'Sleep' AND time > 300;" | grep "KILL" | mysql -u root -p

执行后,被占用的连接数配额会迅速回落。


坑四:代码里连接池泄露,连接借出去就再没还回来

如果在缩短超时时间并清理空闲连接后,连接数依然持续上涨,问题大概率出在业务代码的连接池使用方式上。

常见的问题代码形态如下:

1. 手动获取 Connection 缺少异常安全保证

在一些使用原生 JDBC 或自定义数据源工具类的老系统里:

// 错误示例:业务逻辑抛出异常会导致连接无法归还
Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM orders WHERE id = 1");
// 如果后续的业务处理逻辑抛出异常
// 下面的 close 代码将永远无法被执行
conn.close();

只要中间的业务处理抛出了未捕获的运行时异常,conn.close() 便被直接跳过。
在连接池(如 HikariCP、Druid 等)的管理机制下,conn.close() 实际是将连接对象归还给池子,并不会直接断开底层的 TCP socket。一旦归还失败,连接池就认为该连接一直被外界借用。
在高频接口被调用时,每次异常便吞掉一个连接,几分钟内就可以把连接池借空并向 MySQL 请求更多连接,直到上限。

规范写法应当始终采用 try-with-resources 结构:

try (Connection conn = dataSource.getConnection();
     PreparedStatement ps = conn.prepareStatement("SELECT * FROM orders WHERE id = ?")) {
    ps.setLong(1, orderId);
    try (ResultSet rs = ps.executeQuery()) {
        // 读取并处理数据
    }
} // 无论正常结束还是抛出异常,连接都会被安全归还给连接池

2. 开启连接池的泄露监控机制

在微服务集群环境下,仅凭 MySQL 端很难直接反查是哪个服务、哪行代码导致了泄露。
此时可以在应用配置中开启连接池的泄露检测。

以常用的 HikariCP 为例,可在配置文件中增加:

spring:
  datasource:
    hikari:
      # 连接借出超过 60 秒未归还,自动记录警告并打印完整堆栈
      leak-detection-threshold: 60000
      # 连接池最大连接数,微服务节点一般建议设为 10 到 20
      maximum-pool-size: 20
      # 最小空闲连接数
      minimum-idle: 5
      # 空闲连接存活时间
      idle-timeout: 300000
      # 连接最大生命周期
      max-lifetime: 1800000

配置好 leak-detection-threshold 后,一旦某个线程借出连接超过 60 秒没有归还,HikariCP 就会在日志中输出明确的报警:
Apparent connection leak detected on Thread [http-nio-8080-exec-5], exception will follow
紧随其后的是当初借出该连接的代码堆栈。根据堆栈信息定位具体文件与行号,修复工作就会非常直接。


坑五:慢 SQL 和事务未提交引发的连接雪崩

还有一种情况,代码里连接都正确关闭了,但连接数依旧飙高。
此时执行 SHOW PROCESSLIST;,排查列表里会看到大量状态为 Sending data、Sorting for group 或者 Updating 的活跃查询,此时 Sleep 连接反而不多。

这种情况多半是慢查询导致的连接堆积。

如果一个接口原本耗时仅为 10 毫秒,单服务节点只需要 5 个连接即可轻松承载日常请求。
一旦因为数据膨胀导致索引失效,或者执行了耗时很长的全表扫描,接口耗时猛增至数秒。
原本 10 毫秒即可释放的连接被长期占用,后续请求源源不断涌入,连接池只能不断向 MySQL 索取新连接,最终引发全链路打满。

同时,未提交的长事务还会引发锁等待链条:

-- 排查当前运行中的长事务
SELECT 
    trx_id, 
    trx_state, 
    trx_started, 
    TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS duration_sec,
    trx_mysql_thread_id,
    trx_query
FROM information_schema.innodb_trx
ORDER BY duration_sec DESC;

如果发现某个事务已经持续运行了数百秒,状态为 RUNNING,而后序数十个连接全部被阻塞在 LOCK WAIT,可以直接执行 KILL <trx_mysql_thread_id>; 终止源头长事务,下游等待的连接即可快速消化。


线上突发连接打满应急排查命令速查

遇到 Too many connections 报警时,按以下操作顺序快速定界:

# 1. 优先尝试通过本地管理端口或者 root 登入
mysql -u root -p -h 127.0.0.1 -P 33062

# 2. 如果无法登入,在 Linux 检查哪个客户端 IP 占用的 3306 连接最多
ss -ant '( sport = :3306 )' | awk '{print $5}' | cut -d: -f1 | sort | uniq -c | sort -rn | head -n 10

# 3. 成功登入后统计当前连接状态分布
mysql -u root -p -e "SELECT command, count(*) FROM information_schema.processlist GROUP BY command;"

# 4. 批量清理长时间处于空闲状态的连接
mysql -u root -p -e "SELECT CONCAT('KILL ', id, ';') FROM information_schema.processlist WHERE command = 'Sleep' AND time > 300;" | grep "KILL" | mysql -u root -p

# 5. 检查是否存在长事务阻塞锁资源
mysql -u root -p -e "SELECT trx_id, trx_state, TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS sec, trx_query FROM information_schema.innodb_trx WHERE TIMESTAMPDIFF(SECOND, trx_started, NOW()) > 30;"

# 6. 查看超时与连接数核心参数
mysql -u root -p -e "SHOW VARIABLES LIKE '%timeout%';"
mysql -u root -p -e "SHOW VARIABLES LIKE 'max_connections';"

把 admin_port 管理通道预先保留好,缩短 wait_timeout 空闲回收时间,在连接池开启泄露监控并合理规划连接配额。把这几层关卡守住,线上数据库就不会再被突发的连接风暴轻易击穿。

评论一下?

OωO
取消