☰
MySQL连接数查看与连接状态排查:从PROCESSLIST到连接池调优
2026/11/3 21:06:22 网站建设 项目流程

凌晨两点手机上弹出一条数据库告警,第一反应往往是登上机器敲SHOW PROCESSLIST,然后被几百行 Sleep 状态刷屏,越看越糊涂——连接数到底是 200 还是 2000?是数据库扛不住了,还是应用端在白白占着不放?这类场景里,"MySQL 查看连接数"和"查看连接状态"看起来是最基础的两条命令,实际上口径不清、字段不熟、权限不对,都会让你在关键时刻拿到一份误导性的数据。我见过不止一次事故复盘,结论从"数据库连接数打满"一路查到"应用连接池配置写错一位数",中间绕了两三个小时,根子就在于没人把几个数字之间的关系理清楚。

这篇内容面向的是需要真正盯 MySQL 连接状况的人:写业务代码的后端、负责数据库的运维、做容量评估的架构岗,也包括刚接手一套老系统、面对一堆慢查询和连接泄漏不知道从哪下手的新同学。下面我不会只给你一堆命令清单,而是把每个数字背后的含义、命令之间的差异、排查的完整链路,以及参数调整时那些文档里不会写的坑,一层层拆开讲。

1. 先把"连接数"这个词拆开:三个口径不能混着看

很多人上来就问"现在有多少连接",这个问题本身就是模糊的。在 MySQL 里至少存在三套计数方式,它们各自回答不同的问题,混用就会得出矛盾结论,进而把排查方向带偏。

1.1 Threads_connected、Threads_running 与 TCP 连接数的错位

Threads_connected是服务端当前认为已经建立并持有的连接线程数量,包含正在干活的和在睡觉的。Threads_running是其中不处于 Sleep 状态、真正在执行语句的线程数。这两个值差距巨大是常态,比如Threads_connected300、Threads_running5,说明两百九十多个连接在空转,数据库本身并不忙,问题出在连接管理而不是算力。

SHOW GLOBAL STATUS WHERE Variable_name IN ('Threads_connected','Threads_running','Threads_cached','Threads_created', 'Max_used_connections','Aborted_connects','Connection_errors_max_connections');

而操作系统层面数出来的 TCP 连接,又和Threads_connected不完全相等。用ss数建立态连接,会包含三次握手刚完成、服务端还没来得及分配线程的短暂时窗,也会因为客户端和服务端在同一台机器上、存在本地回环连接而多算或少算。

ss -tn state established '( sport = :3306 )' | tail -n +2 | wc -l

我的经验是:这三套数字不要试图让它们完全对上,正负 5% 的偏差都是正常的。真正有意义的是看趋势——Threads_connected随时间缓慢爬升且不回落,才是需要警觉的信号;某一瞬间三个数字差距大,反而说明不了什么问题。

注意:SHOW STATUS默认行为在部分版本和客户端下会返回会话级变量,为避免歧义,涉及连接数的查询统一写成SHOW GLOBAL STATUS。

1.2 为什么 PROCESSLIST 行数和 netstat 计数经常对不上

最常见的困惑是:SHOW PROCESSLIST显示 120 行,netstat数出 180 个 ESTABLISHED,差的 60 个去哪了?可能的原因有这么几类,按发生频率排下来很好用。

差异来源典型表现判断方法
权限不足导致行被过滤只能看到自己的连接用高权限账号查一次对比
短连接快速建连断连netstat 瞬时值偏高间隔重复采样看是否稳定
客户端侧连接池已建连但未认证服务端尚未分配线程看Aborted_connects是否增长
中间层转发(代理、负载均衡)服务端连接数远小于客户端查代理侧连接统计
CLOSE_WAIT 堆积netstat 中非 ESTABLISHED 状态多ss -tan state close-wait

权限这一条是新手最容易踩的。用普通业务账号执行SHOW PROCESSLIST,只能看到自己会话发起的线程,其余全部不可见,而Threads_connected是全局统计,不受权限影响。于是就会出现"我查了只有 3 个连接,为什么还报 Too many connections"这种自相矛盾的局面。排查连接问题,第一件事就是换一个有PROCESS权限的账号,或者干脆用管理账号。

2. 五种查法的取舍:从一条命令到一张视图

真正常用的查询方式大概五种,各有各的适用场景。选错了不是查不出来,而是查出来的信息量不够支撑判断。

2.1 SHOW STATUS 与 mysqladmin:秒级体检

