MySQL连接数过多怎么办?查看连接状态与优化数据库配置

"Too many connections"大概是 MySQL 最常见的报错之一。它的表现很直接:应用突然连不上数据库,日志里刷一片这个错误,业务开始大面积超时。但很多人的第一反应是"把 max_connections 调大到 2000",改完重启,安静两天又复发了——因为连接数爆掉从来不是原因,而是结果。连接堆积的背后通常是慢查询、连接泄漏或者连接池配置过大这三种问题之一。

这篇文章讲三件事:怎么把当前的连接状况看清楚(哪些是真正在干活、哪些只是在睡觉)、连接池该怎么配才不会反噬数据库、以及线上真的报 "Too many connections" 时,在还能登录和已经登录不进去两种情况下分别怎么救。文末会给出根因排查的顺序和长期治理的建议。

一、连接数的几个概念,先分清

MySQL 里跟连接相关的数字有好几个,含义完全不同,混着看会得出错误结论。

max_connections 是服务器端允许的最大连接数,默认通常是 151,生产环境一般会调到几百到一千多。它是一条硬限制,超过就报错。

Threads_connected 是当前已经建立的连接总数,包括空闲连接。应用侧连接池里维持的空闲连接也算在内。

Threads_running 是当前真正在执行 SQL 的连接数,也就是真正占用 CPU 和 IO 的那部分。这个数字远比 Threads_connected 重要。

Max_used_connections 是开机以来连接数的历史峰值,用来判断 max_connections 是否够用。

一个典型的健康状态是:Threads_connected 几百,Threads_running 只有个位数或十几。反过来,如果 Threads_connected 和 Threads_running 都很高且接近 max_connections,那就是真的堵住了。

查看方式:

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

还有一个容易忽略的点:MySQL 会给 SUPER 权限用户预留一个额外连接。也就是说,max_connections = 500 时,普通用户最多连 499 个,第 500 个位置留给拥有 SUPER 或 CONNECTION_ADMIN 权限的账号。这个是应急登录的救命通道,后面会讲到。

二、把当前连接看清楚:SHOW PROCESSLIST

基本用法

SHOW PROCESSLIST;                  -- 只看前 100 条(受 Info 长度限制)
SHOW FULL PROCESSLIST;             -- 完整显示,能看到完整 SQL

输出的关键列:

列 含义
Id 连接 ID,杀连接时用
User / Host 来自哪个账号、哪台机器哪个端口
db 当前默认库
Command 连接当前在做什么(Sleep、Query、Connect 等)
Time 处于当前状态已经多少秒,这是最重要的排障字段
State 执行阶段,如 Sending data、Locked、Copying to tmp table
Info 正在执行的 SQL,SHOW FULL PROCESSLIST 才能看全

等价的 SQL 写法,好处是可以过滤和排序:

SELECT id, user, host, db, command, time, state, LEFT(info, 100) AS sql_text
FROM information_schema.PROCESSLIST
WHERE command != 'Sleep'
ORDER BY time DESC
LIMIT 20;

这条语句排查时几乎必用:排除掉 Sleep,按耗时倒序,直接看到最该处理的那几条。

快速统计:有多少在睡、有多少在跑

SELECT command, COUNT(*) AS cnt, MAX(time) AS max_time
FROM information_schema.PROCESSLIST
GROUP BY command
ORDER BY cnt DESC;

再按来源机器聚合,能快速定位是哪台应用服务器在大量建连:

SELECT SUBSTRING_INDEX(host, ':', 1) AS ip, COUNT(*) AS cnt
FROM information_schema.PROCESSLIST
GROUP BY ip
ORDER BY cnt DESC;

performance_schema 提供的更细视角

如果开启了 performance_schema(默认开启),可以看到语句级别的统计,不用抓现场也能知道历史上什么 SQL 最慢:

-- 按 schema 维度看语句执行统计
SELECT schema_name, SUM(count_star) AS exec_count,
       ROUND(SUM(sum_timer_wait)/1000000000000, 2) AS total_sec
FROM performance_schema.events_statements_summary_by_digest
WHERE schema_name IS NOT NULL
GROUP BY schema_name
ORDER BY total_sec DESC
LIMIT 10;

按单条 SQL 模板排序(去掉了具体的参数值,所以能聚合出"同一类 SQL"):

SELECT DIGEST_TEXT AS sql_tpl, COUNT_STAR AS exec_cnt,
       ROUND(SUM_TIMER_WAIT/1000000000000, 2) AS total_sec,
       ROUND(AVG_TIMER_WAIT/1000000000, 2) AS avg_ms,
       ROUND(MAX_TIMER_WAIT/1000000000, 2) AS max_ms
