1. 项目概述与核心思路拆解
1.1 核心需求解析
做MySQL运维的人,几乎都遇到过这样的场景:业务方突然报障,说某个接口响应变慢了、订单超时了、后台卡住了,打开监控一看,数据库的线程数飙高,慢查询日志哗哗往外吐。这时候你会怎么做?很多人第一反应是去看慢查询日志,但慢查询日志记录的是执行完的语句,等你看清楚问题,数据库可能已经濒临崩溃了。真正要紧的,是当下这一刻数据库里正在跑什么。
show processlist 就是干这个用的——它直接列出当前数据库实例中所有活动的连接和线程状态,告诉你谁在跑、跑了多久、卡在哪个环节、处于什么状态。这玩意儿在网上被讨论得很多,但我发现大多数人其实只用了它最浅层的能力:看一眼有没有长事务、有没有锁等待,然后就没了。实际上,如果你能把这个命令返回的每一个字段、每一种State都吃透,再结合慢查询日志和EXPLAIN执行计划,你完全能做到“精准定位问题SQL”,而不是靠猜、靠试、靠碰运气。
这就是我写这篇东西的初衷。我不打算讲什么高深莫测的内部原理,而是用庖丁解牛的方式,把 show processlist 这条命令从头到尾拆开,把每个字段背后的含义、每种常见状态的成因、每个排查场景下该怎么组合使用这个工具,都讲清楚。无论你是刚接手MySQL运维没几个月的新人,还是被线上慢查询折磨了无数个夜晚的“老油条”,这篇内容应该都能给你一些之前没注意到的视角。
1.2 为什么优先看 processlist 而不是慢日志
先说一个我踩过很多次的坑:慢查询日志有一个致命的缺陷——它是事后记录。一条SQL跑完了,耗时超过阈值,才被写进日志。可问题是,如果这条SQL一直在跑,没有结束,慢查询日志里根本不会有它。就好比一个病人一直在持续发烧,你非要等他体温恢复正常之后再去翻病历,那能看到什么?真正造成系统雪崩的,往往是那些执行了30秒还没结束、甚至永远不结束的查询,它们牢牢占着连接不释放,线程池被耗干,新请求全部排队。
show processlist 恰恰能补上这个盲区。它是实时的、在线的、直接反映当下数据库内部状态的。你执行这条命令,看到的每一行都是一个存活的线程,能直观地看到某条SQL已经跑了多少秒、当前卡在什么状态。我见过很多次数据库CPU飙到100%的场景,一执行 show processlist,立刻就能看到一条 SELECT 在拿着全表扫描,Rows_examined 已经几千万了还在往前冲。这种问题,靠慢日志根本来不及。
所以我的习惯是:遇到数据库性能问题,第一件事永远是 show processlist,慢查询日志是第二看的东西。前者管“正在发生”,后者管“曾经发生”,两个视角配在一起才完整。
2. 核心细节解析:processlist 里的每个字段都别放过
2.1 字段逐个拆解:不只是看懂,要能看懂背后的信息
show processlist 默认显示的内容很精简,通常包含 Id、User、Host、db、Command、Time、State、Info 这些字段。但你有没有试过用SHOW FULL PROCESSLIST?区别在于 FULL 版本会显示完整的 Info 字段,不会被截断。
先说Id这个字段,它就是连接线程的ID,也就是information_schema.processlist表里的ID列。这个ID在后边杀会话的时候要用到,比如KILL 12345,杀的就是这个ID对应的线程。需要注意一点,不同版本的MySQL对Id的显示方式略有差异,但基本逻辑是一致的。
User和Host字段代表连接来源的账号和客户端地址。很多人忽略这俩字段的价值——当数据库连接数暴涨的时候,你一眼就能看出是某个特定账号在发起大量连接,还是某个IP在疯狂重连。我曾经遇到过一次连接数告警,一看 Host 全是同一个应用服务器的IP,而且 Command 全是 Sleep,状态是空,这就是典型的连接池没配好,空闲连接不释放,把连接数堵满了。
db字段表示这个连接当前选中的默认数据库,如果为空则说明还没执行 USE。这里有个小坑:如果连接是复用过来的,db字段可能跟你实际查询的库不一致,这种时候不要被误导。
Command字段是灵魂所在。它表示这个线程正在执行的命令类型,常见的有 Query、Sleep、Connect、Quit、Binlog Dump 等。如果看到 Command 是 Query,说明正在执行一条SQL语句;如果是 Sleep,说明线程处于空闲状态,等待下一条指令。很多新手看到一堆 Sleep 就慌了,以为是数据库出问题了,其实大部分 Sleep 是正常的——只要没有积累到异常数量,就不用担心。
Time字段表示线程处于当前状态的时间,单位是秒。这个字段特别有用,因为它直接告诉你“这个问题已经持续多久了”。一个 Query 的 Time 到了几十秒,基本可以断言有性能问题。但要注意,Time 是“处于当前状态”的时间,不是SQL总执行时间,比如一条SQL在查询计划和数据读取之间切换状态,Time 会被重置。
State字段是最能体现MySQL的“内心活动”的地方,它反映SQL执行过程中当前环节的状态。这个字段值非常丰富,后面我专门用一节来详细讲。
最后是Info字段,显示线程正在执行的SQL语句。没有FULL版本时会被截断到前100个字符,所以最好习惯用 FULL 版本。通过 Info 你可以看到事情的全貌,也是定位问题SQL最直接的入口。
2.2 State 状态详解:数据库到底卡在哪一步
State 字段是我认为最有分析价值、但也最容易被人忽略的。它告诉你当前线程正在干什么,就像拍X光片一样,让你看到SQL执行路径上卡住的环节。
常见的State值不少,我挑几个出现频率最高的说:
Sending data。这可能是被误解最多的一个状态。名字听上去像是数据正在发送给客户端,但实际上它覆盖了很长的执行区间——从存储引擎读取数据、过滤、排序、计算,一直到把结果发送出去之前,都可能处于这个状态。所以看到 Sending data 数量多、Time 大,意味着这条SQL在数据读取和加工阶段花的时间很长,是慢查询的“重灾区”。
Waiting for table metadata lock,即元数据锁等待。这个状态在DDL操作和DML操作冲突时非常常见。比如对一个正在被频繁写入的表执行 ALTER TABLE,或者在一个事务里先查询了某张表,事务未提交,元数据锁没释放,后边其他会话再来访问这张表,就会被卡在这里。很多人遇到数据库突然“卡死”,看 processlist 一堆这状态,就不知道该杀哪个——正确做法是找到持有锁的那个最早连接。
Updating和Locked。Updating 表示正在执行UPDATE操作并修改表中的数据,这个状态通常在执行写操作时出现。Locked 则是被锁阻塞了,一般是因为另一个线程持有行锁或表锁。这俩状态在并发写入场景下经常成对出现。
Waiting for handler commit和Writing to binlog。前者表示等存储引擎提交事务,后者表示正在写二进制日志。在实际主从架构中,如果从库的 binlog 写入或重放产生瓶颈,你会看到大量线程卡在这里。这种情况通常需要调整innodb_flush_log_at_trx_commit参数,或者优化从库的复制效率。
User sleep和System lock。User sleep 表示SQL中调用了 SLEEP() 函数,一般不用紧张;System lock 则说明线程正在等待系统级的锁,比如表结构的锁或元数据锁。这两个状态相对少见,但出现时需要结合上下文分析。
好,字段和状态讲到这里,你已经有了基础。接下来我要聊的重点是:怎么把这些状态组合起来,实际解决一个慢查询问题。
3. 实操过程:从定位慢查询到优化落地的完整流程
3.1 经典的慢查询定位三板斧
一个标准的慢查询排查流程,我自己反复用了很多遍,每次都奏效,分享给你。先说场景:假设线上数据库CPU使用率飙到80%以上,所有业务接口都在超时,连接数逼近上限,你接到告警通知,接下来半小时要做完定位和止血。
第一步,先抓现场快照。立刻执行SHOW FULL PROCESSLIST,把结果完整保存下来。为什么一定要保存?因为线上问题稍纵即逝,可能等你切换日志或者开个新会话再跑一次,现场就变了。我习惯用一个临时表把快照存下来,方便后续慢慢分析。
CREATE TABLE tmp_processlist_snapshot AS SELECT * FROM information_schema.processlist;这里有个细节:information_schema.processlist 表其实就是 show processlist 的数据源,查询它等于执行了 show processlist,但好处是可以配合 WHERE 条件过滤,比如只看某个用户、只看 Command=Query、或者只看 Time 超过阈值的。
第二步,过滤出可疑连接。看快照里哪些线程值得关注,我的标准很简单:Command=Query 且 Time>10秒的,或者 State 是 Waiting for table metadata lock / Locked / Updating 的,或者 Info 字段里出现明显异常SQL的(比如没有 WHERE 条件的全表UPDATE,或者关联了十几张表的复杂JOIN)。
SELECT * FROM information_schema.processlist WHERE Command = 'Query' AND Time > 10;这一步做完,基本能确定问题的大方向了——是哪几条SQL在作妖,它们是谁发起的、从哪个IP来的、卡在什么状态。
第三步,决定止血还是根治。如果数据库已经濒临崩溃,THEN就别想什么优化了,直接KILL <thread_id>把最耗时的会话掐掉,先把连接数释放出来,让业务恢复响应。这个过程要果断,我见过太多人舍不得杀连接硬扛,结果整个库hang死,最后连 KILL 都执行不进去(因为连接池满了,管理操作排不上队)。MySQL 允许 KILL 自己账号权限范围内的会话,如果是云厂商的实例,通常也允许KILL任何会话。
3.2 一个完整的慢查询案例:从 processlist 到 EXPLAIN 再到底层优化
光说不练假把式。我给你完整还原一次我处理线上慢查询的真实过程。某天下午3点半,业务突然上报:核心报表接口超时率飙升,大量请求堆积,数据库CPU告警。我连上实例,先执行SHOW FULL PROCESSLIST,截取关键部分如下:
Id User Host db Command Time State Info 1001 app 10.0.0.11:52301 report Query 42 Sending data SELECT a.*, b.name, c.status FROM order_info a LEFT JOIN user_info b ON... 1002 app 10.0.0.12:52301 report Query 39 Sending data SELECT a.*, b.name, c.status FROM order_info a LEFT JOIN user_info b ON... 1003 app 10.0.0.13:52301 report Query 35 Sending data SELECT a.*, b.name, c.status FROM order_info a LEFT JOIN user_info b ON... 1004 dba localhost NULL Query 0 starting SHOW FULL PROCESSLIST 2001 app 10.0.0.11:52301 report Sleep 0 NULL NULL一眼扫过去,三条一模一样的SQL全部处于 Sending data 状态,慢了三十到四十秒。这SQL是报表接口的主查询,关联了 order_info、user_info 和另外一张表。我立刻做两件事:一是 KILL 掉这三条会话,让接口先恢复;二是把这个SQL拿出来单独跑了一遍,用 EXPLAIN 看执行计划。
EXPLAIN SELECT a.*, b.name, c.status FROM order_info a LEFT JOIN user_info b ON a.uid = b.uid LEFT JOIN product_info c ON a.pid = c.pid WHERE a.create_time >= '2024-11-01' AND a.create_time < '2024-11-08' ORDER BY a.create_time DESC LIMIT 20;EXPLAIN 的结果让我头皮发麻:order_info 表扫了四百多万行,type=ALL,Extra 里没有 Using index;join 的时候 user_info 和 product_info 都没有走索引,全表逐行匹配。但因为接口要分页只取20条,MySQL 的优化器选错了执行计划,先把 order_info 全表扫出来排序,再逐行去关联另外两张表,等于把四百多万行数据全部过了一遍,不慢才怪。
那为什么平时没这么慢?因为以前这个报表接口的数据量在几十万行,走全表扫也就一两秒;现在业务涨到了数百万行,执行计划还是老样子,代价就几何级上升。这就是典型的数据量增长引发执行计划失效的问题。
处理手段分两步走。第一步是加索引:order_info 表在 create_time 和 uid 上建联合索引,user_info 的 uid 和 product_info 的 pid 各自补上索引。这样 WHERE 过滤能走索引,关联也能走索引。第二步是在应用层给接口做分页优化,把深分页的 ORDER BY a.create_time DESC LIMIT 20 改成基于上一次最大ID的下一页查询,避免大偏移量的排序开销。
索引加完后,再跑那条SQL,耗时从40秒降到150毫秒。这就是一个非常标准的流程:processlist 定位 → KILL 止血 → EXPLAIN 分析 → 加索引/改SQL → 验证效果。
3.3 怎么和安全、权限结合使用
说一个很多人忽略的点:show processlist 不是谁都能看全的。在MySQL 5.7之前,普通用户执行 show processlist 只能看到自己账号的会话;MySQL 5.7开始引入了PROCESS权限,拥有这个权限才能查看所有用户的会话。如果你交给开发同学的只读账号没有 PROCESS 权限,他们执行 show processlist 就只能看到自己的连接,那你排查问题时把输出发给对方,很容易出现“为什么我这看不到XX那个连接”的困惑。
所以,如果是团队协作排查问题,建议给相关账号授予 PROCESS 权限(但不是SUPER,SUPER权限太大):
GRANT PROCESS ON *.* TO 'readonly'@'%';另外,KILL操作所需要的权限也一并说明:KILL命令需要 SUPER 权限。如果你用的是云数据库,通常管理控制台里会提供“终止会话”的功能入口,本质上也是调 KILL。
4. 常见问题与排查技巧实录
4.1 各种卡顿状态的应对速查表
平时被人问得最多的,不是“这字段什么意思”,而是“我看到了XX状态,该怎么处理”。我整理一个速查表,每次遇到类似问题都可以直接对照。
| 现象 | 常见状态 | 可能原因 | 处理建议 |
|---|---|---|---|
| 连接很多但CPU很低 | Sleep | 连接池空闲连接过多或未超时回收 | 检查应用连接池配置,调低空闲连接超时时间 |
| CPU高但查询耗时不长 | Sending data | 大量并行短查询+高并发 | 考虑限流、缓存、读写分离 |
| 单条SQL长时间Sending data | Sending data | 缺索引/全表扫描/复杂连表 | 用EXPLAIN分析,补索引或改写SQL |
| 大量线程卡在Waiting for table metadata lock | 元数据锁等待 | 存在未提交的长事务+DDL | 先找长事务KILL,再执行DDL,或使用在线DDL工具 |
| 大量线程处于Updating/Locked | Updating / Locked | 并发写同一行或死锁 | 检查事务提交频率,看是否同一行被频繁更新;死锁则查看死锁日志 |
| UPDATE或DELETE突然变慢 | Updating | 二级索引过多、大事务回滚段膨胀 | 优化表结构、减少冗余索引、拆分大事务 |
| 主从延迟且大量线程Writing to binlog | Writing to binlog | 大事务写入binlog太大,或磁盘IO瓶颈 | 优化大事务,把sync_binlog调整到合理值(权衡安全与性能) |
这张表不是万能药,但覆盖了我工作中90%的场景。剩下10%就属于那种需要深入看 InnoDB 状态、performance_schema 的疑难杂症了。
4.2 你最该养成的三个排查习惯
第一,时刻记住“先保存现场再操作”。每次执行 show processlist 之前,先把结果存起来,哪怕你看到的是一堆怪异的SQL和诡异的锁等待,也要原样存下来。这些现场信息对事后复盘极有价值,特别是在故障总结会上,没有一手现场数据你就只能讲“猜测版”的故事。
第二,KILL 前必须确认线程归属和业务影响。我曾见过有人看到一条 Time 很大的慢SQL就果断 KILL,杀完才发现那是 ETL 在凌晨跑的批处理任务,本来就是要跑5分钟的,结果因为“看起来慢”被误杀了。还有一次,线上一个存储过程的临时表清理连接挂在那边,因为没研判就KILL,直接导致存储过程中途失败,数据没刷完。正确做法是:先看 Info 里的SQL内容,再看用户和来源IP,最后判断这个任务是否关键,宁可留一条观察5秒也不要手滑杀掉。
第三,建立 processlist + 慢日志 + EXPLAIN 的联动分析习惯。单独看 processlist 只能告诉你“病人在发烧”,要找到病因必须配合慢查询日志看历史规律,配合 EXPLAIN 看执行计划,配合 performance_schema 看锁等待和IO统计。我现在每次处理慢查询,都会在三五分钟内把这几样东西全部拉齐,形成一个完整的证据链,而不是凭一条 show processlist 就下结论。
4.3 特殊场景:如何观察而不是干扰
还有一个很多人问的问题:show processlist 本身对数据库有影响吗?会不会因为频繁执行反而把数据库打挂了?
答案是:正常频率(比如每5秒、10秒一次)执行 show processlist 是非常轻量的,它只是读一下内存中的线程列表,不会产生额外的SQL执行,更不会触发锁等待。它的开销比慢查询日志的写入还要小得多。
但有个例外:如果你在脚本里写了个死循环,每0.1秒执行一次 show processlist,那在连接数非常多(比如上千个活跃连接)的情况下,确实会占用一些CPU。我的经验是监控采集频率设置在5秒左右就可以了,既能看到问题,又不会打扰到数据库本身。
如果确实需要高频率观察某一个会话的活动,比如分析一条长查询的实时状态变化,更专业的办法是用 performance_schema 来追踪。比如查询 performance_schema.events_statements_current 表,可以拿到当前线程正在执行的语句和状态,数据比 processlist 更细。或者干脆用SELECT * FROM sys.session,这是MySQL提供的sys库视图,直接封装了 processlist 字段,还额外带了一些进程元数据,用起来很顺手。
5. 从 show processlist 延伸出去:让排查更体系化
5.1 不要把 processlist 当成万能的
如果非要说 show processlist 有什么局限,那就是它只能反映“当前时刻”的快照,没法告诉你“趋势”。SQL慢可能是慢慢变慢的,也可能是一秒钟之内集中爆发的。要想看清楚整个变化的过程,你得结合监控系统做历史趋势分析。
我见过很多运维同学手里拿着 show processlist 的输出一头雾水——明明现在看一切正常,可十几分钟前数据库确实飙了一下。这就是快照式工具的天然缺陷:那一波高峰已经过去了,现场没了。所以更合理的做法是:在数据库侧部署一套基于 performance_schema 的监控(比如用开源方案或者云监控),持续采集活跃连接数、慢查询数、锁等待时长这些指标,然后配合 show processlist 做定点深挖。快照用来“定位”,指标曲线用来“证实”,两者配合才是完整的排查链条。
另外,processlist 也没有历史索引使用情况、缓冲池命中率、redo log 刷盘延迟这些信息。所以如果一条SQL的 query 计划看着没毛病,但执行就是慢,那你需要去看系统层面的资源:磁盘IO负载、临时表空间、网络带宽、内存换页。这些都可能是压垮SQL的最后一根稻草。
5.2 如何把宝贵的现场信息沉淀成团队资产
我养成了个习惯:每次处理完一个棘手问题,把当时的 processlist 快照、慢SQL、EXPLAIN 输出、处理方案整理成一篇简短的笔记,归档到一个共享文档或者wiki里。几个月下来,这些笔记成了团队里最值钱的“故障手册”。
举个例子,我们团队总结过一组“高频故障模式”:比如“同一SQL并发度超过20次且Time都是Sending data,大概率是执行计划退化需要加索引”“一堆Waiting for table metadata lock且最早会话Time超过一分钟,优先KILL最早会话而不是后来的等待者”“Sleep连接长期不释放且来自同一个IP,大概率是连接池泄漏”。这些结论不是哪个官方文档告诉你的,而是从无数次 processlist 输出里归纳出来的。
这套方法对个人成长也非常有效。看 processlist 就像医生看X光片,你多看多积累,就能形成一种“一眼看出问题”的条件反射。从技工到专家的距离,往往就是无数次分析和复盘拉开的。
6. 我踩过的那些坑,和最后两条小建议
6.1 三个很容易被processlist带偏的误区
第一个误区,把Sleep连接当成罪魁祸首。我刚接手数据库运维那阵,看到 processlist 输出里一大片 Command=Sleep 就紧张,以为连接池要炸了。后来才明白,Sleep 只是“空闲连接”,如果连接池配了合理的最小空闲数和超时回收机制,Sleep 存在是正常现象。真正要留意的是 Sleep 连接数量持续增长、从不下降,那才是泄漏的信号。我见过一个应用因为连接池的 validate 配置不对,每轮请求都新建连接,导致数据库层出现大量 TIME_WAIT 状态的连接堆积,表现就是进程列表里瞬间多出几百个 Sleep。
第二个误区,看到 Time 大就杀,不关心业务语义。Time 大不代表SQL是垃圾,也可能是系统在干正经活。举个例子:一个每小时跑一次的数据归档任务,全量更新几亿行数据的归档表,Time 跑到两三分钟都是正常的。你如果手一抖把它杀了,数据归档不完整,业务第二天看到数据对不上,那才是大事故。真正的坏SQL,是“又慢又没有价值”的那种——重复查询相同数据、没有合理索引、在大表上做全扫描。判断标准不是Time,而是它做的事和它消耗的资源是否匹配。
第三个误区,只看 processlist 不看 error log 和慢日志。processlist 告诉你的是当下发生的事情,但很多问题的根源在几分钟甚至几小时之前。比如锁等待,processlist 里看到的是一堆以“Waiting for table metadata lock”的状态挂在那里,但最早是哪个会话持有的锁?这个会话之前执行过什么?这些信息 processlist 只给你一个快照,必须配合慢日志、binlog 或者 general log 才能拼出完整图景。
这几点不留意,很容易白忙活一场,最后还得出个错误结论,把问题定位到了无关的地方。
6.2 我的个人习惯:给processlist套上自动化
经常要救火的人,最怕的就是在故障发生的第一时间手忙脚乱。所以我给自己搭了一个简单的自动化报警逻辑:写了一个小脚本,每隔10秒采集一次 processlist 并做基础判断——如果有 Command=Query 且 Time>30秒的线程、或者有 Waiting for table metadata lock 状态的线程、或者活跃线程数超过某个阈值,就把现场快照自动存下来,并往值班群推送一条告警。
#!/bin/bash # 每分钟跑一次快照,保存可疑问题现场 mysql -uroot -p*** -e "SHOW FULL PROCESSLIST;" >> /var/log/processlist_$(date +%Y%m%d%H%M).log mysql -uroot -p*** -e " SELECT id, user, host, db, command, time, state, LEFT(info, 100) FROM information_schema.processlist WHERE command='Query' AND time>30 " >> /var/log/suspect_query_$(date +%Y%m%d%H%M).log这种预案的价值在于:真正故障来的时候,你不需要依靠“人记得执行命令”这个脆弱环节,系统会自动帮你把现场留下来。事后分析的时候打开日志,就能直接回溯故障发生的具体时刻,数据库的状态一目了然。
我处理线上问题这些年,一个最深的感觉是:所谓经验不是看了多少文档,而是面对真实故障时,能够冷静、有序、有章法地一步步逼近真相。show processlist 就是你工具箱里最基础也最好用的一件工具,把它练熟,比收藏一百篇“性能调优教程”都管用。
看完这篇内容,建议你下次遇到数据库卡顿问题时,别急着翻慢日志,先敲一遍SHOW FULL PROCESSLIST,把输出完整看一遍,再对照这篇文章里的状态表和排查流程一步一步来。多实践几次,你会发现在数据库的世界里,最有价值的那种“庖丁解牛”感,恰恰就是在这种看似简单的基础命令里练出来的。