MySQL进程KILL不掉?事务提交回滚卡死的排查与处理 做 MySQL 运维或者后端开发的人几乎都会碰到一个特别憋屈的场面一条 SQL 卡在那里跑不动你想把它 KILL 掉结果 KILL 指令发出去之后线程在SHOW PROCESSLIST里确实变成了 Killed但就是不退出与此同时这个连接上的事务既提交不了、也回滚不了后面的请求全部堵住数据库就像被人按了暂停键。这种“MySQL 进程 kill 不掉事务提交或回滚不了”的现象在业务高峰期、大促前或者半夜跑批的时候特别常见。先别急着反复执行 KILL也别一上来就重启数据库搞不好问题会越搞越大。这篇就把我实际操作中踩过的坑、排查思路和处理手法摊开讲一讲你如果正好遇到类似现场可以直接照着这套流程去定位。1. 先弄明白KILL 到底干了什么为什么它会失败1.1 KILL 本质上只是“发信号”不是强制终止很多人对 KILL 有一个误解以为它像操作系统里的kill -9一样可以把一个线程直接抹掉。MySQL 里的 KILL 并不是这么设计的。它本质上是给目标线程设置一个“终止标志”真正的清理动作还要由那个线程自己走到一个安全的检查点之后才能执行。MySQL 在服务层把 KILL 分成了两种语义KILL QUERY id只终止当前正在执行的查询连接保持不断开会话和事务状态保留。KILL CONNECTION id或者简写成KILL id终止整个连接包括查询、会话和连接状态。无论是哪种最终都要等目标线程“自己愿意退出”。InnoDB 引擎在执行 SQL 的过程中会在某些循环节点检查这个终止标志。如果线程正卡在一个无法返回的地方——比如等待一个锁、等待一次磁盘 IO、或者在回滚一个巨大的事务——那它根本没有机会走到检查点表现出来就是你 KILL 了线程还在那里挂着。MySQL 官方帮助文档里有一句话很关键如果用户线程在存储引擎内部被卡住KILL 命令可能无法立即生效线程会一直持续到外部条件解除。这句话基本概括了我们在生产环境里遇到的所有尴尬情况。1.2 事务为什么既提交不了、也回滚不了KILL 一个连接之后InnoDB 需要对该连接上未提交的事务做回滚处理把已经修改的数据页恢复到事务开始之前的状态。这个恢复过程靠的是 undo log。理论上回滚是必然要发生的但问题在于回滚本身可能是重活、慢活。举个具体的例子某个跑批任务往一张千万级别的表里批量 UPDATE一次事务改了 500 万行。这个时候你去 KILL 它KILL 信号是收到了InnoDB 会立刻进入ROLLING BACK状态。但回滚需要逐条读取 undo log再把原来的数据写回去这个工作量基本和当初执行 UPDATE 的工作量是对等的有时候甚至更慢因为还要额外处理锁信息和清理操作。在回滚完成之前这个事务占用的行锁、间隙锁、MDL 锁全都还捏在手里。后果是什么呢就是其他事务要么在等待这个锁要么看到这个事务处于 ROLLING BACK 状态后产生死锁检测而当前这个连接你又不能再发任何命令——因为它已经在清理途中了不是不响应你是没法响应你。最终你站在控制台前左等右等状态栏里那个 Killed 就是一直不消失。1.3 KILL 失效的几种典型场景这几种情况我都在真实环境里遇到过你可以对号入座大事务回滚中事务修改行数几十万甚至上百万KILL 后长时间显示 Killed 或者 Rolling back。等待 MDL 锁一个会话持有了表的元数据锁另一个会话被阻塞你去 KILL 被阻塞的会话时经常发现它半天没反应因为它正卡在等待锁的环节根本抽不出身去处理 KILL 信号。磁盘 IO 打满或磁盘故障线程在等待 IO 完成整个 InnoDB 都在等待 IO 子系统返回KILL 指令排不上队。内存不足导致系统 swap 严重mysqld 进程的线程都在等内存页换入换出所有操作都很慢KILL 也跟着一起慢。诊断卡死 / 数据字典锁某些版本在特定操作下会卡在数据字典层面线程无法响应终止信号。2. 拿到现场之后先判断它到底卡在哪一步2.1 第一眼看 processlist别只看有没有 Killed很多人排查的第一步就是SHOW PROCESSLIST但只看这一条不够。我一般会先执行SHOW FULL PROCESSLIST把完整的 SQL 文本也带出来因为 SQL 往往能直接告诉你它卡在什么地方。SHOW FULL PROCESSLIST;或者直接查 information_schemaSELECT id, user, host, db, command, time, state, info FROM information_schema.processlist ORDER BY time DESC;重点关注 state 一列。不同的 state 代表完全不同的卡点Killed说明 KILL 信号已经发出正在等待回收。如果持续好几分钟多半是事务回滚中或者线程卡在 IO 等待。Sending data这是一个大坑名字有迷惑性。它其实表示正在执行 SELECT 并把结果数据发到客户端也可能是临时表排序、文件排序大量数据读取时经常挂在这里。Waiting for table metadata lock典型的 MDL 锁等待后面会有专节讲。Waiting for handler commit等待存储引擎完成提交一般和 redo log 刷盘有关。Rolling back明确告诉你正在回滚一个大事务。如果 processlist 里一堆Sleep状态的连接也别忽略。一个处于 Sleep 状态的连接不代表它没有事务。应用层把 autoCommit 关掉之后在同一个连接上发了多条 SQL然后一直保持着连接不释放这时候连接表面上是 Sleep实际上事务可能已经开启很久了持有大量锁。2.2 查 InnoDB 事务视图锁定真正的事务归属从 processlist 只能看到线程看不到事务。要想知道谁在真正占着资源必须查information_schema.innodb_trxSELECT trx_id, trx_state, trx_started, trx_mysql_thread_id, trx_query, trx_rows_modified, trx_operation_state FROM information_schema.innodb_trx;这里有几个字段我每次都会重点关注trx_started事务开始时间。如果这个时间已经过去几个小时这基本就是一坨陈旧大事务优先级最高。trx_state状态如果是LOCK WAIT说明事务在等锁如果是RUNNING说明事务正在执行如果是ROLLING BACK说明它在痛苦地回滚。trx_rows_modified修改了多少行这个数字直接决定了回滚要花多久。几十万以上的行数回滚时间基本是按分钟甚至小时算的。trx_operation_state正在做什么操作比如Rolling back还是starting index read。举个例子我看到过最极端的一次一个事务执行了 4 个多小时修改了 800 万行KILL 之后回滚了将近 3 个小时。这段时间里整个业务连这个表的写操作全部堵死你敢信这就是真实生产环境里正在发生的故障。另外在 MySQL 8.0 里还可以用performance_schema.data_lock_waits看更细的锁等待细节8.0 对锁信息的采集比 5.7 全面很多。2.3 查锁等待链找到谁在阻塞谁如果事务状态是LOCK WAIT就一定要把阻塞链找出来。常用的查询是 sys 库自带视图SELECT * FROM sys.innodb_lock_waits;这个视图会告诉你哪个事务在等待哪个事务在持有锁。类似于链式结构A 等 BB 等 C最后找到根因的那个人。我把这个视图比作“交通拥堵时的探头画面”它能直接拍到你前面那辆车为什么不走。还有一个辅助手段是SHOW ENGINE INNODB STATUS\G它输出的LATEST DETECTED DEADLOCK和TRANSACTIONS段落能提供事务和锁的实时快照。虽然这个输出很长但里面信息密度很高尤其是事务状态和等待锁的类型值得逐行读一遍。SHOW ENGINE INNODB STATUS\G如果 MySQL 8.0 的 performance_schema 里metadata_locks表可查还可以直接查 MDL 锁的持有情况SELECT OBJECT_TYPE, OBJECT_SCHEMA, OBJECT_NAME, LOCK_TYPE, LOCK_STATUS, SOURCE FROM performance_schema.metadata_locks WHERE OBJECT_SCHEMA 你的库名;3. 分原因处理从最轻量到最强制按次序来3.1 如果是 MDL 锁等待杀被阻塞者没用要去杀持有者先说一个我见过很多次的误区。业务侧反馈某张表所有查询都超时开发同学一看 processlist发现一个线程卡在Waiting for table metadata lock然后顺手就把它 KILL 了。结果 KILL 之后下一个线程又卡进来无限循环原因就是在他们背后还蹲着一个持有 MDL 锁的连接。MDL 锁是 MySQL 在 server 层对表结构加的锁只要有会话拿着这张表的 MDL 锁其他所有 DML 和 DDL 都会被挡在外面。最常见的持有者场景是一个连接里执行了ALTER TABLE正在执行或者还没提交。一个连接执行完查询结果集还没完全发给客户端事务也没结束。一个连接长时间处于 Sleep但事务没有提交把 MDL 锁一直攥着。处理 MDL 锁等待的正确思路是先找到持有者再考虑怎么处理持有者。查询持有者的方法在 2.3 节已经写过核心就是通过sys.schema_table_lock_waits去看阻塞关系。SELECT * FROM sys.schema_table_lock_waits\G从输出中能看到waiting_thread_id和blocking_thread_id直接按照blocking_thread_id去 processlist 里找对应连接再评估这个连接能不能被杀。需要特别注意的是如果持有 MDL 锁的会话只是 Sleep上面还带着一个未提交事务贸然 KILL 它也会进入回滚流程。所以在杀持有者之前最好先看一眼information_schema.innodb_trx里对应的trx_rows_modified评估一下杀它的代价。3.2 如果是大事务回滚KILL 成功只是万里长征第一步当你对一个正在执行大 UPDATE 的连接执行 KILL CONNECTION 后连接状态会变成 Killed但真正的大戏才刚刚开始。InnoDB 会启动回滚流程此时你不能再对它做什么了唯一能做的就是监控回滚进度。MySQL 从 5.7 开始在SHOW PROCESSLIST里可以看到 state 列为Rolling back同时information_schema.innodb_trx里对应事务的trx_state也是ROLLING BACK。但具体回滚到百分之多少MySQL 本身没有直接的进度接口。我用的土办法是盯着两个指标trx_rows_modified的数值变化它会随着回滚的推进逐渐减少最终归零。processlist 里事务是否消失。我写过一个粗糙的监控脚本大概就是隔 10 秒去查一次innodb_trx把 trx_rows_modified 打印出来观察递减速度用来估算剩余回滚时间。这个方法不精确但在现场足够用了。SELECT trx_id, trx_state, trx_started, trx_rows_modified, NOW() AS check_time FROM information_schema.innodb_trx WHERE trx_state ROLLING BACK;回滚期间需要做什么我的建议是别再去 KILL 其他相关事务等它回滚完。把该表的写入流量暂时切换走或者让应用侧做降级。通知业务方明确告知这个表会有多长时间不可写。千万注意回滚大事务期间如果你再去重启 MySQL重启后 InnoDB 的崩溃恢复还是会把这个未完成事务再回滚一遍该等的时间一分都不会少甚至因为 crash recovery 要处理的问题更多恢复时间可能更长。3.3 如果是锁等待和死锁回滚也有自己的优先级如果事务卡在LOCK WAITKILL 通常是能成功的因为 MySQL 会主动唤醒等待锁的线程让它放弃等待并回滚事务。但如果你发现 KILL 之后它还是不动就要考虑是不是 InnoDB 内部死锁检测机制出了状况。MySQL 默认开启了死锁检测检测到死锁时会回滚一个代价更低的事务。但死锁检测本身在大量事务竞争时会消耗不少 CPU而且它和锁等待的冲突处理偶尔会表现得比较迟钝。这时候真正的问题不是 KILL 失效而是你该直接处理持有锁的那一方。找到持锁方之后无非是两个方向持锁方是零星的小事务且业务可以中断直接 KILL。持锁方是大事务KILL 的代价太高看是否能等它在正常周期内提交。这里有一个经验法则如果持锁方已经运行了很久并且业务对这个事务的容忍度很低宁可牺牲这个小事务来解局也不要让它拖垮整个实例。现场处置的第一优先级永远是“恢复可用性”而不是“保住所有事务”。3.4 如果是物理资源耗尽杀线程之前先看 IO 和负载有时候你 KILL 不掉不是 MySQL 的问题而是整个机器的 IO 已经打满了。mysqld 的线程全在等待磁盘返回包括执行 KILL 的回包也可能排队。排查命令其实不复杂iostat -x 1 10 vmstat 1 10 free -h重点关注%util是否接近 100%wa是否居高不下。如果 IO 已经 100% 打满你这时候 KILL 一个大查询可能反而加剧回滚导致的写放大因为回滚也是要写数据页的。处理优先级应该是先看是什么在消耗 IO可能是其他实例、备份任务、或者当前实例的某个大查询。把消耗 IO 的外部任务暂停给数据库喘口气。然后再决定要不要 KILL 目标查询。如果是因为内存不足导致 swapfree -h能看出来。swap 严重时任何操作都慢如蜗牛KILL 信号也照样排队。这个场景的处理往往需要重启才能缓解但在重启之前要先把磁盘上的脏状态想清楚。4. 最坏情况下的兜底方案4.1 让 InnoDB 自己完成恢复innodb_force_recovery 的正确用法如果前面所有手段都无效事务回滚就是死活完不成或者连 processlist 都查不出来了那就只剩下重启和强制恢复两条路。而innodb_force_recovery是最后的基于数据库自身的兜底手段。参数有 1 到 6 六个等级。简单说1忽略损坏页继续尝试恢复一般不会造成数据丢失。2阻止主线程运行。3回滚不执行只恢复到一致状态。4不计算统计信息。5不执行 undo log 回滚启动后数据库以只读方式访问。6连日志也不做前滚通常是最不得已时才用。注意5 和 6 级别的启动是以牺牲数据一致性为代价的启动后数据库基本是只读的业务不可能正常运行。它们只是在数据救急时用来把表数据导出来。我的建议是从 1 开始试逐级往上能到哪一级就停在哪一级。不要上来就设 6因为级别越高数据丢失风险越大。设置方法[mysqld] innodb_force_recovery 4在这个模式下启动后目标不是让业务恢复而是把数据尽量抢救出来比如用mysqldump导出或者直接复制数据文件。抢救完毕立刻把这个参数移除再正常重启。4.2 系统层面强制结束 mysqld 进程的利弊当你发现连mysqladmin shutdown都执行不动的时候就必须考虑系统层面的处理了。有两条路一是给 mysqld 发 SIGTERM让 MySQL 走优雅关闭流程kill $(cat /var/run/mysqld/mysqld.pid)优雅关闭会等待所有事务处理完成。如果问题本身就出在一个回滚不完的大事务身上优雅关闭一样会卡住可能等多久都关不掉。二是发 SIGKILLkill -9 $(cat /var/run/mysqld/mysqld.pid)这个操作会直接终止进程但不要天真地以为这样就不用回滚了。进程重启后InnoDB 的崩溃恢复机制会扫描 redo log 和 undo log那些未提交事务照样要回滚。换句话说大事务该占用的时间还是占用只是把时间从“业务持续不可用”变成了“重启后恢复阶段不可用”。好处是什么好处是你能争取到一个相对明确的恢复窗口而且可以在重启前做一次整体环境的梳理比如扩充磁盘、调整参数。坏处是如果数据文件有损坏崩溃恢复可能失败要配合 innodb_force_recovery 使用。我的现场经验是大事务回滚超过 3 个小时还看不到头并且业务已经完全不可用与其干等不如果断 kill -9 后重启。因为回滚迟早要做晚做不如早做至少给业务一个明确的恢复预期。4.3 重启后的恢复时间预估与观测点重启之后别急着接流量。先看日志InnoDB 恢复阶段会打印类似Applying batch of N row changes的信息。tail -f /var/log/mysql/error.log重点观察InnoDB: Starting recovery之后是否快速推进。InnoDB: Rollback of uncommitted transaction如果出现了说明事务回滚开始了。日志最后出现InnoDB: Recovery completed才算恢复结束。恢复期间尽量不要去操作数据库甚至不需要登录因为它忙不过来。等恢复完成后再检查一次information_schema.innodb_trx确认没有残留事务然后逐步放开业务流量。我见过有人在恢复期间强行打开数据库查询结果把恢复进程干扰了导致数据文件损坏范围扩大最后只能从备份恢复这是真的很惨痛的教训。5. 现场排查速查表与长期避坑经验5.1 症状-原因-处理对照表我把常见情况整理成一张表你可以在现场直接对照省得临时想现象可能原因优先级动作线程显示 Killed 持续不消失大事务回滚中查 innodb_trx 估算回滚行数和时间线程显示 Waiting for table metadata lock有会话持有 MDL 锁查 schema_table_lock_waits杀持有者线程显示 LOCK WAIT锁等待或死锁查 sys.innodb_lock_waits定位阻塞源头实例整体响应缓慢KILL 没反应IO 瓶颈或 swap用 iostat/vmstat 确认资源先解除资源瓶颈事务删不掉也提交不了stateROLLING BACK回滚耗时过长评估继续等待 vs kill -9 重启重启后仍无法启动数据文件损坏从 innodb_force_recovery1 开始逐级尝试这张表我直接贴在了自己的笔记本内页每次处理类似问题都会先看一眼从最轻量、最安全的手段开始而不是一上来就重启。5.2 怎么从源头避免“KILL 不掉”的局面说句实话等到 KILL 不掉再处理已经是被动了。主动防御的价值远大于事后救火。我复盘过几次重大故障发现根源几乎没有意外全是大事务、长事务和锁使用不当。几个我现在一直在用的检查动作第一监控大事务。数据库中设置一个定时任务每 5 分钟扫一次information_schema.innodb_trx凡是trx_started超过 10 分钟的事务都要告警。重点看trx_rows_modified超过某个阈值我一般设 5 万行直接推送给值班人员。第二拆分批量操作。开发同学喜欢一个 UPDATE 更新全表百万行执行很快但回滚很慢。我要求所有批量更新改成按主键分批执行每次只更新几千行然后提交事务。这样即使出问题KILL 掉也只是损失一批不会把所有业务拖死。第三调整合理的锁等待超时。innodb_lock_wait_timeout默认 50 秒对于在线交易系统来说太长。很多公司把它调到 5 秒甚至 3 秒让锁等待的事务快速失败而不是无限堆积。当然这个值也不能太小否则业务稍微有一点高峰就会大量报错需要根据业务特点反复打磨。第四严格排查长连接里的隐式事务。应用的连接池如果没有正确提交事务或者中间件把 autocommit 改了就会产生很多 Sleep 状态但持有事务的连接。这种问题难发现但危害极大。我建议在数据库侧开启init_connect‘SET autocommit1’来兜底大部分场景。第五在全量 ALTER 操作前先评估 MDL 锁的影响。特别是大表加索引、加字段建议使用在线 DDL 工具并且避开业务高峰期执行。5.3 几点个人体会后面这一点是我个人在实际处理故障中最深的感触算不上什么官方建议纯粹是经验之谈。第一遇到 KILL 不掉的情况心态上先别慌因为你越是反复 KILL越容易把 InnoDB 的内部分布式事务状态搞得更复杂。KILL 一次就够了剩下的交给回滚。第二动手之前先把现场信息收集齐。processlist 快照、innodb_trx 快照、锁等待快照、引擎状态快照这四样是后续做分析和复盘的基础材料。现场信息比任何记忆都可靠。第三杀进程之前永远先确认有备份或者至少知道备份集在哪里。如果数据库因为强制恢复之后数据不完整你至少还有退路。最后再分享一个小技巧如果你在生产环境里需要快速判断一个 KILL 是否真的生效可以看performance_schema.threads里的PROCESSLIST_STATE字段如果它停在 Killed 上超过 30 秒基本上就说明进入回滚流程了这个时候你盯着 processlist 也没用不如赶紧评估回滚时间、通知业务方然后把监控脚本跑起来。这个内容后续还可以扩展的方向是把事务监控接入统一的告警平台让 KILL 不掉这种事在发生之前就被拦截下来。