只想快速知道"现在几个连接、历史峰值多少",SHOW GLOBAL STATUS是最快的。但更省事的是mysqladmin,它不需要进交互式客户端,适合写进脚本和监控采集。

mysqladmin -h127.0.0.1 -uroot -p status mysqladmin -h127.0.0.1 -uroot -p extended-status | grep -E 'Threads_|Max_used|Aborted'

mysqladmin status输出的一行里包含Threads:字段,就是当前连接数。Max_used_connections反映自启动以来的历史峰值,这个指标在做容量规划时比当前值有用得多——它告诉你系统最糟糕的时候顶到过多少,max_connections是不是留足了余量,看这个比看现在准。MySQL 8.0.14 之后还多了Max_used_connections_time,能直接告诉你峰值出现在什么时候,配合当时的业务日志能快速对上某个批处理或者大促活动。

Aborted_connects和Connection_errors_max_connections这两个计数器值得单独盯。前者是连接尝试失败次数(认证失败、握手超时等),后者是明确因为达到max_connections上限而被拒绝的次数。只要后者在涨,就说明系统已经真真切切地拒绝过业务请求了,不是"可能有问题",是"已经出过问题"。

2.2 SHOW PROCESSLIST 与 information_schema.PROCESSLIST 的差异与权限坑

要看明细,SHOW PROCESSLIST是入口,但它有两个容易被忽略的特性。第一个是Info列默认只显示前 100 个字符,稍长一点的 SQL 就被截断成半句话,排查慢查询时看着像天书,必须用SHOW FULL PROCESSLIST。第二个是它输出的是快照,不是实时流,在连接数剧烈波动的时候,你看到的和半秒后看到的可能完全是两回事。

information_schema.PROCESSLIST的价值在于能当表来查,可以聚合、可以排序、可以塞进子查询。这在排查时是质变——从"肉眼扫一百行"变成"一条 SQL 出结论"。

-- 按用户、来源、库、命令类型聚合 SELECT user, db, command, COUNT(*) AS cnt FROM information_schema.PROCESSLIST GROUP BY user, db, command ORDER BY cnt DESC; -- 按来源 IP 聚合(Host 列形如 10.0.0.5:54321) SELECT SUBSTRING_INDEX(host, ':', 1) AS client_ip, COUNT(*) AS cnt FROM information_schema.PROCESSLIST GROUP BY client_ip ORDER BY cnt DESC LIMIT 20;

第二条 SQL 特别值得记住。连接数异常时,先按来源 IP 聚合一次,往往一眼就能看出是不是某个应用实例的连接池没配好,或者某台机器上的定时任务在疯狂建连。比起逐行看 Host 列,这个办法能省掉十几分钟的机械劳动。

提示:MySQL 8.0 中information_schema.PROCESSLIST的数据来源已经改为performance_schema.processlist,在performance_schema被关闭的极端配置下这张表可能不可用,此时退回SHOW FULL PROCESSLIST即可。

2.3 performance_schema 和 sys.session:能看等待事件的版本

前面两种方式只能告诉你"这个连接在跑什么 SQL、跑了多久",看不到"它在等什么"。真遇到连接卡住但 CPU 不高的情况,就必须往下挖一层,performance_schema和建立在它之上的sys库视图是唯一答案。

-- 正在执行的非 Sleep 会话,带当前语句和等待事件 SELECT thd_id, conn_id, user, db, command, time, state, LEFT(current_statement, 100) AS stmt FROM sys.session WHERE command <> 'Sleep' ORDER BY time DESC;

sys.session是官方提供的封装视图,把performance_schema.threads、events_statements_current、events_waits_current关联成一张人类可读的表,省掉了自己写多表 JOIN 的麻烦。它的state列会给出比SHOW PROCESSLIST更细的等待信息,比如具体在等哪把锁、等哪个 IO 事件。

如果这个视图查不出东西,先确认performance_schema相关 consumer 是否开启。默认配置下语句和等待事件的采集是打开的,但有些为了性能做过裁剪的实例会关掉,此时只能看到线程列表,看不到语句明细。这是接手一套陌生实例时必须先验证的一项,别等到故障现场才发现监控是瞎的。

3. 把 PROCESSLIST 的 Command 和 State 读明白

拿到明细之后,绝大多数人的问题变成了"这一屏到底哪个不正常"。Command和State这两列就是答案,但它们的取值有几十种,全背下来没意义,抓住几个高频的就行。

3.1 Command=Sleep 才是连接数的大头

