思路
本篇文章的介绍思路以下图的思维导图为大纲。也有利于读者更好的分辨可读性!
为什么需要kill
测试
从测试方面考虑的话,就是我们在写SQL语句的时候。开始寻找不足点,有些时候会偶然间执行一个大事务,这个时候我们为了提升效率往往会直接干到这个查询
生产
随着数据量爆发式的增长,一些查询功能越来越慢的。而我们在调试一个功能的时候往往就是调试背后的SQL语句。还有一种情况就是,修改表结构的时候,由于数据量过大,我们会放弃普遍方法,会寻找一个快速的方法。
锁等待
比如发生死锁的时候,或者两个锁在争锁的时候。往往会需要kill。结束掉一个事务给事务回滚放行另一个事务
kill内部都做了啥
首先介绍两种kill写法吧
- kill query + 线程 id:表示终止这个线程中正在执行的语句
- kill connection + 线程 id:表示断开这个线程的连接,当然如果这个线程有语句正在执行,也是要先停止正在执行的语句的。
言归正传
前几篇文章我们介绍过,对一个表进行增删改查的时候,会在表上加一个读锁。这个时候用户虽然处于blocked状态,但是还拿着MDL读锁。如果线程直接被kill的话,读锁就没办法释放了。所以最理想的状态应该是,kill之后,让他做一些收尾工作,全部结束之后再结束掉线程。
那么它到底做了啥
- 把 session B 的运行状态改成 THD::KILL_QUERY(将变量 killed 赋值为 THD::KILL_QUERY);
- 给 session B 的执行线程发一个信号。
流程扩展
我们继续按照上述的流程作一个扩展。为什么要发信号呢?
举一个多用户请求的例子。如果用户A处于锁等待状态,如果只是把用户A的线程状态设置为THD::KILL_QUERY
,线程A并不知道这个状态变化,还是会继续等待。发一个信号的目的,就是让 用户A 退出等待,来处理这个 THD::KILL_QUERY
状态。
换言之。如果发现线程状态是THD::KILL_QUERY
才开始进入语句终止逻辑。
kill不掉是啥鬼
首先我们介绍一个参数,控制线程并发上限的这个参数innodb_thread_concurrency
举例说明一下
- sessionA:select sleep(100) from t
- sessionB:select sleep(100) from t
- sessionC:select sleep(100) from t (blocked)
- sessionD:kill query C
- sessionE:kill C <=> kill connection C
通过上述5个用户的执行,我们先把他设置成set global innodb_thread_concurrency=2
可以看到
- sesssion 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 这个线程的 Commnad 列显示的是 Killed。也就是说,客户端虽然断开了连接,但实际上服务端上这条语句还在执行过程中。
为什么在执行 kill query 命令时,不像 update 语句一样退出呢?
在实现上,等行锁时,使用的是 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。
所以在show processlist列表中会把connection改成killed,也就是我们看到的状态。
综上所述 kill无效的几种情况:
- 线程没有执行到判断线程状态的逻辑
跟这种情况相同的,还有由于 IO 压力过大,读写 IO 的函数一直无法返回,导致不能及时判断线程的状态。
- 终止逻辑耗时较长
从 show processlist 结果上看也是 Command=Killed,需要等到终止逻辑完成,语句才算真正完成。比如超大事务被kill需要做很多回收查找,大查询回滚,DDL命令。
我们可以对大查询回滚做一个扩展介绍,前几篇文章我们介绍过查询数据的时候,当数据量超过一个数量的时候我们会采用硬盘存储,如果小于这个数量的时候会采用内存存储。所以如果在磁盘中查询的时候。进行回滚!相应的磁盘页也会消耗一定的时间去关闭释放。
客户端与服务端的关闭问题
Ctrl+C 关闭的什么
客户端的操作只能操作到客户端的线程,客户端和服务端只能通过网络交互,是不可能直接操作服务端线程的。而由于 MySQL 是停等协议,所以这个线程执行的语句还没有返回的时候,再往这个连接里面继续发命令也是没有用的。实际上,执行 Ctrl+C 的时候,是 MySQL 客户端另外启动一个连接,然后发送一个 kill query 命令。
所以,你可别以为在客户端执行完 Ctrl+C 就万事大吉了。因为,要 kill 掉一个线程,还涉及到后端的很多操作。
数据表过多会影响性能吗
我们在第一篇文章就介绍了每个客户端和服务器建立连接时,做了哪些事情。比如TCP握手,用户校验,获取权限等
但实际上,当使用默认参数连接的时候,MySQL 客户端会提供一个本地库名和表名补全的功能。为了实现这个功能,客户端在连接成功后,需要多做一些操作:
- 执行 show databases;
- 切到 db1 库,执行 show tables;
- 把这两个命令的结果用于构建一个本地的哈希表。
最耗时的也就是第三步的哈希表构建了,也就是说,我们感知到的连接过程慢,其实并不是连接慢,也不是服务端慢,而是客户端慢
-A,-quick 可以跳过这个阶段。为什么这么说呢,我们可以介绍一下 -quick参数涉及的MySQL配置。
MySQL 客户端发送请求后,接收服务端返回结果的方式有两种:
- 一种是本地缓存,也就是在本地开一片内存,先把结果存起来。如果你用 API 开发,对应的就是 mysql_store_result 方法。
- 另一种是不缓存,读一个处理一个。如果你用 API 开发,对应的就是 mysql_use_result 方法。
MySQL 客户端默认采用第一种方式,而如果加上–quick 参数,就会使用第二种不缓存的方式。
采用不缓存的方式时,如果本地处理得慢,就会导致服务端发送结果被阻塞,因此会让服务端变慢
扩展
为什么取名为quick呢?
- 第一点,就是前面提到的,跳过表名自动补全功能。
- 第二点,mysql_store_result 需要申请本地内存来缓存查询结果,如果查询结果太大,会耗费较多的本地内存,可能会影响客户端本地机器的性能;
- 第三点,是不会把执行命令记录到本地的命令历史文件。
综上所述:quick是提升客户端的性能,不是提升服务端的性能
总结
今天大概介绍了MySQL内部在kill线程的时候,都做了哪些操作以及强化了最开始的文章深度。
这里的文章深度主要是在哈希表构建的那一段。