“Too many connections”这个报错,凡是跟 MySQL 打过交道的人应该都不陌生。尤其是在业务高峰期、活动大促、或者某个定时任务突然抽风的时候,数据库连接数瞬间被打满,应用层一堆报错,后台日志哗啦啦地刷屏。这个时候很多人的第一反应是:连接数怎么这么快就满了?到底是谁占用了连接?max_connections 是不是该调大了?
先说结论:连接数打满,不一定就是 max_connections 太小。多数情况下是连接没被正确释放、连接池参数和实际负载不匹配、或者某个会话卡死在那里长期不退出。盲目的把 max_connections 调大,往往治标不治本,甚至会让数据库在内存压力下更快崩溃。
这篇文章不打算讲太深奥的性能调优理论,单纯围绕“连接数”这个主题,把我这些年排查连接数问题用到的查询语句、配置思路、以及踩过的坑一次性整理出来。无论你是刚接触 MySQL 的开发者,还是需要独立维护数据库的运维,这篇文章里的内容基本都能直接用到。
1. 连接数打满到底有多痛:一次现场排障的复盘
先说一个我自己经历过的案例。某个业务模块在每天凌晨的结算任务里会批量调用接口,正常情况下几十个连接就扛得住。某天上线了一个新功能,结果第二天凌晨数据库直接报 ERROR 1040: Too many connections,应用层所有需要访问数据库的请求瞬间全挂。更麻烦的是,因为连不上数据库,连排查用的客户端都登录不进去。
当时的第一反应就是:把 max_connections 调大,先恢复业务。但问题在于,你怎么用 MySQL 客户端连进去执行 SET GLOBAL max_connections = 1000?答案是,哪怕连接数满了,MySQL 一般还是会为超级权限账号(root)预留一个连接通道,这个是由 extra_max_connections 和超级账号的权限机制决定的。实际执行时,用 root 登录通常能成功,赶紧调大参数,业务恢复,然后再慢慢查是谁把连接占满了。
事后分析发现,罪魁祸首是新上线的功能在某个业务分支里忘记关闭数据库连接,代码里只是执行完查询就算完事,连接池里的连接只借不还,最终把连接数堆满了。这个案例说明两件事:连接数打满不只是配置问题,更多时候是应用代码或者连接池使用方式的问题;排查连接数问题,需要一套完整的查询手段,而不是光看一个数字。
1.1 连接数打满时,业务侧到底是什么表现
连接数打满时,应用层最常见的就是 SQLException: Data source rejected establishment of connection, message from server: "Too many connections" 这类异常。Java 的 HikariCP、Druid,Go 的 database/sql 连接池,Python 的 SQLAlchemy,表现都类似:拿不到连接,请求超时,然后被上层重试机制放大了请求量,导致雪崩。
数据库侧的表现为:新的连接请求被拒绝,但已经建立的连接不受影响。这里有个认知误区,很多人以为连接数满了之后所有请求都会立刻失败,实际上已经处于 Sleep 状态的老连接仍然占着位置,新连接进不来。这也是为什么连接池场景下出现连接数打满时,你会在 processlist 里看到一大堆 Sleep 状态的连接。
1.2 哪些行为最容易把连接数打满
根据我碰到的案例,连接数被打满一般集中在下面几种情况:
- 应用代码里获取连接后没有在 finally 或 defer 中释放,连接泄漏。这是最常见的,尤其是老项目里手写 JDBC 代码,
Connection用完不 close,连接池中的连接被逐渐耗尽。 - 连接池最大连接数配置过大,多个服务实例乘以池大小之后,总量远超数据库的 max_connections。比如数据库设 500,你有 20 个服务实例,每个实例连接池配 50,理论上就需要 1000,直接超了。
- 慢查询堆积导致连接占用时间过长。一个查询跑 30 秒,连接就占用 30 秒,如果并发上来,连接数自然飙升。
- 空闲连接没有及时回收。MySQL 默认的
wait_timeout是 8 小时,如果应用侧连接池的 idle 超时设置比数据库还长,连接就会一直挂在那边。 - 登录风暴,比如应用重启后所有服务实例同时去连数据库,瞬间建立大量连接。
理解了这些背景,再来看具体的查询和配置方法,才有针对性。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 查询连接数:状态变量、系统表、进程列表怎么选
很多文章一上来就让你执行 SHOW STATUS LIKE 'Threads_connected',这句话没错,但只靠它是不够的。你不仅要看当前连接数,还要看历史峰值、当前正在执行的线程数、连接来源、以及哪些连接在干什么。下面把常用的几条命令按场景讲清楚。
2.1 三条最基础的状态命令:Threads_connected、Max_used_connections、Threads_running
先看这三条:
sql复制SHOW GLOBAL STATUS LIKE 'Threads_connected';
SHOW GLOBAL STATUS LIKE 'Max_used_connections';
SHOW GLOBAL STATUS LIKE 'Threads_running';
Threads_connected:当前有多少个连接(包括 Sleep 状态的空闲连接)。Max_used_connections:MySQL 启动以来,同时使用的连接数历史峰值。这个值很有参考意义,如果它离 max_connections 还很远,说明当前打满可能只是瞬间的突发;如果它经常顶到 max_connections,说明上限确实不够用。Threads_running:当前正在执行查询的线程数,也就是非 Sleep 状态的连接。这个值才是真正反映数据库并发负载的指标。
实际操作中你会发现,连接数打满的时候 Threads_connected 非常高,但如果 Threads_running 很低,说明绝大多数连接都是空闲挂着的,那就是连接泄漏或者回收机制没生效。
2.2 用 information_schema.processlist 查看连接明细
状态变量只告诉你“有多少”,不能告诉你“都是谁”。要查明细,用 processlist:
sql复制SELECT id, user, host, db, command, time, state, info
FROM information_schema.processlist
ORDER BY time DESC;
这个表里每个字段都值得看:
id:连接线程 ID,必要时可以用KILL id杀掉。user:连接使用的账号。如果大量连接都是同一个业务账号,那么就是这个业务模块的问题。host:连接来自哪个 IP 和端口。如果是应用服务器 IP,属于正常;如果出现了奇怪的 IP,就得考虑是不是被入侵了(这种情况我遇到过,连接数打满是因为有人拿数据库爆破)。db:当前连接的默认数据库,如果为空说明这个连接还没选择数据库。command:连接当前的状态。Sleep表示空闲;Query表示正在执行查询;Connect表示正在建立连接。time:该连接处于当前状态持续了多少秒。state:更细粒度的执行状态,比如Sending data、Waiting for table metadata lock等。info:正在执行的 SQL 语句(如果当前是 Sleep 则为 NULL)。
一个很实用的查询,直接把连接按 user、host、command 分组统计:
sql复制SELECT user, host, db, command, COUNT(*) AS cnt
FROM information_schema.processlist
GROUP BY user, host, db, command
ORDER BY cnt DESC;
这样一眼就能看出:是某个业务账号的空闲连接太多,还是某个 IP 在疯狂建连,或者是有大量查询在执行。
2.3 只看 Sleep 状态的连接,判断是不是连接泄漏
连接泄漏的典型特征就是大量的 Sleep 连接长期存在。可以这样查:
sql复制SELECT COUNT(*) AS total,
SUM(command = 'Sleep') AS sleep_count
FROM information_schema.processlist;
如果 sleep_count 占了 total 的七八成以上,而且这些 Sleep 连接的 time 字段很大(比如几百秒甚至几千秒),那基本可以断定是连接泄漏了。进一步定位到具体来源:
sql复制SELECT user, host, COUNT(*) AS cnt
FROM information_schema.processlist
WHERE command = 'Sleep'
GROUP BY user, host
ORDER BY cnt DESC;
比如结果里全是 app_user@192.168.1.10 这种,那就说明 192.168.1.10 这台机器上的应用肯定有连接没有归还。
2.4 别忘了 performance_schema 里的连接审计信息
MySQL 5.7 以上,performance_schema 里也有一些和连接相关的表。比如 performance_schema.threads 可以看到每个线程的属性,performance_schema.events_statements_current 可以看到连接当前执行的 SQL。不过日常排查,information_schema.processlist 已经够用了。只有在需要查看某个连接的完整 SQL 历史、或者分析连接被拒绝的原因时,才需要去翻 performance_schema。
还有一个状态变量在 5.7 之后特别有用:
sql复制SHOW GLOBAL STATUS LIKE 'Connection_errors_max_connections';
这个值记录了因为连接数达到上限而被拒绝的次数。如果这个数字一直在涨,说明你的 max_connections 确实频繁被打满,不只是偶发现象。
3. max_connections 的配置逻辑:别只拍脑袋改数字
如果说查询连接数是诊断环节,那配置 max_connections 就是治疗环节。很多人改这个参数就是觉得“不够就加”,但加到多少合适,其实需要考虑几个因素。
3.1 官方默认值和常见配置范围
MySQL 的 max_connections 默认值是 151(5.7 和 8.0 都是)。这个数字对一个小型应用来说够用,但对稍微有点并发的业务来说肯定不够。常见的线上配置在 300~1000 之间。但要注意,并不是配置越大越好,每个连接都会消耗内存和线程资源。
查看当前配置:
sql复制SHOW VARIABLES LIKE 'max_connections';
SHOW VARIABLES LIKE 'max_user_connections';
max_user_connections 是限制单个用户的最大连接数,默认是 0,表示不限制。如果担心某个业务账号把整个数据库的连接耗尽,可以用它单独限制一下。
3.2 估算合理值的思路:内存约束和业务并发
每个 MySQL 连接会占用一定的线程栈空间和内存缓冲区。实际的内存占用跟 sort_buffer_size、join_buffer_size、read_buffer_size、max_allowed_packet 等参数有关。简单估算时,可以按每个连接 1MB~5MB 来计算,如果业务查询比较重,可能需要按 10MB 以上估算。
举个例子,如果数据库服务器内存是 32GB,innodb_buffer_pool_size 配了 20GB,操作系统预留 4GB,加上其他开销,留给连接的内存大概在 6GB~8GB 之间。按每个连接平均 5MB 算,max_connections 配 1000 问题不大,配 2000 就比较危险了。
另一个角度是业务并发量。假设应用层的连接池总共需要 500 个连接,那么 max_connections 至少要在 500 的基础上留出约 20%~30% 的余量,用于处理后台任务、运维查询、临时连接等。所以配 700 左右比较合理。如果配合 max_user_connections 给不同的业务账号做隔离,会更好管理。
3.3 在线修改和持久化:SET GLOBAL 与 SET PERSIST 的区别
临时修改连接数上限,只需要:
sql复制SET GLOBAL max_connections = 800;
这个操作会立即生效,但 MySQL 重启后失效。5.7 之前的版本,你还需要手动改 my.cnf;5.7 之后可以写到配置文件;8.0 开始推荐用 SET PERSIST:
sql复制SET PERSIST max_connections = 800;
SET PERSIST 会把参数写入 mysqld-auto.cnf 文件,重启后依然生效。这个细节我见过不少人踩坑——线上用 SET GLOBAL 改完,觉得万事大吉,结果机房一重启,配置又变回原来的值,连接数又在高峰期打满了。
3.4 配合哪些参数一起调:wait_timeout 与 thread_cache_size
调整 max_connections 的同时,有两个参数值得联动检查。
wait_timeout:非交互连接的空闲超时时间,默认 28800 秒(8 小时)。如果应用服务器的连接池里有大量空闲连接,而且因为代码问题没有正确返还,那么长超时时间会加剧连接堆积。可以先临时把 wait_timeout 调低一点,比如 300 秒,让空闲连接被数据库主动断开,缓解连接数压力:
sql复制SET GLOBAL wait_timeout = 300;
SET GLOBAL interactive_timeout = 300;
注意 interactive_timeout 是针对交互式客户端的,两个一起改更稳妥。但这里有个坑:如果你改小了 wait_timeout,应用侧连接的存活时间变短,连接池需要更频繁地重建连接,连接池本身的配置要能适应这种变化。所以线上调参不能只改一头。
thread_cache_size:这个参数控制 MySQL 缓存的线程数,默认通常是 9 或者 16。连接数较大的场景下,适当调大 thread_cache_size 可以减少频繁创建、销毁线程的开销。比如:
sql复制SET GLOBAL thread_cache_size = 100;
不过这个参数对连接数上限本身没有直接影响,只是优化连接创建和销毁的效率。
4. 连接数告急时的完整排查链路
接下来写一写连接数已经很高、甚至已经打满的情况下,我的排查思路。这套流程我总结成一个固定的操作顺序,每次遇到连接数问题就直接按这个来,不容易乱。
4.1 第一步:确认当下状态,判断是“持续满”还是“瞬间满”
先用最快速的方式确认状态:
sql复制SHOW GLOBAL STATUS LIKE 'Threads_connected';
SHOW GLOBAL STATUS LIKE 'Max_used_connections';
SHOW GLOBAL STATUS LIKE 'Connection_errors_max_connections';
如果 Connection_errors_max_connections 一直在增长,说明确实有连接被拒绝。再看 Max_used_connections,如果它一直贴着 max_connections,说明是持续满;如果当前 Threads_connected 很高,但 Max_used_connections 低于 max_connections,说明是最近突然涨起来的,可能是某个时刻的并发峰值。
4.2 第二步:按 user、host、db 分组,定位“谁”在占用连接
执行:
sql复制SELECT user, host, db, command, COUNT(*) AS cnt
FROM information_schema.processlist
GROUP BY user, host, db, command
ORDER BY cnt DESC;
这一步是核心。结果会直接告诉你:
- 某个 user 的连接特别多?那这个业务账号的使用方需要自查。
- 某个 host 的连接特别多?那这台机器上的应用可能出了问题,比如多实例启动、连接泄漏。
- command 大部分是 Sleep?连接泄漏或空闲连接没回收。
- command 大部分是 Query?说明真的在跑大量查询,需要看慢查询和并发。
4.3 第三步:抓 State 和 SQL,判断连接是在“干活”还是在“卡死”
如果大部分连接都在 Query 状态,继续往下查:
sql复制SELECT id, user, host, db, time, state, LEFT(info, 120) AS sql_text
FROM information_schema.processlist
WHERE command = 'Query'
ORDER BY time DESC;
重点关注 time 很长的查询。state 不同代表的问题不同:
Sending data:正在执行查询,可能也是正常的慢查询,需要看 SQL 和索引。Waiting for table metadata lock:有 DDL 语句在等待元数据锁,后续所有用到该表的查询都会被堵住。这种场景下连接数很快就会涨满。常见原因是某个长事务一直没提交,DDL 拿不到锁,然后一堆查询排队。Waiting for table flush:类似,通常是 FLUSH TABLES 和查询之间的等待。Waiting for lock to be granted:等待行锁或表锁,一般是并发写冲突。
如果是 Waiting for table metadata lock,可以用下面的语句查一下当前正在运行的长事务:
sql复制SELECT trx_id, trx_state, trx_started, trx_mysql_thread_id
FROM information_schema.innodb_trx
ORDER BY trx_started ASC;
一旦确认有长期未提交的事务,处理方式基本就是确认后结束对应会话。
4.4 第四步:区分连接泄漏和真实高并发
这一步决定了你的应对策略。
如果 processlist 里大量是 Sleep 连接,而且 time 很大,说明连接根本没有被释放。这时即使你把 max_connections 调大到 2000,也只是把问题延后。真正要做的是改代码,或者临时调低 wait_timeout 让数据库强制回收空闲连接。
如果 processlist 里大量是 Query 状态,而且 time 并不长,说明是业务并发确实高。这种情况下要检查是不是有缓存穿透、SQL 没走索引、连接池最大连接数配置过大、或者 SQL 被频繁重复执行。单纯调大 max_connections 只是被动应对。
4.5 第五步:确认无误后,再决定 KILL 还是调参
如果某个连接明显有问题,比如一个查询跑了十几分钟,或者一个事务卡了很久,可以用:
sql复制KILL 12345;
注意,KILL 一个正在执行大事务的连接,回滚可能需要很长时间,而且会占用大量资源。执行之前最好确认一下这个连接执行了什么操作。另外 KILL 空闲连接是比较安全的,但应用连接池可能不会立刻感知到连接被断开,需要等连接池的心跳检测发现连接失效后重建。
调参的话,优先用 SET PERSIST(MySQL 8.0)或同时修改配置文件和 SET GLOBAL,避免重启后失效。
5. 从救火到防火:连接数的日常监控与防御性配置
排查过几次连接数打满的问题之后,我养成了给数据库做连接数监控的习惯。不用很复杂,一套简单的脚本加告警就够了。
5.1 用脚本定时采集连接数关键指标
下面这个 Shell 脚本我放在 cron 里,每分钟执行一次,把连接数相关指标写到日志文件。工具是 mysql 自带的命令行客户端。
bash复制#!/bin/bash
MYSQL_USER="monitor_user"
MYSQL_PASSWORD="your_password"
MYSQL_HOST="127.0.0.1"
MYSQL_PORT="3306"
TIMESTAMP=$(date "+%Y-%m-%d %H:%M:%S")
STATUS=$(mysql -h"$MYSQL_HOST" -P"$MYSQL_PORT" -u"$MYSQL_USER" -p"$MYSQL_PASSWORD" -Nse "
SELECT CONCAT(
'threads_connected=', @@GLOBAL.threads_connected,
', max_used_connections=', @@GLOBAL.max_used_connections,
', threads_running=', @@GLOBAL.threads_running,
', max_connections=', @@GLOBAL.max_connections,
', connection_errors_max_connections=', @@GLOBAL.connection_errors_max_connections
);
" 2>/dev/null)
echo "$TIMESTAMP $STATUS" >> /var/log/mysql_connection_monitor.log
监控账号只需要 PROCESS、REPLICATION CLIENT 或者至少 SHOW STATUS 权限就行,不要用 root:
sql复制CREATE USER 'monitor_user'@'localhost' IDENTIFIED BY 'your_password';
GRANT PROCESS, SHOW DATABASES ON *.* TO 'monitor_user'@'localhost';
FLUSH PRIVILEGES;
有了这个日志,后续再做连接数趋势分析就有数据了。如果你用的是 Prometheus + mysqld_exporter,mysql_global_status_threads_connected、mysql_global_status_max_used_connections、mysql_global_variables_max_connections 这些指标也是现成的,直接配告警规则就行。
5.2 告警阈值怎么设置才不“狼来了”
告警阈值设置得太敏感,每天轰炸你,最后就没人看了;设置得太宽,又起不到提前发现的作用。我的经验是这样:
Threads_connected / max_connections超过 80% 时告警,留给运维一定的缓冲时间。Connection_errors_max_connections如果非零,立即告警,说明已经出现过被拒连接了。Threads_running持续大于 CPU 核数的 4~6 倍时告警,这时候往往不是连接数问题,而是查询负载异常。Max_used_connections只要接近过 max_connections,就值得复盘一次。
5.3 应用侧连接池参数的联动配置
数据库的 max_connections 和应用侧的连接池参数必须放到一起评估。以 Java 的 HikariCP 为例,核心参数有:
maximumPoolSize:连接池允许的最大连接数。minimumIdle:连接池保持的最小空闲连接数。idleTimeout:空闲连接被回收前的最长等待时间。maxLifetime:连接的最大存活时间,建议小于数据库 wait_timeout。
配置逻辑是这样的:如果数据库 max_connections 是 800,应用有 8 个服务实例,每个实例的 maximumPoolSize 就应该控制在 60~80 之间,同时给数据库预留 10%~20% 的连接数给运维和后台任务。一定要用“实例数 × 最大池大小”来估算总连接数,很多连接数打满的线上事故就是这个简单的乘法没算明白。
maxLifetime 应该比数据库 wait_timeout 小,比如数据库 wait_timeout 是 300 秒,连接池 maxLifetime 设置在 240 秒左右,这样连接会被连接池主动换新,而不是被数据库静默断开后还留在池子里。HikariCP 默认 maxLifetime 是 1800000 毫秒(30 分钟),一般没问题,但如果你手动改小了数据库 wait_timeout,连接池这里一定要同步调。
5.4 几个容易忽略的防御性设置
除了监控和连接池参数,下面这几个设置也建议顺手做掉:
-
skip_name_resolve:如果应用都是用 IP 连接数据库,建议在 my.cnf 里开启
skip_name_resolve,跳过反向 DNS 解析。否则每次建立新连接时数据库会尝试解析客户端主机名,解析超时的时候连接就会卡住,连接数在高峰期容易暴涨。注意开启这个参数后,MySQL 授权表里的 host 只能写 IP 或 localhost,不能写域名了。 -
max_user_connections:给每个业务账号设置一个连接数上限,避免一个模块的问题拖垮整个数据库。比如:
sql复制ALTER USER 'app_user'@'%' WITH MAX_USER_CONNECTIONS 200;
- connection_control 插件:如果担心有人暴力破解数据库账号,可以启用
connection_control插件。它可以在连续连接失败后自动延迟后续连接,间接保护连接数不被恶意尝试打满。配置方式:
sql复制INSTALL PLUGIN connection_control SONAME 'connection_control.so';
SET GLOBAL connection_control_failed_connections_threshold = 3;
SET GLOBAL connection_control_min_connection_delay = 1000;
- 定期巡检:每周至少看一次
Max_used_connections和Connection_errors_max_connections。如果 Max_used_connections 和 max_connections 的比值持续走高,就要提前做扩容或者优化,而不是等线上出问题。
拿我自己的体会来说,连接数问题排查得多了,最大的感受是:不要等到报错了才去查。每次处理完线上故障,把当时的 processlist 快照、状态变量、应用侧日志保存下来,对照着复盘一次,下一次再遇到类似情况心里就有底了。最后再分享一个实用小技巧:如果你只想快速看一眼数据库当前是不是健康,直接执行 SHOW GLOBAL STATUS LIKE 'Threads_running';,这个数超过 CPU 核心数的一定倍数时,别纠结连接数了,大概率是 SQL 出了问题。
