适用场景

本文适用于线上业务访问 MySQL 时突然出现连接失败、接口大量报错、应用日志出现 Too many connections、连接池获取连接超时,或者监控中 MySQL Threads_connected 快速逼近 max_connections 的场景。

这类问题通常不是简单把 max_connections 调大就结束。连接数打满往往说明应用连接池、慢 SQL、锁等待、突发流量、长事务或连接泄漏中至少有一个环节异常。排查目标是先恢复服务,再找出连接被谁占用、为什么释放不掉。

现象描述

业务侧常见报错如下:

java.sql.SQLNonTransientConnectionException: Data source rejected establishment of connection, message from server: "Too many connections"
com.mysql.cj.jdbc.exceptions.MySQLNonTransientConnectionException: Too many connections
HikariPool-1 - Connection is not available, request timed out after 30000ms

MySQL 侧可能看到连接数已经接近上限:

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

关键字段看法:

  • max_connections:MySQL 允许的最大并发连接数。
  • Threads_connected:当前已建立连接数,接近上限时新连接会失败。
  • Max_used_connections:实例启动以来达到过的最高连接数,用来判断是否经常顶到上限。

可能原因

  • 应用连接池配置过大,多实例叠加后超过 MySQL 承载能力。
  • 连接池连接泄漏,代码获取连接后没有释放。
  • 慢 SQL 或锁等待导致连接长时间占用。
  • 上游流量突增,接口并发远超日常基线。
  • 定时任务、批处理、报表查询集中启动。
  • MySQL wait_timeout 过长,空闲连接迟迟不释放。
  • 数据库主机 CPU、磁盘 I/O 或网络异常,导致请求处理变慢。
  • 中间件、网关或容器滚动发布导致应用实例短时间重复建连。

排查思路

第一步先确认是不是连接数真的打满,而不是网络、账号权限或数据库宕机。第二步按来源统计连接,找出连接最多的应用主机和账号。第三步看连接状态,如果大量连接处于 Sleep,优先怀疑连接池或空闲连接回收;如果大量连接处于 QuerySending dataWaiting for ... lock,优先查慢 SQL、锁等待和资源瓶颈。第四步再决定是临时扩容连接数、杀连接、限流,还是回滚应用。

不要一上来直接重启 MySQL。重启会短暂恢复连接,但会丢失现场,也可能引发更大的业务中断。

常用命令

查看连接上限和当前连接数:

mysql -uroot -p -e "SHOW VARIABLES LIKE 'max_connections'; SHOW GLOBAL STATUS LIKE 'Threads_connected'; SHOW GLOBAL STATUS LIKE 'Max_used_connections';"

字段解读:

  • Threads_connected / max_connections 超过 80% 时需要关注,超过 95% 基本进入故障边缘。
  • Max_used_connections 如果长期等于 max_connections,说明连接上限经常被顶满。

按来源主机统计连接数:

mysql -uroot -p -e "SELECT SUBSTRING_INDEX(host, ':', 1) AS client_host, user, COUNT(*) AS conn_count FROM information_schema.PROCESSLIST GROUP BY client_host, user ORDER BY conn_count DESC LIMIT 20;"

字段解读:

  • client_host:连接来源主机,能快速定位是哪批应用实例。
  • user:数据库账号,能区分业务、报表、运维脚本等来源。
  • conn_count:该来源当前占用连接数。

按连接状态统计:

mysql -uroot -p -e "SELECT command, state, COUNT(*) AS cnt FROM information_schema.PROCESSLIST GROUP BY command, state ORDER BY cnt DESC LIMIT 20;"

字段解读:

  • Sleep 多:连接空闲但未释放,重点看连接池、空闲连接回收、wait_timeout
  • Query 多:正在执行 SQL,继续抓慢 SQL 和执行计划。
  • Waiting for ... lock 多:存在锁等待,继续查事务和锁。

查看当前执行时间最长的 SQL:

mysql -uroot -p -e "SELECT id, user, host, db, command, time, state, LEFT(info, 300) AS sql_text FROM information_schema.PROCESSLIST WHERE command <> 'Sleep' ORDER BY time DESC LIMIT 10;"

字段解读:

  • time:当前状态持续秒数,时间越长越可疑。
  • state:执行阶段,能判断卡在锁、发送数据、临时表还是其他阶段。
  • sql_text:SQL 片段,用来定位业务接口或任务。

查看 InnoDB 当前事务:

mysql -uroot -p -e "SELECT trx_id, trx_mysql_thread_id, trx_started, trx_state, trx_query FROM information_schema.INNODB_TRX ORDER BY trx_started LIMIT 10;"

字段解读:

  • trx_mysql_thread_id:对应 PROCESSLIST.id,可以关联到具体连接。
  • trx_started:事务开始时间,老事务可能持锁或拖慢 purge。
  • trx_query:事务当前 SQL,为空不代表没有持锁。