Sleep意味着服务端分配给这个连接的线程正闲着,等待客户端发下一条语句。它占用了一个连接位、一份线程栈和若干缓冲区内存,但不消耗 CPU。生产环境里 80% 以上的连接通常都是 Sleep 状态,这是连接池机制的必然结果——连接建立后不急着关,下次复用。

问题就出在这个"不急着关"上。如果应用拿连接的方式有泄漏(比如异常分支里没归还),或者连接池的maxLifetime配得远大于数据库的wait_timeout,Sleep 连接会越积越多,直到顶到max_connections。这时候来一条新请求就是Too many connections,而数据库的 CPU、IO 可能都还很闲。

-- 找出睡得最久的连接 SELECT id, user, SUBSTRING_INDEX(host, ':', 1) AS client_ip, db, time, LEFT(info, 60) AS last_sql FROM information_schema.PROCESSLIST WHERE command = 'Sleep' AND time > 600 ORDER BY time DESC LIMIT 30;

time列在 Sleep 状态下表示空闲了多少秒。超过wait_timeout本该被服务端主动断开,如果还能查到,说明wait_timeout被调得很大,或者连接一直在临界点前被复用。看到成片的 3000 秒以上的 Sleep 连接,基本可以判定连接池空闲回收策略没配或者没生效。

除了 Sleep,Query是最正常的执行态,Killed表示线程收到了终止指令正在退出,Binlog Dump是主从复制线程。如果在一台从库上看到大量Binlog Dump,别慌,那是正常的复制连接。

3.2 State 里的几个高危信号

State描述的是线程当前正在做的事情,它的取值会随版本变化,但有几个信号跨版本通用,看到就该警觉。

State 取值含义处理方向
Waiting for table metadata lock等元数据锁找阻塞的 DDL 或未提交事务
Waiting for table flush等表刷新完成通常由长查询或 FLUSH 操作引起
Locked等行锁查innodb_trx找持锁事务
Copying to tmp table建临时表检查排序字段索引、结果集大小
Creating sort index排序中同上,关注sort_buffer_size
Sending data读数据并发给客户端大结果集或网络慢,不等同于慢查询
statistics优化器统计信息阶段高频出现说明查询解析开销大

Waiting for table metadata lock是线上最让人头疼的一个,因为它会连锁——一个未提交的长事务挡住了 DDL,DDL 挡住后续所有访问该表的查询,连接数在几分钟内就能堆到上限。这个场景下的连接数暴涨是"结果"不是"原因",如果只盯着连接数去调max_connections,方向完全错了。

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

这张表里trx_started最早的那条,往往就是罪魁祸首。配合trx_mysql_thread_id可以拿到连接 id,再对照 processlist 就能定位到具体是哪个应用发的。

4. 连接数飙升时的现场排查链路

真到了告警时刻,需要一套固定的动作顺序,而不是凭感觉乱点。我按自己用过最多次的路径整理一遍,每一步都给出判断依据,遇到不符合的情况能及时换方向。

4.1 从聚合统计锁定来源

第一步永远是分清"连接多但闲"还是"连接多且忙"。查Threads_connected和Threads_running的比值,如果后者只占前者的百分之几,问题在连接管理;如果两者接近,说明数据库真的在被压。

SELECT (SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME='Threads_connected') AS connected, (SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME='Threads_running') AS running, (SELECT VARIABLE_VALUE FROM performance_schema.global_variables WHERE VARIABLE_NAME='max_connections') AS max_conn;

确认是"多而闲"之后,进入第三步:按来源和用户聚合,找出是谁在堆连接。这一步用 2.2 节里那条按 IP 聚合的 SQL 就能完成。实战中常见的结果是:某一台应用服务器的连接数占了总量的七成,其他几台都很正常。这种情况几乎可以断定是那台机器的实例配置异常,或者上面的定时任务在跑批量逻辑,去查它比查数据库有效得多。

如果聚合结果比较分散、每台都差不多,那要考虑整体容量问题:连接池总容量是不是超过了max_connections。算一笔账——8 个应用实例,每个实例连接池最大 50 个连接,理论上限就是 400,如果数据库max_connections只有 300,那么在流量高峰必然打满。这种配置错误在上线评审时极难被发现,因为两边单独看都"合理"。

-- 查看被拒绝的累计次数,确认是否真的打满过 SHOW GLOBAL STATUS LIKE 'Connection_errors_max_connections';

这个值非零就是铁证。我建议把它加进监控告警,阈值设成"1 分钟内增长大于 0",因为被拒绝意味着真实用户请求失败了,没有任何容忍空间。

