凌晨两点,监控告警响个不停:应用日志里刷满了 ERROR 1040 (HY000): Too many connections,网站前台开始大面积 500,后台管理登录不进去。运维同学第一反应通常是"把 max_connections 调大",但从521调到2000之后没撑过一周又满。连接数打满从来不是一个孤立的参数问题,它背后通常是慢查询、连接池泄漏或者事务没提交。这篇文章按"先确认现象—定位根因—算内存账—改参数—改架构"的顺序,把 MySQL 连接数打满的完整处理链路讲清楚,并给出可以直接复制执行的命令。
一、先确认:是不是真的"连接打满了"
报错信息往往有迷惑性。Too many connections 一定和连接数有关,但 Can't connect to MySQL server、Lost connection during query 可能是网络、超时或权限问题。第一步要做的就是用官方保留的那条管理连接登进去,把真实数据取出来。
MySQL 默认会为拥有 SUPER(MySQL 8.0 起为 CONNECTION_ADMIN)权限的账号额外保留 1 个连接,就是为了在连接打满时留一条救火通道。所以排障账号务必具备该权限,且不要用应用程序在用的那个账号。
1.1 取关键指标
# 登录后一次性取全貌(MySQL 8.0 可直接复制执行) SHOW VARIABLES LIKE 'max_connections'; SHOW VARIABLES LIKE 'max_user_connections'; SHOW VARIABLES LIKE 'wait_timeout'; SHOW VARIABLES LIKE 'interactive_timeout'; SHOW VARIABLES LIKE 'thread_cache_size'; SHOW VARIABLES LIKE 'open_files_limit'; SHOW VARIABLES LIKE 'max_allowed_packet'; SHOW STATUS LIKE 'Threads_connected'; SHOW STATUS LIKE 'Threads_running'; SHOW STATUS LIKE 'Threads_cached'; SHOW STATUS LIKE 'Threads_created'; SHOW STATUS LIKE 'Max_used_connections'; SHOW STATUS LIKE 'Connections'; SHOW STATUS LIKE 'Aborted_connects'; SHOW STATUS LIKE 'Uptime';
1.2 怎么读这些数字
- Threads_connected:当前已建立的连接数,这是与
max_connections直接对比的值。默认max_connections = 151,当 Threads_connected 长期贴着上限跑(比如 >85%),就说明已经处于危险区。 - Threads_running:正在执行语句的连接数。如果 Threads_connected 很高但 Threads_running 只有个位数,说明大量连接是空闲
Sleep,属于"占着茅坑不拉屎",治理成本低;如果 Threads_running 也很高(超过 CPU 核心数的 2-3 倍持续不降),说明是慢查询或锁等待,调大连接数只会加速雪崩。 - Max_used_connections:历史峰值。它除以 Uptime 没有直接意义,但它可以回答"从来没有超过 300,为什么 151 就爆了"这类疑问。
- Threads_created / Connections:线程创建率。若 Threads_created 与 Connections 的比值持续偏高(超过百分之几),说明
thread_cache_size太小,每次都新建线程,在高并发短连接下会造成明显抖动。 - Aborted_connects:失败连接尝试次数。它突然飙升通常意味着连接池里的连接在数据库侧被提前断开(wait_timeout 设置小于连接池空闲校验周期),或者密码/网段/最大连接数限制。
二、连接为什么会堆积:五类根因
把 Threads_running 和 Threads_connected 的比例搞清楚之后,基本可以对号入座:
2.1 空闲连接释放不掉(Sleep 堆积)
这是最常见的一类。wait_timeout 默认值是 28800 秒(8 小时),意味着一条没有任何动作的连接可以白占 8 小时。如果应用使用了连接池但没有设置 idleTimeout 或 maxLifetime,或者代码里 Connection 没有在 finally 中关闭,连接会一路累积。判定标准是:Threads_connected 高、Threads_running 低、SHOW PROCESSLIST 里大片 Command = Sleep 且 Time 值成千上万。
2.2 慢 SQL 占住连接
一条原本 20ms 的查询,因为索引失效变成 8 秒,在同样的 QPS 下并发连接数会瞬间放大十几倍,直到打满上限。这类问题的表象是 Threads_running 高、CPU 或 IO 吃紧,处理方向是把慢 SQL 找出来,而不是加连接。
2.3 大事务与锁等待
一个批量更新跑了 10 分钟不提交,所有访问同一批行的请求会全部堵住,堵住的连接不会释放,新的连接继续涌入,5 分钟内就可以把 151 全部耗尽。此时 SHOW PROCESSLIST 里会看到大量 State 为 updating、Lock wait timeout exceeded 的连接。
2.4 连接池参数与实例数没算过账
这是最容易被忽视的乘法效应。实际最大连接数 = 应用节点数 × 单节点连接池上限。举例:8 个 Java Pod,每个 HikariCP maximumPoolSize=30,理论上限就是 240,已经超过默认的 151;若再配上管理后台、定时任务、BI 查询账号,打满是必然的。合理做法是让"节点数 × 池大小"留出 30% 余量后再反推数据库参数。
2.5 连接风暴与建连开销
秒杀、定时任务集中触发、缓存集体失效后全量回源,都会在几百毫秒内产生大量新建连接。thread_cache_size 不足时每个连接要现场 pthread_create,表现为 CPU sys 升高、QPS 反而下降。
三、按图索骥:从"谁占着连接"开始查
不要用肉眼扫 SHOW PROCESSLIST 的几百行输出,直接用 information_schema 做聚合和生成处置语句。
-- 1) 按 Command 维度统计连接分布
SELECT command, COUNT(*) AS cnt, MAX(time) AS max_time, AVG(time) AS avg_time
FROM information_schema.processlist
GROUP BY command ORDER BY cnt DESC;
-- 2) 找出 Sleep 超过 300 秒的空闲连接
SELECT id, user, host, db, command, time, LEFT(state,40) AS state
FROM information_schema.processlist
WHERE command = 'Sleep' AND time > 300
ORDER BY time DESC LIMIT 20;
-- 3) 找出运行超过 5 秒的活动查询
SELECT id, user, host, db, command, time, LEFT(state,40) AS state, LEFT(info,100) AS sql_head
FROM information_schema.processlist
WHERE command != 'Sleep' AND time > 5
ORDER BY time DESC LIMIT 20;
-- 4) 批生成 KILL 语句(先看结果再复制执行,不要直接拼 --force)
SELECT CONCAT('KILL ', id, ';') AS kill_sql
FROM information_schema.processlist
WHERE command = 'Sleep' AND time > 1800;
-- 5) 找长事务(MySQL 5.7 / 8.0 通用)
SELECT trx_id, trx_mysql_thread_id AS thread_id, trx_started,
TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS trx_seconds,
trx_rows_locked, trx_rows_modified, LEFT(trx_query, 80) AS sql_head
FROM information_schema.innodb_trx
WHERE TIMESTAMPDIFF(SECOND, trx_started, NOW()) > 10
ORDER BY trx_seconds DESC LIMIT 20;
拿到 thread_id 之后,用 KILL 断开单个连接,或用 KILL QUERY 只终止正在执行的语句而保留连接。对 Sleep 连接用 KILL,对跑了一半的大事务要谨慎:直接断连接会触发回滚,回滚耗时可能比执行还久,此时优先优化 SQL 或让业务自己结束。
应急时可以用一条 shell 快速取数,避免交互式登录:
# 每 2 秒采样一次连接数与运行连接数 watch -n 2 "mysql -uroot -p'你的密码' -e \"SHOW STATUS LIKE 'Threads_connected'; SHOW STATUS LIKE 'Threads_running';\"" # 非交互式统计 Sleep 连接数量 mysql -uroot -p'你的密码' -N -e "SELECT COUNT(*) FROM information_schema.processlist WHERE command='Sleep';"
四、参数怎么调:max_connections 与它的搭档
调大 max_connections 是有成本的:每一个连接都要分配线程栈和会话级缓冲区,并不是"免费的"。下面是数据库侧最常动的几个参数:
| 参数 | 常见默认值 | 建议值(Web 类 OLTP) | 作用与注意事项 |
|---|---|---|---|
| max_connections | 151 | 500 - 2000 | 硬上限,动态参数,但同时必须写进配置文件;MySQL 8.0 理论上界很大,实际受内存约束 |
| max_user_connections | 0(不限) | 设为 max_connections 的 70%-80% | 限制单账号,防止一个应用把整实例拖死,管理/监控账号不受此限 |
| wait_timeout | 28800 | 60 - 300 | 非交互连接空闲多久后断开;必须大于连接池的空闲检测周期 |
| interactive_timeout | 28800 | 600 - 1800 | 交互式连接(命令行、客户端工具)空闲断开时间 |
| thread_cache_size | 8.0 默认 -1(自动) | 50 - 100 | 缓存空闲线程;短连接高并发场景必须调大 |
| max_allowed_packet | 5.7 为 4MB,8.0 为 64MB | 16MB - 64MB | 单包上限,过大 INSERT/批量导入报 Packet too large 时需要调 |
| open_files_limit | 系统值,常为 1024 的倍数 | ≥ 65535 | 每个连接与每张表都要 fd,不足时会卡住打开表 |
4.1 先算内存账,再决定上限
会话级缓冲区不是每条连接都全额占用,但估算容量时必须按最坏情况算:
单条活跃连接估算 = thread_stack
+ read_buffer_size (默认 128K)
+ read_rnd_buffer_size (默认 256K)
+ sort_buffer_size (默认 256K,MySQL 8.0.28 之后版本官方默认值有调整)
+ join_buffer_size (默认 256K)
+ net_buffer_length (初始 16K,可增长至 max_allowed_packet)
其中 thread_stack 在 64 位 Linux 上约 256KB(MySQL 8.0 部分版本约 286720 字节)。把这些加总,一条"什么都不做"的连接约 1MB 起步;一条正在做排序和大表 join 的连接很容易到 5-10MB。
一个保守的容量公式:
实例总内存需求 ≈ innodb_buffer_pool_size
+ key_buffer_size
+ max_connections × 1MB(空闲连接基线)
+ 活跃连接数 × (sort_buffer_size + join_buffer_size + read_buffer_size + read_rnd_buffer_size)
+ tmp_table_size × 并发临时表查询数
+ 2GB(操作系统、网络栈、文件系统缓存预留)
举例:64GB 内存的物理机,innodb_buffer_pool_size 按 60% 设为 40GB,扣掉 OS 预留 2GB,剩下约 22GB 给连接相关开销。如果按"空闲 1MB + 活跃时平均 4MB"估算,max_connections = 1500 是完全够呛但可控的;反过来,一台 8GB 的小机器把 max_connections 调到 2000,光连接就可能吃掉 2-4GB,再叠上 buffer pool,OOM 只是时间问题。
4.2 配置文件写法
# /etc/my.cnf 或 /etc/mysql/mysql.conf.d/mysqld.cnf 的 [mysqld] 段 [mysqld] max_connections = 1000 max_user_connections = 800 wait_timeout = 180 interactive_timeout = 600 thread_cache_size = 100 max_allowed_packet = 32M thread_stack = 262144 open_files_limit = 65535 table_open_cache = 4000
在线生效(重启后失效,所以两者都要做):
SET GLOBAL max_connections = 1000; SET GLOBAL wait_timeout = 180; SET GLOBAL interactive_timeout = 600; SET GLOBAL thread_cache_size = 100; -- MySQL 8.0 推荐用 PERSIST,写入 mysqld-auto.cnf,重启不丢 SET PERSIST max_connections = 1000; SELECT * FROM performance_schema.persisted_variables;
4.3 别忘了操作系统侧的限制
每个 MySQL 连接与打开的每张表文件都要占用文件描述符,经验公式是:
open_files_limit ≥ max_connections × 5 + table_open_cache × 2
systemd 托管的环境下,光改 /etc/security/limits.conf 往往不生效,需要加大 service 级限制:
# mkdir -p /etc/systemd/system/mysqld.service.d # vi /etc/systemd/system/mysqld.service.d/override.conf [Service] LimitNOFILE=65535 LimitNPROC=65535 # systemctl daemon-reload # systemctl restart mysqld # cat /proc/$(pidof mysqld)/limits | grep -i "open files"
五、治本方案:连接池、线程池与代理层
参数调完之后,如果应用层不改,连接数还是会被慢慢吃回去。三种成熟的收敛思路:
| 方案 | 适用场景 | 优点 | 代价与坑 |
|---|---|---|---|
| 规范连接池参数 | 所有场景,首选 | 零成本;把总连接数直接压下来 | 需要改应用,且要随节点数变化重新对账 |
| ProxySQL / 连接池中间件 | 节点多、语言杂 | 前端成百上千连接复用到后端数十连接;自带读写分离与查询规则 | 多一层故障域,需要高可用部署 |
| 线程池(thread_handling=pool-of-threads) | 短连接、高并发小查询 | 连接数与线程数解耦,千连接也不炸 | MySQL 社区版没有;需换 Percona Server / MariaDB,或商业版线程池插件 |
| 读写分离 | 读多写少 | 把读压力摊到多个从库 | 有主从延迟,强一致读必须走主库 |
连接池大小不要拍脑袋。连接数 = CPU 核数 × 2 + 磁盘数 是一个常用的起点公式(源于 HikariCP 官方对机械盘时代的分析),SSD 环境下这个值偏保守;更实用的做法是:先用 10-30 起步压测,观察数据库侧 Threads_running 是否超过 4×CPU 核数,如果超过了说明已经过饱和,加连接只会让平均响应时间继续上升。同时务必保证 idleTimeout < wait_timeout,并开启 connectionTestQuery 或 validationQuery,避免应用拿到被数据库关掉的死连接。
六、排查与处置步骤清单
- 用具备 SUPER / CONNECTION_ADMIN 权限的账号登录(打满时被拒就用 MySQL 保留的那条管理连接)。
- 取
SHOW STATUS LIKE 'Threads_%'与Max_used_connections,确认 Threads_connected 是否贴着上限。 - 看 Threads_running:个位数说明是空闲堆积(走第 4 步),持续高位说明是慢查询或锁(走第 5 步)。
- 空闲堆积:统计并 KILL 掉 Sleep 时间超过阈值的连接;同时核对连接池 maximumPoolSize 与实例数的乘积。
- 忙连接堆积:用
SHOW PROCESSLIST与information_schema.innodb_trx找出长 SQL / 长事务;优先终止源头事务,再优化 SQL 与索引。 - 按第 4 节公式算内存,确认目标 max_connections 不会触发 OOM,再
SET GLOBAL临时放大。 - 同步调
wait_timeout、thread_cache_size、max_user_connections,三者缺一不可。 - 改操作系统文件描述符限制与 systemd 配置,避免 fd 先于连接数耗尽。
- 把改动写进配置文件(或
SET PERSIST),避免重启回滚。 - 加监控:Threads_connected / max_connections 超过 80% 告警,并保留 1 分钟的 Threads_running 采样曲线。
小结
MySQL 连接数打满的本质是"资源供给小于需求",把 max_connections 从 151 调到 2000 只是把账单往后挪。真正有效的顺序是:先用 Threads_connected / Threads_running 区分空闲堆积与忙堆积,再针对性处理 Sleep 连接、慢 SQL 或长事务,最后结合内存公式与 OS 限制去调整参数。日常的避坑点包括:只改运行时不改配置文件(重启即回滚)、wait_timeout 小于连接池空闲周期(导致 Aborted_connects 飙升)、忽略 max_user_connections(一个账号拖垮全实例)、以及忘记调 ulimit 导致 open_files_limit 成为新的瓶颈。把这些点做成巡检清单,连接数问题基本就不会在半夜再响一次。
企业QQ咨询




