MySQL深入28-kill不掉的语句
# MySQL深入28-kill不掉的语句
MySQL中有两个kill命令:
- kill query + 线程 id 表示终止这个线程中正在执行的语句
- kill connection + 线程id 表示断开这个线程的连接
其实大多数情况下,kill query/connection命令是有效的。比如,执行一个查询的过程中,发现执行时间太久,要放弃继续查询,这时我们就可以用kill query命令,终止这条查询语句。
还有一种情况是,语句处于锁等待的时候,直接使用kill命令:
session C 执行 kill query 以后,session B 几乎同时就提示了语句被中断。
# 收到kill之后,线程做什么
session B被终止掉线程后,并不是什么不都管了直接退出。当对一个表做增删改查操作的时候,会在表上加MDL读锁,所以session B虽然处于blocked状态,但还是拿着一个MDL读锁。如果线程被KILL之后,直接终止,那么MDL读锁就没有机会释放了。
kill并不是马上停止的意思,而是告诉执行线程说,这条语句已经不需要继续执行了,可以开始“执行停止的逻辑了”。
有点类似java中的interrupt()通知你该结束了,而不是强制结束。
实现上,当用户执行 kill query thread_id_B 时,MySQL 里处理 kill 命令的线程做了两件事:
- 把 session B 的运行状态改成 THD::KILL_QUERY
- 给 session B 的执行线程发一个信号
为什么要发送信号
session B 处于锁等待状态,如果只是把 session B 的线程状态设置 THD::KILL_QUERY,线程 B 并不知道这个状态变化,还是会继续等待。发一个信号的目的,就是让 session B 退出等待,来处理这个 THD::KILL_QUERY 状态。
上面的分析包含了三层含义:
- 一条语句执行过程中会有多处检测点,在这些检测点判断线程状态,如果发现线程状态是THD::KILL_QUERY,才开始进入语句终止逻辑;
- 如果处于等待状态,必须是一个可以被唤醒的等待,否则根本不会执行到监测点。
- 语句从开始进入终止逻辑,到终止逻辑完全完成,是有一个过程的。
下面来分析一个kill不掉的例子,也就是innodb_thread_concurrency 不够用的例子。
首先,执行 set global innodb_thread_concurrency=2,将 InnoDB 的并发线程上限数设置为 2;然后,执行下面的序列:
- session C执行的时候被堵住了
- session D 执行的 kill query C 命令却没什么效果,
- session E 执行了 kill connection 命令,才断开了 session C 的连接,提示“Lost connection to MySQL server during query”,
- 如果在 session E 中执行 show processlist,你就能看到下面这个图。
id=12这个线程显示的killed,也就是说断开的是mysql客户端与服务端之间的链接,语句还在执行中。
# 案例分析
在第一个案例中,等行锁使用的是pthread_cond_timedwait 函数,这个等待状态可以被唤醒。但是,在第二个例子里,12 号线程的等待逻辑是这样的:每 10 毫秒判断一下是否可以进入 InnoDB 执行,如果不行,就调用 nanosleep 函数进入 sleep 状态。
12号线程的状态虽然已经被设置成了KILL_QUERY,但是在这个等待进入InnoDB过程中,是没有去判断线程的状态的,而一旦不去判断线程的状态,就不会进入终止逻辑阶段。
session E执行kill connection命令时,流程如下:
- 把 12 号线程状态设置为 KILL_CONNECTION;
- 关掉 12 号线程的网络连接。因为有这个操作,所以你会看到,这时候 session C 收到了断开连接的提示。
同时由于执行show processlist有一个特别逻辑:如果一个线程的状态是KILL_CONNECTION,就把Command列显示成Killed。所以我们看到12号线程显示为killed。
线程什么时候会退出呢?
只有等到满足进入 InnoDB 的条件后,session C 的查询语句继续执行,然后才有可能判断到线程状态已经变成了 KILL_QUERY 或者 KILL_CONNECTION,再进入终止逻辑阶段。
这个案例是kill无效的第一类情况:线程没有执行到判断线程状态的逻辑。
另一类情况:终止逻辑耗时很长。也就是说在终止的过程中使用命令show processlist进行查询也会显示Command=Killed。这种场景有如下几种:
- 超大事务执行期间被 kill。这时候,回滚操作需要对事务执行期间生成的所有新数据版本做回收操作,耗时很长。
- 大查询回滚。如果查询过程中生成了比较大的临时文件,加上此时文件系统压力大,删除临时文件可能需要等待 IO 资源,导致耗时较长。
- DDL 命令执行到最后阶段,如果被 kill,需要删除中间过程的临时文件,也可能受 IO 资源影响耗时较久。
# 两个关于客户端的误解
误解1:如果库里面表特点多,连接就很慢
有些线上的库,会包含很多表,这时候去连接数据库客户端页面会卡住。但是这个所谓的卡住和数据库表的个数无关。
每个客户端在和服务端建立连接的时候,需要做的事情就是 TCP 握手、用户校验、获取权限。但这几个操作,显然跟库里面表的个数无关。
但实际上,正如图中的文字提示所说的,当使用默认参数连接的时候,MySQL 客户端会提供一个本地库名和表名补全的功能。为了实现这个功能,客户端在连接成功后,需要多做一些操作:
- 执行 show databases;
- 切到 db1 库,执行 show tables;
- 把这两个命令的结果用于构建一个本地的哈希表。
在这些操作中,最花时间的就是第三步在本地构建哈希表的操作。所以,当一个库中的表个数非常多的时候,这一步就会花比较长的时间。如果在连接命令中加上 -A,就可以关掉这个自动补全的功能,然后客户端就可以快速返回了。
误解2:–quick
设置这个参数并非是让服务端加速,实际上恰恰相反,设置了这个参数可能会降低服务端的性能。
MySQL 客户端发送请求后,接收服务端返回结果的方式有两种:
- 一种是本地缓存,也就是在本地开一片内存,先把结果存起来。如果你用 API 开发,对应的就是 mysql_store_result 方法。
- 另一种是不缓存,读一个处理一个。如果你用 API 开发,对应的就是 mysql_use_result 方法。
MySQL客户端默认采用第一种方式,而如果–quick 参数,就会使用第二种不缓存的方式。
采用不缓存的方式时,如果本地处理得慢,就会导致服务端发送结果被阻塞,因此会让服务端变慢。