FROM performance_schema.events_statements_summary_by_digest
WHERE schema_name = 'your_db'
ORDER BY total_sec DESC
LIMIT 10;

这个视图最大的价值是不用开着慢查询日志也能看到历史最耗时的语句类型,而且带平均值、最大值、扫描行数等维度。注意 DIGEST_TEXT 有长度限制,超长 SQL 会被截断。

三、连接状态(State)怎么看

State 字段告诉你这条连接此刻卡在哪一步,比看 SQL 文本更直接地指向问题类型。

状态 含义 通常意味着
Sleep 空闲,没在执行 SQL 连接池维持的空闲连接,太多说明池子配大了或没回收
Query 正在执行 正常状态
Locked 等待表级锁(MyISAM 常见) InnoDB 上少见,出现要查是否有 LOCK TABLES
Sending data 正在读取并发送数据 最常见也最容易误解,见下面说明
Sorting result 排序中 可能没走索引排序
Copying to tmp table 正在建内存或磁盘临时表 GROUP BY / DISTINCT / 子查询导致,可能落盘
Copying to tmp table on disk 临时表已落盘 比上一个更糟,说明超出临时表内存上限
Waiting for table metadata lock 等元数据锁 通常有未提交事务或长 DDL 挡住了
statistics / preparing 优化器阶段 复杂查询在生成执行计划
Opening tables 打开表 table_open_cache 可能偏小

几个容易判断错的点:

Sending data 不等于"正在往客户端发数据"。 它的实际含义是从存储引擎读取数据、进行过滤处理后的阶段,包含了绝大部分的 IO 和 CPU 消耗。一条慢查询卡在 Sending data 几十秒,说明它正在扫大量数据,而不是网络慢。

Sleep 太多不一定是问题,但要看 Time。 连接池本来就会保留一些空闲连接。问题是:如果几百个 Sleep 连接的 Time 都是几千秒甚至几万秒,说明这些连接已经很久没被使用却没被释放——典型的连接泄漏或者连接池配置不当(比如没配 maxLifetime、或者最小空闲数设得过大)。

Waiting for table metadata lock 是隐形杀手。 一个忘记提交的事务(比如有人在命令行开了 BEGIN 没 COMMIT)持有元数据锁,后面所有对这个表的 DDL 甚至部分查询都会排队。查法是:

SELECT * FROM information_schema.innodb_trx ORDER BY trx_started LIMIT 10;

找到运行时间长的 trx_mysql_thread_id,用 KILL 处理前先确认它的业务影响。

处理连接时的风险提示:KILL 会中断正在执行的事务,可能导致应用报错或数据处于应用层的中间状态。执行前务必确认该连接的来源(Host)与 SQL 内容,并优先在业务低峰操作。 杀查询用 KILL QUERY(只终止当前语句,保留连接),比 KILL(直接断开连接)温和:

KILL QUERY 12345;   -- 只杀掉这条 SQL
KILL 12345;         -- 断开整个连接

批量生成杀连接的语句时,先生成再人工确认,不要直接拼成一条管道执行:

SELECT CONCAT('KILL QUERY ', id, ';') AS kill_sql
FROM information_schema.PROCESSLIST
WHERE command != 'Sleep' AND time > 60 AND user = 'app_user';

四、根因一:连接池配置过大

这是最常见的根因,而且往往是"好心办坏事"。

设想一个场景:应用有 20 个实例,每个实例的 HikariCP 配了 maximumPoolSize = 50。理论上高峰期数据库连接数可以达到 1000。如果 max_connections 只有 500,就会直接爆掉。更糟的是,即使 max_connections 调到 2000,1000 个连接里真正干活的也许只有十几个——大量并发线程只会增加锁争用和上下文切换,吞吐量反而下降。

数据库连接数不是越大越好,业界常见的经验是:连接池总数应该明显小于数据库的 CPU 核数乘以一个较小的系数,活跃并发控制在几十个以内就能跑满一个不错的数据库。

HikariCP 配置示例

# 连接池大小:按实例数分摊,不要一个实例就吃掉全部配额
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5

# 连接最大存活时间,必须小于数据库 wait_timeout
spring.datasource.hikari.max-lifetime=1800000        # 30 分钟,单位毫秒

# 获取连接的超时,快速失败好过无限排队
spring.datasource.hikari.connection-timeout=30000

# 空闲连接回收阈值
spring.datasource.hikari.idle-timeout=600000

# 连接有效性检测
spring.datasource.hikari.validation-timeout=5000

几个参数的要点:

max-lifetime 必须比数据库端的 wait_timeout 短。 否则数据库会先把连接掐掉,应用拿到一个已断开的连接再报错。max-lifetime 建议设为 wait_timeout 的 1/2 到 2/3,且不要低于 30 秒(HikariCP 有这个下限要求)。

minimum-idle 不建议设得和 maximum-pool-size 一样大。 那等于池子永远是满的,即使在业务低峰也占着数据库几百个连接。设成最大值的 1/4 左右比较合理。HikariCP 官方甚至建议把 minimumIdle 设成与 maximumPoolSize 相同来避免伸缩开销,但那是针对单实例的;多实例部署时要算总量,宁可保守。

connection-timeout 要设置。 默认 30 秒。如果不设,连接耗尽时应用线程会无限等待,把线程池也拖垮,形成级联故障。快速失败并给出降级,比全线卡死好。

Druid 配置示例

spring.datasource.druid.max-active=20
spring.datasource.druid.min-idle=5
spring.datasource.druid.initial-size=5
spring.datasource.druid.max-wait=30000           # 获取连接最大等待时间
spring.datasource.druid.time-between-eviction-runs-millis=60000
spring.datasource.druid.min-evictable-idle-time-millis=300000
spring.datasource.druid.test-while-idle=true
spring.datasource.druid.validation-query=SELECT 1
spring.datasource.druid.remove-abandoned=true
spring.datasource.druid.remove-abandoned-timeout=180
spring.datasource.druid.log-abandoned=true

Druid 的 remove-abandoned 系列是排查连接泄漏的利器:开启后,连接被借出超过指定秒数仍未归还,会被强制回收并打日志(log-abandoned 会打印借用时的堆栈,直接定位到代码行)。注意这个功能主要用于排查,生产长期开启有误杀长事务的风险,建议定位完问题就关掉。

连接池总量的算法

一个实用的估算方式:

单实例池大小 = 数据库能承受的活跃连接总数 / 应用实例数

先把数据库端的 max_connections 按业务需要定下来(比如 500),再除以应用实例数(比如 20),得到每实例 25,再留些余量设成 20。应用扩容时要同步往下调单实例的池大小,这一点经常被忘记——加了几台机器之后连接数就爆了。

五、根因二:慢查询堆积

这类问题的特征是:Threads_running 很高,SHOW PROCESSLIST 里大量连接处于 Sending data、Copying to tmp table、Sorting result,且 Time 持续累积。

逻辑链条是:一条 SQL 从 10 毫秒变成 10 秒 → 同样的 QPS 下需要 1000 倍的并发连接才能维持吞吐 → 连接池被借空 → 新请求排队 → 应用线程也排队 → 连接数爆掉,同时业务全面超时。

所以"连接数不够"往往只是慢查询的下游症状。 这时候把 max_connections 调大,只会让更多慢查询同时跑,进一步拖垮磁盘和 CPU,恢复时间更长。

正确的处理是:

-- 先看谁在跑、跑了多久
SELECT id, user, host, db, time, state, LEFT(info, 200) AS sql_text
FROM information_schema.PROCESSLIST
WHERE command != 'Sleep' AND time > 5
ORDER BY time DESC;

然后针对最耗时的几条 SQL 做执行计划分析(详见第 063 篇)。紧急止血时,确认影响后杀掉最老的几条长查询。

判断是不是慢查询导致的,一个很快的指标是看 Threads_running 的趋势:

for i in $(seq 1 10); do
  mysql -uroot -p'xxx' -N -e "SHOW GLOBAL STATUS LIKE 'Threads_running'" 2>/dev/null
  sleep 1
done

风险提示:把密码写在命令行里会被 ps 看到,也会进 shell history。建议使用 ~/.my.cnf 配置 [client] 段的账号密码并 chmod 600,或改用 mysql_config_editor 存储凭据。

六、根因三:连接泄漏

连接泄漏的表现是:业务量没涨,但 Threads_connected 单调上升,且大量是 Sleep 状态、Time 很大。

常见原因:

  • 代码里借了连接没在 finally 里归还(老式 JDBC 直连代码最常见)
  • 用了连接池但某些分支路径没有 close(),比如异常提前 return
  • 开启了事务但忘记提交或回滚,连接一直被占用
  • 长轮询、定时任务持有连接时间过长
  • 应用实例发生了网络闪断,池子里的连接变成了"僵尸连接",数据库端还留着

定位手段:先用前面的"按 IP 聚合"找出是哪台机器,再到那台机器上用 Druid 的 log-abandoned 或 HikariCP 的泄漏检测阈值定位:

# HikariCP 泄漏检测:连接借出超过此毫秒数就打警告日志(0 表示关闭)
spring.datasource.hikari.leak-detection-threshold=60000

