代码编织梦想


MySQL45讲

实践篇

32 | 为什么还有kill不掉的语句?

在 MySQL 中有两个 kill 命令:一个是 kill query + 线程 id,表示终止这个线程中正在执行的语句;一个是 kill connection + 线程 id,这里 connection 可缺省,表示断开这个线程的连接,如果这个线程有语句正在执行,也要先停止正在执行的语句。

大多数情况下,kill query/connection 命令有效。比如,执行一个查询的过程中,发现执行时间太久,要放弃继续查询,这时就可以用 kill query 命令,终止这条查询语句。
还有一种情况是,语句处于锁等待的时候,直接使用 kill 命令也是有效的。
在这里插入图片描述

收到 kill 以后,线程做什么?

当对一个表做增删改查操作时,会在表上加 MDL 读锁。所以,session B 虽然处于 blocked 状态,但还是拿着一个 MDL 读锁。如果线程被 kill 的时候,就直接终止,那之后这个 MDL 读锁就没机会被释放了。

kill 并不是马上停止的意思,而是告诉执行线程,这条语句已经不需要继续执行,可以开始“执行停止的逻辑了”。
当用户执行 kill query thread_id_B 时,MySQL 里处理 kill 命令的线程做了两件事:

  1. 把 session B 的运行状态改成 THD::KILL_QUERY(将变量 killed 赋值为 THD::KILL_QUERY);
  2. 给 session B 的执行线程发一个信号。

疑问:为什么要发信号?
session B 处于锁等待状态,如果只是把 session B 的线程状态设置 THD::KILL_QUERY,线程 B 并不知道这个状态变化,还是会继续等待。发一个信号的目的,就是让 session B 退出等待,来处理这个 THD::KILL_QUERY 状态。
上述分析包含三层意思:

  1. 一个语句执行过程中有多处“埋点”,在这些“埋点”的地方判断线程状态,如果发现线程状态是
    THD::KILL_QUERY,才开始进入语句终止逻辑;
  2. 如果处于等待状态,必须是一个可以被唤醒的等待,否则根本不会执行到“埋点”处;
  3. 语句从开始进入终止逻辑,到终止逻辑完全完成,是有一个过程的。

示例:执行 set global innodb_thread_concurrency=2,将 InnoDB 的并发线程上限数设置为 2。
在这里插入图片描述
执行 show processlist,看到下图:
在这里插入图片描述
id=12 这个线程的 Commnad 列显示的是 Killed。也就是说,客户端虽然断开了连接,但实际上服务端上这条语句还在执行过程中。

为什么在执行 kill query 命令时,这条语句不像第一个例子的 update 语句一样退出?
在实现上,等行锁时,使用的是 pthread_cond_timedwait 函数,这个等待状态可以被唤醒。 但是,在这个例子里,12 号线程的等待逻辑是这样的:每 10 毫秒判断一下是否可以进入 InnoDB 执行,如果不行,就调用 nanosleep 函数进入 sleep 状态。
虽然 12 号线程的状态已经被设置成 KILL_QUERY,但是在这个等待进入 InnoDB 的循环过程中,并没有去判断线程的状态,因此根本不会进入终止逻辑阶段。

当 session E 执行 kill connection 命令时,是这么做的:

  1. 把 12 号线程状态设置为 KILL_CONNECTION;
  2. 关掉 12 号线程的网络连接。因为有这个操作,所以会看到,这时候 session C 收到了断开连接的提示。

疑问:为什么执行 show processlist 的时候,会看到 Command 列显示为 killed ?
在执行 show processlist 的时候,如果一个线程的状态是KILL_CONNECTION,就把Command列显示成Killed。

所以,即使是客户端退出了,这个线程的状态仍然是在等待中。

kill 无效的情况:

  • 第一类情况,线程没有执行到判断线程状态的逻辑。跟这种情况相同的,还有由于 IO 压力过大,读写 IO 的函数一直无法返回,导致不能及时判断线程的状态。
  • 第二类情况,终止逻辑耗时较长。这时候,从 show processlist 结果上看 Command=Killed,需要等到终止逻辑完成,语句才算真正完成。

第二类情况,比较常见的场景有以下几种:

  • 超大事务执行期间被 kill。 这时候,回滚操作需要对事务执行期间生成的所有新数据版本做回收操作,耗时很长。
  • 大查询回滚。 如果查询过程中生成了比较大的临时文件,加上此时文件系统压力大,删除临时文件可能需要等待 IO 资源,导致耗时较长。
  • DDL 命令执行到最后阶段,如果被 kill,需要删除中间过程的临时文件,也可能受 IO 资源影响耗时较久。

三个关于客户端的误解

第一个误解是:在客户端通过 Ctrl+C 命令,是不是就可以直接终止线程?

不可以。在客户端的操作只能操作到客户端的线程,客户端和服务端只能通过网络交互,不可能直接操作服务端线程。实际上,执行 Ctrl+C 的时候,是 MySQL 客户端另外启动一个连接,然后发送一个 kill query 命令。

第二个误解是:如果库里面的表特别多,连接就会很慢。

每个客户端在和服务端建立连接的时候,需要做的事情就是 TCP 握手、用户校验、获取权限。但这几个操作,显然跟库里面表的个数无关。

当使用默认参数连接的时候,MySQL 客户端会提供一个本地库名和表名补全的功能。 为了实现这个功能,客户端在连接成功后,需要多做一些操作:

  • 执行 show databases;
  • 切到 db1 库,执行 show tables;
  • 把这两个命令的结果用于构建一个本地的哈希表。

在这些操作中,最花时间的就是第三步在本地构建哈希表的操作。 所以,当一个库中的表个数非常多的时候,这一步就会花比较长的时间。

也就是说,感知到的连接过程慢,其实并不是连接慢,也不是服务端慢,而是客户端慢。

如果在连接命令中加上 -A,可以关掉这个自动补全的功能,然后客户端就可以快速返回了。

第三个误解:–quick(或者简写为 -q) 参数应该是一个让服务端加速的参数。

设置了 –quick 这个参数可能会降低服务端的性能。

MySQL 客户端发送请求后,接收服务端返回结果的方式有两种:

  • 一种是本地缓存,也就是在本地开一片内存,先把结果存起来。如果用 API 开发,对应的就是 mysql_store_result 方法。
  • 另一种是不缓存,读一个处理一个。如果用 API 开发,对应的就是 mysql_use_result 方法。

MySQL 客户端默认采用第一种方式,而如果加上 –quick 参数,就会使用第二种不缓存的方式。

采用不缓存的方式时,如果本地处理得慢,就会导致服务端发送结果被阻塞,因此会让服务端变慢。

疑问:为什么要给这个参数取名叫作 quick ?
因为使用这个参数可以达到以下三点效果:

  1. 第一点,跳过表名自动补全功能;
  2. 第二点,mysql_store_result 需要申请本地内存来缓存查询结果,如果查询结果太大,会耗费较多的本地内存,可能会影响客户端本地机器的性能;
  3. 第三点,不会把执行命令记录到本地的命令历史文件。
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/TQ20160412/article/details/122193056