查看系统资源是否拖慢 MySQL:

top -H -p $(pidof mysqld)
iostat -x 1 5
ss -antp | grep ':3306' | awk '{print $1}' | sort | uniq -c

字段解读:

  • top -H:看 mysqld 线程是否 CPU 打满。
  • iostat %utilawait:磁盘繁忙或等待高会放大 SQL 耗时。
  • ss 状态:ESTAB 多代表已建立连接多,TIME-WAIT 异常多要看短连接风暴。

定位示例

一次线上接口告警中,应用日志大量出现:

HikariPool-1 - Connection is not available, request timed out after 30000ms
Too many connections

先查看 MySQL 连接状态:

mysql -uroot -p -e "SHOW VARIABLES LIKE 'max_connections'; SHOW GLOBAL STATUS LIKE 'Threads_connected';"

输出显示:

+-----------------+-------+
| Variable_name   | Value |
+-----------------+-------+
| max_connections | 800   |
+-----------------+-------+
+-------------------+-------+
| Variable_name     | Value |
+-------------------+-------+
| Threads_connected | 792   |
+-------------------+-------+

继续按来源统计:

mysql -uroot -p -e "SELECT SUBSTRING_INDEX(host, ':', 1) AS client_host, user, COUNT(*) AS conn_count FROM information_schema.PROCESSLIST GROUP BY client_host, user ORDER BY conn_count DESC LIMIT 10;"

发现 10.20.3.4510.20.3.46 两台报表服务各占用 250 多个连接。再看状态:

mysql -uroot -p -e "SELECT command, state, COUNT(*) AS cnt FROM information_schema.PROCESSLIST GROUP BY command, state ORDER BY cnt DESC;"

大量连接处于 Query / Sending data,说明不是纯空闲连接泄漏,而是 SQL 执行慢导致连接被占住。抓取长 SQL:

mysql -uroot -p -e "SELECT id, time, state, LEFT(info, 300) AS sql_text FROM information_schema.PROCESSLIST WHERE command <> 'Sleep' ORDER BY time DESC LIMIT 5;"

定位到报表任务在高峰期全表扫描订单明细表,单条 SQL 执行超过 180 秒。连接池最大连接数配置为 200,两个实例叠加后把 MySQL 连接几乎占满,在线业务无法新建连接。

修复方案

先做止血:

mysql -uroot -p -e "KILL 123456;"

123456 替换为确认无误的异常连接 PROCESSLIST.id。只杀明确归属异常任务的连接,不要批量盲杀所有连接。

如果业务已经大面积失败,可以临时提高连接上限:

SET GLOBAL max_connections = 1200;

这只是临时手段。调大前要确认 MySQL 主机内存足够,因为每个连接都会消耗线程、网络缓冲和会话内存。永久配置需要写入 MySQL 配置文件:

[mysqld]
max_connections=1200

然后在维护窗口重启 MySQL 生效。

针对根因修复:

  • 报表任务改到低峰期运行,或迁移到只读库。
  • 给慢 SQL 补充合适索引,避免全表扫描。
  • 限制报表应用连接池,例如单实例 maximumPoolSize 从 200 降到 30。
  • 设置合理的查询超时,避免异常 SQL 长时间占用连接。
  • 对高耗时接口做限流、分页、异步化或缓存。
  • 检查代码是否存在连接未关闭,重点看手写 JDBC、事务异常分支和批处理脚本。

连接池侧以 HikariCP 为例:

spring.datasource.hikari.maximum-pool-size=30
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.idle-timeout=600000
spring.datasource.hikari.max-lifetime=1800000

配置原则是所有应用实例的最大连接数总和不能超过 MySQL 可承载连接数,还要给 DBA、迁移任务、监控和突发流量保留余量。

预防措施

  • 监控 Threads_connectedMax_used_connections、连接使用率和连接失败数。
  • 对连接使用率设置分级告警,例如 70% 提醒、85% 警告、95% 紧急。
  • 定期统计各业务账号连接数,避免单个服务无限制占用。
  • 应用连接池配置纳入容量评审,发布新实例前计算总连接数。
  • 慢 SQL、锁等待、长事务要有独立告警。
  • 批处理和报表任务避开业务高峰,并限制并发。
  • 为核心业务和离线任务使用不同数据库账号,方便隔离和限流。
  • 建立故障期间保留现场的流程,先采集 PROCESSLIST、事务、锁和系统负载,再做重启或杀连接。

总结

MySQL Too many connections 的直接原因是连接数达到上限,但真正根因通常在应用连接池、慢 SQL、锁等待、突发流量或连接泄漏。排查时先确认连接使用率,再按来源主机、账号、状态和执行时间逐层收敛。止血可以临时杀异常连接、限流或调整连接上限,但长期修复必须回到连接池容量、SQL 性能、任务调度和监控告警上。