4.2 长事务和元数据锁造成的"假连接堆积"

有些连接数异常跟连接池一点关系都没有,纯粹是被阻塞出来的。典型链条:业务发起一个批量更新,没有走索引,扫了全表并且迟迟不提交;另一个会话执行了一条 DDL;后续所有访问这张表的请求全部进入Waiting for table metadata lock状态,连接就这么一个个堆上去。

这时候能看到的表象是连接数曲线陡直上升,五分钟内从 100 冲到 500。如果单纯提高max_connections,只是把爆炸时间往后推,阻塞依然存在,最终还是打满,而且更高的上限意味着更多线程在等锁,内存压力更大,可能诱发更严重的连锁故障。

-- 找执行时间最长的非空闲会话 SELECT id, user, SUBSTRING_INDEX(host, ':', 1) AS ip, db, command, time, state, LEFT(info, 80) AS sql_head FROM information_schema.PROCESSLIST WHERE command <> 'Sleep' ORDER BY time DESC LIMIT 20;

拿到可疑的 id 之后,KILL是最后手段,但必须先确认三件事:这条语句是不是真的可以中断、它是否已经改了一半数据、中断后应用能否正确处理异常。对一个大事务直接 KILL 会触发回滚,回滚耗时可能比执行本身还长,期间锁依然持有,局面不会立刻好转。这个细节很多文档不会强调,但在故障现场非常关键。

如果确认要批量清理阻塞连接,可以先生成语句再人工核对,不要直接执行拼接结果:

SELECT CONCAT('KILL ', id, ';') AS stmt FROM information_schema.PROCESSLIST WHERE command = 'Sleep' AND time > 1800 AND user = 'app_user';

把输出复制出来逐条看一遍,确认没有误伤再执行。我吃过一次亏,条件里少写了一个user限制,差点把复制账号的连接也带进去。

5. 参数侧的取舍:max_connections 不是越大越好

调参这件事,最大的误区是把max_connections当成一个可以随便往大调的旋钮。它确实能立刻缓解症状,但背后有两道看不见的墙,撞上去的时候报错信息完全不同,排查起来很容易走弯路。

5.1 文件描述符与内存的两道隐形上限

每个连接在操作系统层面都要占用一个文件描述符。Linux 默认的单进程 fd 上限常见是 1024,如果max_connections调到 2000 而open_files_limit和系统ulimit -n没跟上,等到连接数真正涨到那个量级时,报的错会是Can't create a new thread或者Too many open files,而不是连接数超限。这类错误信息指向性很差,容易让人误以为是线程栈或者内存问题。

# 查看数据库进程当前实际可用的 fd 上限 cat /proc/$(pidof mysqld)/limits | grep 'open files'

open_files_limit在配置文件里设置不总是生效,MySQL 会尝试用setrlimit去提升,但受限于 systemd 服务单元里LimitNOFILE的配置。也就是说改了my.cnf还得改服务单元,改完还得重启服务,三件缺一不可。这个坑在容器化部署里更隐蔽——容器内的ulimit和宿主机的不是一回事。

内存这一侧更微妙。每个连接都会分配连接级缓冲区,read_buffer_size、sort_buffer_size、join_buffer_size、read_rnd_buffer_size这些参数是"按需分配",但用的连接一多,累加起来非常可观。粗算一下:假设四个缓冲区各 256KB,一个活跃连接峰值能吃到 1MB,200 个并发活跃连接就是 200MB 额外的内存开销,还不算performance_schema本身占用的部分。所以把max_connections从 500 提到 2000,如果没有相应的内存余量,代价可能是频繁的 swap 甚至实例被系统 OOM 杀掉,后果比连接拒绝严重得多。

我的做法是:max_connections的取值优先服从"应用侧连接池总容量 + 预留 30%",而不是拍一个整数。先把每个应用实例的连接池上限列出来求和,再加上运维工具、备份、复制、监控采集的固定占用,最后留出三成余量。这个数值往往比拍脑袋的结果小,反而更稳。

5.2 wait_timeout 与连接池 maxLifetime 的配合

wait_timeout(默认 28800 秒,也就是 8 小时)决定服务端多久主动断开一个空闲连接。interactive_timeout管的是交互式客户端,命令行登录属于这一类。这两个值调大能减少"连接被服务端悄悄断掉"导致的应用报错,但代价是空闲连接长期占用资源。

