一条SQL执行到一半被终止,现象往往很诡异:应用日志里出现Query execution was interrupted或者Lost connection to MySQL server的报错,数据库里却看不到明显错误;有时候kill了一条慢查询,结果连接数没降、CPU还是被打满。要搞清楚这些问题,得先理解SQL从发起到执行结束经历了哪些环节,超时和kill分别在哪个环节生效,以及中断之后数据库内部还要做哪些收尾工作。本文以MySQL为主线,对照PostgreSQL,把这套机制讲透。

一条SQL的生命周期:中断可能发生在哪一层
从客户端发出一条SQL到拿到结果,中间要穿过好几层,每一层都有自己独立的超时设置。第一层是客户端驱动层,比如JDBC的queryTimeout、MySQL驱动的socketTimeout,这一层超时是客户端自己掐断连接,数据库可能根本不知情。第二层是连接层,像MySQL的wait_timeout、interactive_timeout,控制的是空闲连接的存活时间。第三层才是服务端执行层,比如MySQL的max_execution_time参数,它会在SQL执行过程中真正地打断执行。
理解这个分层非常关键,因为不同层的超时中断,数据库端的反应完全不同。如果是客户端socketTimeout触发,客户端断开了socket,MySQL在执行过程中检测到连接已断开,才会中断查询;但如果SQL已经把数据写进socket缓冲区,而客户端早已离开,服务端可能要等下一次写socket时才发现异常。这就是为什么有时候客户端早就报错了,数据库上的查询还在跑,监控里能看到那条SQL持续占用资源。
实际排查时,建议按顺序确认三件事:应用侧的超时配置是多少,连接池有没有额外的超时收缩逻辑,数据库端的max_execution_time是否设置。三层超时不一致是生产事故的常见根源,典型情况是客户端超时5秒、数据库没限制,大量查询在客户端早已失败,服务端却还在堆积执行。
手动kill到底做了什么:kill与kill query的区别
MySQL里执行kill并不是操作系统层面的杀进程,而是给目标会话设置一个标记。执行KILL CONNECTION id时,MySQL会断开该会话的连接,并给正在执行的语句打上中断标志;执行KILL QUERY id则只中断当前语句,连接本身保留。设置标记这个动作是异步的,真正的中断发生在目标线程执行过程中的检查点上。
MySQL在执行SQL时会周期性地检查中断标志,这些检查点分布在读取表、处理行、发送结果给客户端等关键路径上。如果一条SQL已经进入不可中断的状态,比如正在等待磁盘IO、持有某个内部锁,或者处于事务回滚阶段,kill请求会挂起等待,直到线程走到下一个检查点。这就解释了为什么kill一条大事务的删除语句之后,show processlist里那条线程的状态从executing变成了killed,却迟迟不消失,它正在回滚已修改的数据。
-- 查看当前会话和执行状态 SHOW PROCESSLIST; -- 只终止 id 为 25 的会话当前正在执行的语句,连接保留 KILL QUERY 25; -- 直接断开 id 为 25 的整个连接 KILL CONNECTION 25; -- 设置服务端单条 SELECT 的最大执行时间(毫秒) SET SESSION max_execution_time = 3000; SELECT /*+ MAX_EXECUTION_TIME(3000) */ * FROM big_table WHERE cond = 1;
PostgreSQL的做法不太一样。PG从9.3开始提供pg_cancel_backend(pid)用于取消当前查询,pg_terminate_backend(pid)用于终止整个连接,取消机制基于信号实现,查询收到SIGINT后会在安全点中断并回滚当前事务。PG的中断响应通常比较及时,但如果后端进程在等一个外部锁,取消操作同样要等锁等待结束才能生效,这点和MySQL是一致的逻辑:中断永远只能在安全点发生,这是数据库保证数据一致性的底线。
为什么kill之后SQL还在跑:不可中断状态与回滚成本
被kill之后查询仍然占用资源,主要有三类原因。第一类是线程处在不可中断的等待中,比如等待IO完成、等待MDL元数据锁、等待行锁。以等行锁为例,一个事务拿着锁不放,你kill那个等锁的会话可能秒生效,但如果去kill持锁会话,而它正在执行大事务,中断后紧跟的就是漫长的回滚。第二类是kill之后进入回滚阶段,InnoDB的回滚是逐行撤销修改,速度通常比正向执行慢好几倍,一条跑了十分钟的批量UPDATE,回滚半小时以上并不罕见。回滚期间show processlist可能显示连接已经消失,但通过SHOW ENGINE INNODB STATUS能看到活跃事务列表里还有它在滚。
第三类是客户端超时导致的假象。客户端socketTimeout触发后连接断开,但MySQL服务端的线程要等到向客户端写数据失败时才感知断连。一个典型场景:分页查询只返回少量行,客户端早就超时了,服务端却还在把整张表扫完,因为它压根还没走到发送数据的步骤。这类问题的解法是在服务端配置max_execution_time,或者改造SQL让它尽早触达发送阶段,从根上讲,让超时控制在服务端生效比依赖客户端断连可靠得多。
对于大事务回滚,MySQL 5.7之后可以观察performance_schema.events_transactions_current里的事务状态,估算回滚进度;PG则可以通过查询pg_stat_activity中state为idle in transaction的连接判断积压情况。日常运维的预防措施也很直接:大批量DML拆成小批次提交,每批控制在几千到几万行,即使需要kill,回滚成本也在可控范围内。
超时配置实践:一套可落地的分层方案
配置超时的核心原则是:越靠近数据库的层,超时应该越严格。推荐的层次关系是客户端读写超时略大于服务端执行超时,这样能保证SQL在服务端被干净地打断并正确回滚,客户端拿到的是明确的执行中断错误,而不是模糊的连接断开。以MySQL为例,可以给OLTP查询设置会话级max_execution_time为3到5秒,个别报表类SQL用hint单独放宽,JDBC的socketTimeout设置成比max_execution_time大出余量,比如10秒。
-- 会话级设置,只影响当前连接 SET SESSION max_execution_time = 5000; -- 全局默认值,新连接生效 SET GLOBAL max_execution_time = 5000; -- 针对单条查询使用优化器提示覆盖默认值 SELECT /*+ MAX_EXECUTION_TIME(60000) */ COUNT(*) FROM report_table; -- PG 中给单条语句设置超时 SET statement_timeout = '5s';
需要特别提醒的是,max_execution_time只对只读SELECT有效,UPDATE、DELETE、INSERT不受它约束。写操作的超时控制只能靠应用层拆分事务,或者依赖锁等待超时参数innodb_lock_wait_timeout配合使用。另外kill操作本身需要PROCESS和SUPER(或MySQL 8.0的SYSTEM_USER)权限,生产账号权限规划时别忘了这一点,否则值班同学遇到紧急情况连kill都执行不了。
最后梳理一个排查中断问题的思路:先看应用报错信息确定是哪一层触发的中断,再到数据库查processlist和错误日志确认服务端视角,最后用慢日志和performance_schema还原SQL当时的执行状态。把客户端超时、服务端超时、kill机制这三件事的边界分清楚,绝大多数SQL执行中断问题都能快速定位到根因。