leak-detection-threshold 打出的日志会包含借出连接时的堆栈,直接指向代码位置。它有性能开销,建议只在排查期间开启。

僵尸连接的处理:这类连接数据库端认为还活着,但应用已经用不了了。靠 wait_timeout 自动清理是最省事的办法。

七、超时参数:wait_timeout 与 interactive_timeout

SHOW GLOBAL VARIABLES LIKE '%timeout%';

两个参数的区别:

wait_timeout:非交互式连接(绝大多数应用连接)的空闲超时秒数,超过就断开。默认通常是 28800 秒(8 小时)。

interactive_timeout:交互式连接(比如你在命令行 mysql 客户端里操作)的空闲超时。

这两个值建议在配置文件里显式设成一致且合理的值,比如 600 到 1800 秒。8 小时对一个有几百个连接的服务来说太长了——泄漏的连接要 8 小时才被回收,而在这 8 小时里它们一直占着配额。

[mysqld]
wait_timeout        = 600
interactive_timeout = 600

注意 wait_timeout 是会话级变量,改全局变量后只影响之后新建的连接,已经存在的连接不会变。这也是为什么改完要重启或者等连接自然轮换。

另外要提醒:把 wait_timeout 调小后,应用连接池的 maxLifetime 一定要同步改小,否则会出现"数据库已断开、应用还在用"的情况,表现为应用报 Communications link failure 或 Connection reset by peer。

八、"Too many connections" 的应急处理

情况一:还能登录(有 SUPER 权限)

MySQL 为 SUPER(8.0 里是 CONNECTION_ADMIN)权限用户预留了一个额外连接。用 root 登录通常还能进去:

mysql -uroot -p

进去之后按这个顺序处理:

-- 1. 看总体情况
SHOW GLOBAL STATUS LIKE 'Threads_%';
SHOW GLOBAL VARIABLES LIKE 'max_connections';

-- 2. 找最该杀的连接
SELECT id, user, host, time, state, LEFT(info, 150) FROM information_schema.PROCESSLIST
WHERE command != 'Sleep' AND time > 30 ORDER BY time DESC LIMIT 20;

-- 3. 确认影响后逐个 KILL QUERY
KILL QUERY 12345;

-- 4. 如果确认是配置不够,可以临时调大(重启后失效)
SET GLOBAL max_connections = 1000;

SET GLOBAL 是临时的,重启后回到配置文件的值。要永久生效必须改配置文件:

[mysqld]
max_connections = 1000

改完重启:

# RHEL/CentOS/Rocky
sudo systemctl restart mysqld
# Debian/Ubuntu
sudo systemctl restart mysql

重要提醒:临时调大 max_connections 只是给排查争取时间,不是解决方案。 如果根因是慢查询,调大之后会有更多慢查询同时跑,磁盘和 CPU 更容易被打满,故障反而加重。正确做法是调大后立即查根因,处理完再把值调回去。

情况二:完全登录不进去

如果预留连接也被占满(比如应用账号就有 SUPER 权限),常规登录会直接报 "Too many connections"。

可以试的办法,按推荐顺序:

1. 用 Unix socket 直连,并绕过 TCP 端口的连接限制。 走 socket 不走 TCP,有时能避开被占满的监听队列:

mysql -uroot -p -S /var/lib/mysql/mysql.sock

2. 从应用侧先断流。 停掉部分应用实例(保留一到两个用于验证),数据库端连接会随之释放。这往往比在数据库端杀连接更安全可控,因为你清楚地知道停了什么。

3. 在数据库端批量清理 Sleep 连接。 如果能以 SUPER 身份登录,先只杀掉长时间空闲的:

SELECT CONCAT('KILL ', id, ';')
FROM information_schema.PROCESSLIST
WHERE command = 'Sleep' AND time > 300;

先把生成的语句看一遍再执行,不要直接管道给 mysql 执行。 杀 Sleep 连接的业务影响相对小,但仍要确认这些不是长事务所在的连接(查 innodb_trx 确认)。

4. 关于用 gdb 在线修改 max_connections 的思路。 网上流传的做法是用 gdb -p -ex "set max_connections=2000" -batch 直接改内存里的变量值,声称不用重启。这里必须明确提醒:这个操作在生产环境属于高危行为。gdb attach 会暂停进程,可能造成秒级到分钟级的业务中断;不同版本的变量名和符号是否可用无法保证;修改内存值不落盘,行为不可预期,甚至可能直接导致 mysqld 崩溃。除非你已经做好主从切换、能接受实例挂掉、并且有 DBA 现场把关,否则不要尝试。 更稳妥的替代方案是先切到从库、或者在维护窗口重启实例。