真正的问题出在两侧超时不匹配上。如果连接池的maxLifetime配成 30 分钟,而wait_timeout是 8 小时,没问题;反过来,maxLifetime是 8 小时而wait_timeout只设了 600 秒,就会出现:连接池以为连接还活着,取出来发语句,服务端早就断开了,应用侧报出Communications link failure或者那句经典的The last packet successfully received from the server was xxx milliseconds ago。

解决办法是把连接池的maxLifetime设得明显小于wait_timeout,留出足够的安全余量。经验值是一半左右,比如wait_timeout600 秒、maxLifetime240 到 300 秒。另外别忘了让连接池开启保活检测,绝大多数成熟连接池都有keepaliveTime或者借出前校验的开关,把这层保险加上,比反复调整服务端参数有效。

参数侧别建议关系常见错误
wait_timeout数据库略大于业务空闲间隔设为 8 小时却抱怨连接堆积
maxLifetime连接池约为wait_timeout的一半设得比服务端超时还长
idleTimeout连接池小于maxLifetime从不回收,Sleep 长期堆积
maxPoolSize连接池总和小于max_connections的七成各实例相加超过数据库上限

提示:interactive_timeout和wait_timeout都会影响连接存活,只改其中一个有时看不到效果,建议成对调整并同步观察Threads_connected曲线是否真的有回落。

6. 指标落地:把一次排查变成常态监控

排查做完不代表事情结束。同一类问题第二次出现时,如果没有指标支撑,你还得再走一遍完整链路。把关键量固化下来,是让运维成本指数下降的一步。

6.1 值得盯的六个状态量

状态变量有几百个,全都采集既浪费存储也淹没重点。围绕连接问题,下面六个足够覆盖绝大多数场景,采集频率 10 到 15 秒一次比较合适。

指标含义关注点
Threads_connected当前连接数趋势是否单调上升
Threads_running活跃线程数与上一项的比值
Max_used_connections历史峰值是否逼近上限
Threads_created累计创建线程数增长速率反映长连接是否失效
Aborted_connects连接失败次数认证或网络问题
Connection_errors_max_connections因超限被拒次数一旦非零即为故障

Threads_created的增速特别值得单独说。它是累计值,正常使用连接池的系统里应该增长得非常缓慢,因为线程被复用。如果发现它每秒都在涨,说明连接在被频繁地创建和销毁,连接池形同虚设,要么是配置写错,要么是代码里每次操作都新建连接。thread_cache_size能缓解一部分开销,但治标不治本,还是得回到应用侧去查。配合Threads_cached看命中情况,能进一步确认线程缓存的效率。

6.2 告警阈值与基线怎么定

阈值不能照抄别人的。我习惯的做法是先跑两周只观察不告警,拿到这条业务连接数的日内波动曲线,找出它自己的规律,再定阈值。比如有的系统白天 80、夜里 20,有的系统因为定时任务每天凌晨冲一次 300,波动本来就大,用固定阈值一定会误报。

实用的组合是两条规则的叠加。第一条看绝对占比:Threads_connected超过max_connections的 80% 持续 1 分钟就告警,这条对突发流量有效。第二条看相对变化:相比过去 7 天同时段基线上涨 50% 就提醒,这条对缓慢爬升型的连接泄漏更敏感——那种每天涨一点、一周后打满的问题,绝对阈值要等到最后才触发,而相对基线能在第三天就给出信号。

Threads_running的告警阈值要低得多,我一般设在连接的 20% 或者绝对值 30 这两者取小。因为这个数一旦上去,意味着真实并发压力,比起连接数打满,它更直接地反映用户体验恶化。另外别忘了给Connection_errors_max_connections单设一条零容忍规则,它一旦增长就是已经在拒绝请求了,属于最高优先级。

对于sys.session这类带等待事件的视图,适合做成周期采样落库,用来回答"某次故障时数据库到底在等什么"这种回溯性问题。采样频率不用高,一分钟一次,只保留command <> 'Sleep'的行,存储压力很小,但在复盘时的价值极大——比事后凭记忆回忆"当时好像是锁等待"可靠得多。

最后说个我在实际项目里踩出来的体会:连接数问题十次有八次根子不在数据库,而在应用怎么用连接。所以每次排查完,别只在那台数据库上改参数,一定要顺着 Host 列找到对应的应用实例,把连接池配置、连接归还逻辑、超时设置一起拉出来看一遍。数据库侧的参数只是兜底,真正的病根通常藏在业务代码里一段没有finally的获取连接逻辑,或者一个连异常都没处理的批量任务中。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询