
午后的告警群里突然开始刷屏某个实例的活跃连接数一路走高应用侧反馈一批查询全部卡住努力几次都等不到结果。我按习惯先看show processlist发现一堆连接长时间处于Waiting for table metadata lock或Sending data于是顺手执行KILL结果屏幕上的状态从Query变成了Killed线程却依然挂着。再查事务列表一个超长事务始终处于Running既没有提交也没有回滚。这正是标题里那句MySQL 进程 kill 不掉事务提交或回滚不了的典型现场。这篇文章想把这类故障的排查思路完整梳理一遍它本质上不是杀进程这一个动作而是牵涉连接状态、InnoDB 内部事务、日志刷盘、锁等待等多个环节。适合被线上事务卡死折磨过的 DBA、运维以及需要自己定位问题的业务侧开发参考。1. 先分清kill 不掉到底属于哪一类表象很多人一上来就执行kill -9或者反复KILL但实际效果并不稳定。原因很简单MySQL 进程杀不掉不是单一现象不同表象对应完全不同的处理路径。先做个分类再决定动作。现象直观判断常见根因下一步方向命令行敲完没反应进程忙连接层无法处理新命令线程卡在内核态或日志刷盘受阻检查 IO 状态看是否是硬件瓶颈状态变Killed但线程还在MySQL 已标记中断但事务未结束事务在执行回滚或等待锁、等待 IO查看事务状态和回滚进度事务既不能提交也不能回滚新查询全部卡住事务列表不变化两阶段提交卡住或 binlog/redo 写入受阻定位日志落盘链路而不是继续 kill这张表的重点在于Killed状态本身不意味着线程马上会消失。它只是告诉外部这个连接已经被标记为终止真正的物理线程要等当前事务处理到某个安全点才会退出。1.1 命令直接没响应先怀疑底层资源而不是 SQL 本身如果执行kill query 123后连命令本身都迟迟不回包通常说明 MySQL 的线程调度已经非常吃力。常见情况有三种内存 swap 严重物理内存不足线程被换出调度回来要拖拽巨量页面。存储 IO 挂起数据文件所在的磁盘正在经历极端延迟redo 刷盘线程等不到写完成。锁等待体系内部死锁并发线程都持有一把资源并等待另一把单靠 kill 无法解开。这种环境下单线程层面的KILL往往不解决问题因为目标线程根本没有机会执行退出逻辑。排查时应该立刻看系统层的iostat、vmstat、pidstat确认磁盘读写和 CPU 等待情况。如果所在环境允许再检查线程的堆栈信息判断是卡在futex等待还是卡在 IO 提交。1.2 状态已经变成Killed但线程一直不退这是标题里最常见的一种伪 kill 不掉。MySQL 在收到 kill 指令后会把线程状态置为Killed但这个线程不一定能马上退出它可能在执行大回滚、可能在等待锁、也可能正等在一个不可打断的内核操作上。从业务视角看此时连接还在耗着连接数事务的锁还在持有其他事务不断排队故障并未真正结束。这里有一个常被忽略的细节KILL分为KILL QUERY和KILL CONNECTION两种语义。前者只是终止当前正在执行的语句连接和事务上下文还在后者是终止整个连接。但如果事务已经开始回滚KILL CONNECTION也不会瞬间释放所有资源InnoDB 需要把未完成的事务回滚干净才能释放行锁。官方文档里对Killed 状态有明确说明它不是一个错误而是线程正在收尾的信号。实际经验是不要反复对一个 Killed 线程继续发 kill多次操作不会提速反而可能让监控告警更混乱。1.3 事务既提交不了也回滚不了锁或者日志大概率出问题当一个新的小事务去更新某一行时发现等锁等不到去提交时binlog 又刷不进去此时表现就是既不能提交也不能回滚。这通常意味着有一个大事务长时间占用行锁且该事务本身处于非活跃状态。两阶段提交的第一阶段完成了但 binlog 写入卡住。半同步复制场景下等待备库 ack 超时主库提交被迫挂起。处理这类问题时show processlist只是辅助真正的核心数据在 InnoDB 的事务和锁视图里。我的习惯是先查information_schema.innodb_trx找trx_started最早、trx_state为RUNNING的事务再看它锁住了哪些表、累计修改了多少行。定位到大事务后再决定是等待、干预还是直接重启实例。2. 从 processlist 到事务视图串起完整的排查链路排查这类故障最重要的是别猜看状态。MySQL 本身提供了很多可视化信息的入口只是它们分散在不同视图和命令里。把下面这条链路走一遍绝大多数kill 不掉的根因都会浮出来。2.1 show processlist 里的 State 不要跳过show processlist的输出里State字段经常被当成辅助列扫一眼就略过但它恰恰是最直接的线索。mysql show full processlist; ---------------------------------------------------------------------------------------------------- | Id | User | Host | db | Command | Time | State | Info | ---------------------------------------------------------------------------------------------------- | 101 | app | 10.0.0.8 | order | Query | 3600 | Waiting for table metadata lock | update order set ... | | 102 | app | 10.0.0.9 | pay | Query | 4000 | Sending data | select ... | | 103 | app | 10.0.0.9 | NULL | Kill | 10 | Killed | NULL | ----------------------------------------------------------------------------------------------------看到Waiting for table metadata lock时第一反应不是去 kill 这个查询而是去找谁持有元数据锁。这个锁往往来自另一个连接上未提交的 DDL或者一个打开的事务。Killed状态则提醒你回滚正在进行时间取决于事务规模。Sending data表面上是正在发送数据但在大结果集或大事务里它也可能意味着网络层写缓存阻塞需要结合耗时判断。2.2 用 information_schema 和 sys 库找到真正的事务只看 processlist 很容易被表象迷惑。更可靠的做法是直接查事务和锁等待SELECT trx_id, trx_state, trx_started, trx_rows_modified, trx_query FROM information_schema.innodb_trx ORDER BY trx_started;这个表会列出所有 InnoDB 活动事务。重点关注三列trx_started事务开始时间越早越可能是阻塞源头。trx_rows_modified行数越大回滚成本越高。trx_stateRUNNING是正常执行LOCK WAIT说明在等锁。再进一步用 sys 库的锁等待视图可以直接看到谁锁谁SELECT * FROM sys.innodb_lock_waits\G这条查询会输出waiting_trx_id、blocking_trx_id、waiting_query、blocking_query等字段一条命令就能判断阻塞链的根节点。生产环境里我经常是先跑这第一条再跑第二条基本三分钟内能锁定目标事务。2.3 表结构变更和未提交事务的组合坑还有一种非常隐蔽的情况业务在执行一条ALTER TABLE之前某个老连接上有一个未提交的事务。哪怕这个事务只是简单地做了SELECT ... FOR UPDATE且一直不提交后续所有针对该表的SELECT/UPDATE/DELETE都会卡在Waiting for table metadata lock。这时候KILL掉那些卡住的查询没有意义真正该做的是找到那个老事务并结束它。判断依据同样简单innodb_trx里存在一个trx_started很早、trx_stateRUNNING、但trx_queryNULL的事务。trx_query为 NULL 不表示没有行动而是此刻没有正在执行的语句事务的锁照样持有。处理时先确认是不是遗留连接再决定 kill 哪个 session避免误伤正常请求。3. 真正让事务无法终结的是拿不到的锁和写不动的日志很多情况下进程之所以杀不掉是因为它正卡在一个无法继续、也无法中断的状态。这背后通常有两条关键链路InnoDB 层拿不到需要的锁以及 MySQL 上层写不进去 binlog/redo。两条链路看似独立实际会绕成一条死结。3.1 行锁、间隙锁与元数据锁等不到的对象各不相同InnoDB 的行锁是事务内自动获取的不需要手动LOCK但在高并发更新同一批数据时如果一个大事务一直不提交其他小事务的UPDATE就只能排队。这类等待在sys.innodb_lock_waits里看得一清二楚。间隙锁则更隐蔽两个并发事务在相邻范围插入数据时可能互相等待对方的间隙锁释放形成死锁环。元数据锁MDL是另一个容易被忽视的环节。MySQL 5.5 之后DDL 和 DML 通过 MDL 保证一致性。一个事务哪怕只是打开了事务却没有提交也可能持有 MDL 的共享锁使后续ALTER TABLE永久等待反过来一个等待中的 DDL 又会阻塞后面所有 DML。这种雪球效应会直接表现为大量连接堆积。遇到这种情况先杀 DDL 的等待者往往还不够要回溯到最早持有事务连接的事务。3.2 日志落盘阻塞提交卡在最不该卡的环节事务提交时主要有两条写路径InnoDB 的 redo log 和 MySQL 上层的 binlog。如果innodb_flush_log_at_trx_commit1每个事务提交都要刷 redo 到磁盘如果sync_binlog1binlog 也要同步刷盘。磁盘一旦出现极端延迟或 IO 队列积压事务会一直处于prepare或commit阶段从业务侧看就是提交不了。半同步复制场景更典型主库在提交时等待备库确认接收 binlog若备库宕机或网络中断主库连接会堆积在等待 ack 的状态。此时对事务执行 kill进程表现就是杀不掉因为主库线程正陷入日志确认流程无法响应新的指令。我的处理习惯是先查show slave status和半同步相关的状态变量确认复制链路是否健康再决定是等网络恢复还是直接切异步降级而不是对着已卡住的线程反复发指令。3.3 从能提交却提交不了反推出日志故障有一次线上出现一种奇怪现象所有小事务读写都正常但一提交就报错或者干脆卡住。当时show processlist里全是散落的小查询事务列表里却没有大事务。后来通过SHOW ENGINE INNODB STATUS看到redo log 文件所在磁盘 IO 延迟超过了几十秒Log sequence number和Log flushed up to之间的差值不断拉大。本质是日志写链路已经瘫痪事务根本无法走到最终提交。这时候再多的KILL也无济于事因为被卡住的不是单个线程而是整个存储层的提交路径。所以排查事务提交或回滚不了时必须同步看SHOW ENGINE INNODB STATUS里的LOG段确认 LSN 是否还在推进。LSN 活着说明 InnoDB 本身能处理数据LSN 停滞就要从磁盘能力和 IO 队列入手排查。4. Killed 之后进程仍在按这三层去处理当排查链路走到这一步通常已经锁定目标事务了。接下来要区分三个层面MySQL 内部连接、操作系统进程、以及 InnoDB 大回滚。每一层的处理手法不一样。4.1 MySQL 内部的 KILL 语义确认你的操作对象MySQL 的KILL需要精确区分对象-- 终止某个连接上的当前语句 KILL QUERY 123; -- 终止整个连接等价于断开该会话 KILL CONNECTION 123;如果连接 ID 是别的业务在用的正常连接误杀会造成请求中断。所以在 kill 之前先看processlist和innodb_trx确认trx_mysql_thread_id和 processlist 里的Id是否对应。KILL QUERY只取消当前语句但对已经进入回滚阶段的事务没有作用KILL CONNECTION会强制断开连接但 InnoDB 内部该回滚的事务还是要继续回滚只是外壳线程会被回收。4.2 操作系统层要不要碰kill -9很多 DBA 在 MySQL 线程无法退出时第一反应是找mysqld主进程来kill -9。这个动作不应作为常规手段它会让整个实例直接退出所有连接全部断开崩溃恢复后 InnoDB 依然需要做崩溃恢复也就是把所有未提交事务回滚掉。如果目标是从大事务干扰中快速恢复服务重启实例确实比等待回滚更快但代价是丢失所有在线连接状态同时在恢复期间仍有短暂的写入拒绝。如果只想针对某个线程直接用操作系统层kill -9某个线程号逻辑上是不可行的因为 MySQL 线程是被主进程管理的内部线程操作系统信号无法只杀死其中一个 InnoDB 工作线程而不破坏整个进程。真正要做的是通过 MySQL 层的会话机制去终止或者直接在主进程层面做整体重启的决定。所以在操作系统层能做的是检查整体负载和 IO 状态确认是否必须重启而不是去单独终结某个内部线程。4.3 大回滚场景不是没反应而是在慢慢滚当你看到一个连接长时间处于Killedinnodb_trx里对应事务的trx_state仍为RUNNING且trx_rows_modified的数字在缓慢变化时说明回滚正在推进。这时候最忌再发一个新KILL QUERY去打断因为回滚本身是原子的中途打断并不会加速反而可能阻塞内部回滚线程。观察回滚进度的技巧SELECT trx_id, trx_rows_modified, trx_started FROM information_schema.innodb_trx WHERE trx_state RUNNING;连续执行两次该查询对比trx_rows_modified是否持续下降。如果数字在动说明正在回滚如果完全不动且系统负载正常才需要怀疑是回滚线程本身被其他东西阻塞。很多大事务回滚要几分钟甚至几十分钟此时业务侧能做的往往是加连接数缓冲、摘流量而不是等待一条 SQL 立刻消失。5. 修复后的验证闭环与长期预防故障恢复不代表问题终结。真正有价值的是把为什么 kill 不掉的根因记录在案再落到监控与参数配置上。每次处理完这种事故之后我都强制自己执行一套验证清单同时把经验沉淀成自动化检查项。5.1 修复后的验证清单# 1. 确认没有 Killed 状态的残留线程 mysql show processlist where state like %Killed% or command Kill; # 2. 确认事务表没有遗留的 RUNNING 事务 mysql select count(*) from information_schema.innodb_trx where trx_state RUNNING; # 3. 确认锁等待清零 mysql select * from sys.innodb_lock_waits; # 4. 确认 redo 和 binlog 推进正常 mysql show status like Innodb_log_lsn_current; mysql show master status\G上面四条全部通过才说明故障在数据层面真正解除。如果sys.innodb_lock_waits还有输出那必须继续追阻塞链根节点直到锁等待为空。很多时候进程消失了、连接断开了但事务表还留着残留状态那说明 InnoDB 内部还没做完收尾需要继续观察。5.2 关键参数与监控项不需要一上来就改大遇到锁等待和事务卡死有人习惯把innodb_lock_wait_timeout调大认为多等一会儿就能熬过去。这个想法很危险参数只控制单条语句等待锁的最长时间不解决阻塞源头。大事务该锁的还是锁调大 timeout 只会让排队的小事务等更久放大雪崩。我更推荐根据业务情况设置合理的默认值参数建议方向说明innodb_lock_wait_timeout10~20 秒既给正常事务留余地又不至于长时间排队transaction-isolationREAD COMMITTED业务不依赖 REPEATABLE READ 时可减少间隙锁max_connections按业务峰值 30% 余量防止连接打满后故障更难恢复lock_wait_timeout与业务超时联动元数据锁等待也需要有界innodb-old-threshold-time视事务时长而定只是辅助统计不作为根因监控侧的沉淀更关键。我在监控面板上加了三件事innodb_trx里存在超过 30 秒的活跃事务时告警sys.innodb_lock_waits有记录时告警Killed状态的连接数超过阈值并持续时间较长时立刻告警。这一组指标能提前暴露大量事务治理问题比等业务反馈卡了再介入要主动得多。5.3 大事务拆分与 DDL 平滑执行回到根因这类故障十有八九和大事务有关。大事务不只意味着长时间持有锁还会让 binlog 体积暴涨让主从复制延迟放大。日常治理上我的建议是大批量更新拆成批处理每批控制在几百到一千行批次之间加短暂停顿。DDL 时间尽量安排在低峰期并在执行前检查是否存在长事务。应用侧应该统一设置事务超时避免连接断掉后事务子进程还在跑。经验是很多进程 kill 不掉的事故追到最底层都是业务代码里漏了一条commit或者 ORM 框架在长链路里一直没有关闭事务。数据库层的排查只能处理表象想要根治必须去应用侧补一遍事务边界规范。最后再分享一个每次排障后我都会做的小动作处理完这类故障后我不会急着把所有监控告警关掉而是先导出一份 binlog 文件用mysqlbinlog查看最后几条事务是否完整。这样做有两个目的一是确认故障期间是否有事务停留在 prepare 阶段、写了一半二是在后续决定回滚还是保留数据时手头有一份可靠的现场证据。大回滚前同样建议先做一个全量备份哪怕只是一个冷备也能在万一判断失误时兜底。这些动作看起来繁琐但在线上环境里多花十分钟的谨慎永远比紧急状态下硬着头皮做判断更划算。