5. 最后兜底:重启实例。 重启能清空所有连接,但代价是未提交事务回滚、InnoDB 崩溃恢复需要时间、连接池全部重建后可能有瞬时压力。重启前确认有可用的备份与主从切换方案。

应急后的复盘清单

  • 记录故障发生时的 Threads_connected / Threads_running 峰值与 max_connections 值
  • 找出当时最耗时的 SQL 模板(performance_schema 的 digest 视图)
  • 确认是连接池配置、慢查询还是泄漏,并给出对应的整改项
  • 检查应用实例数最近是否有变化(扩容导致的池大小未同步调整是高频原因)
  • 给 Threads_connected 加告警,阈值设在 max_connections 的 70% 到 80%

常见问题

max_connections 设多少合适?

没有通用答案,但有个反推方法:先确定数据库能承受的活跃并发(经验上不超过 CPU 核数的 2 到 4 倍),再乘以应用实例数得到池子总量,最后 max_connections 在池子总量基础上留 50% 余量给管理连接、备份连接和突发。比如 20 个实例 × 20 连接 = 400,max_connections 设 600 到 800。设成 5000 是没有任何意义的,只会掩盖问题。

Threads_connected 很高但 Threads_running 很低,有问题吗?

不一定是故障,但值得优化。这说明大量连接处于空闲状态,通常是连接池 minimum-idle 设得过大,或者实例数多但每实例池子不小。虽然不消耗 CPU,但每个连接都要占内存(排序缓冲等),且离上限更近。建议收紧池子配置。

为什么改了 wait_timeout 没效果?

wait_timeout 是会话级变量,改全局变量只影响之后新建立的连接。已有的连接沿用建连时的值。要么等连接轮换,要么配合连接池的 maxLifetime 让它主动重建,最直接的是重启数据库实例。

Sleep 连接要不要清理?

看 Time。几百个 Sleep 但 Time 都是几十秒,那是连接池的正常空闲连接,不用管。如果有大量 Time 上万秒的 Sleep,说明连接没被正确回收,要么给 wait_timeout 设个合理值(比如 600 秒)让数据库自动回收,要么查应用的连接归还逻辑。

连接池报 "Connection is not available, request timed out" 是什么问题?

池子里的连接都被借出且超过了 connection-timeout。原因有两层:一是池子太小而并发太高;二是连接被借出后长时间不归还(慢查询或泄漏)。先看数据库端 Threads_running 判断是慢查询还是泄漏,再决定是扩容池子还是优化 SQL。

应用扩容后连接数爆了,怎么快速处理?

先把新加的实例下线几台,或者临时调低单实例的 maximum-pool-size(多数连接池支持动态改)。长期方案是把连接池大小纳入配置中心管理,实例数变化时自动按比例调整,并把 max_connections 与实例数的乘积纳入容量规划检查。

总结

  • 分清三个数字:max_connections 是上限,Threads_connected 含空闲连接,Threads_running 才是真正在干活的——判断数据库是否真的堵住,看 Threads_running 和 SHOW PROCESSLIST 里的 Time 倒序。
  • "Too many connections" 是结果不是原因:三大根因分别是慢查询堆积(表现为大量 Sending data/Copying to tmp table)、连接池配置过大、连接泄漏(表现为 Time 极大的大量 Sleep)。先定位根因再动手。
  • 连接池要按实例数反推:单实例池大小 ≈ 数据库可承受活跃连接 ÷ 应用实例数;maxLifetime 必须小于数据库 wait_timeout(建议取 1/2 到 2/3),否则会出现"数据库已断开、应用还在用"的诡异报错。扩容实例时必须同步调小单实例池大小。
  • wait_timeout 建议显式设成 600 到 1800 秒,不要留默认的 28800;它是会话级变量,改完全局只影响新连接。同时 Druid 的 remove-abandoned 和 HikariCP 的 leak-detection-threshold 是定位泄漏的利器,但都有开销,排查完记得关掉。
  • 应急有两条路:还能登录就用 SUPER 预留连接进去,先杀长查询再临时 SET GLOBAL max_connections;登不进去就从应用侧断流减实例,或用 socket 直连。gdb 在线改内存变量属于高危操作,可能导致进程暂停甚至崩溃,除非已准备好主从切换,否则不要在生产上试。
  • 临时调大连接数只是争取时间:处理完根因后要把值调回去,并给 Threads_connected 加告警(阈值设在 max_connections 的 70% 到 80%)。所有 KILL 操作前先确认连接来源与 SQL 内容,优先用 KILL QUERY 而非 KILL。