You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

224 lines
15 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 32 | 为什么还有kill不掉的语句
在MySQL中有两个kill命令一个是kill query +线程id表示终止这个线程中正在执行的语句一个是kill connection +线程id这里connection可缺省表示断开这个线程的连接当然如果这个线程有语句正在执行也是要先停止正在执行的语句的。
不知道你在使用MySQL的时候有没有遇到过这样的现象使用了kill命令却没能断开这个连接。再执行show processlist命令看到这条语句的Command列显示的是Killed。
你一定会奇怪显示为Killed是什么意思不是应该直接在show processlist的结果里看不到这个线程了吗
今天,我们就来讨论一下这个问题。
其实大多数情况下kill query/connection命令是有效的。比如执行一个查询的过程中发现执行时间太久要放弃继续查询这时我们就可以用kill query命令终止这条查询语句。
还有一种情况是语句处于锁等待的时候直接使用kill命令也是有效的。我们一起来看下这个例子
![](https://static001.geekbang.org/resource/image/17/d0/17f88dc70c3fbe06a7738a0ac01db4d0.png)
图1 kill query 成功的例子
可以看到session C 执行kill query以后session B几乎同时就提示了语句被中断。这就是我们预期的结果。
# 收到kill以后线程做什么
但是这里你要停下来想一下session B是直接终止掉线程什么都不管就直接退出吗显然这是不行的。
我在[第6篇文章](https://time.geekbang.org/column/article/69862)中讲过当对一个表做增删改查操作时会在表上加MDL读锁。所以session B虽然处于blocked状态但还是拿着一个MDL读锁的。如果线程被kill的时候就直接终止那之后这个MDL读锁就没机会被释放了。
这样看来kill并不是马上停止的意思而是告诉执行线程说这条语句已经不需要继续执行了可以开始“执行停止的逻辑了”。
> 其实这跟Linux的kill命令类似kill -N pid并不是让进程直接停止而是给进程发一个信号然后进程处理这个信号进入终止逻辑。只是对于MySQL的kill命令来说不需要传信号量参数就只有“停止”这个命令。
**实现上当用户执行kill query thread\_id\_B时MySQL里处理kill命令的线程做了两件事**
1. 把session B的运行状态改成THD::KILL\_QUERY(将变量killed赋值为THD::KILL\_QUERY)
2. 给session B的执行线程发一个信号。
为什么要发信号呢?
因为像图1的我们例子里面session B处于锁等待状态如果只是把session B的线程状态设置THD::KILL\_QUERY线程B并不知道这个状态变化还是会继续等待。发一个信号的目的就是让session B退出等待来处理这个THD::KILL\_QUERY状态。
上面的分析中,隐含了这么三层意思:
1. 一个语句执行过程中有多处“埋点”在这些“埋点”的地方判断线程状态如果发现线程状态是THD::KILL\_QUERY才开始进入语句终止逻辑
2. 如果处于等待状态,必须是一个可以被唤醒的等待,否则根本不会执行到“埋点”处;
3. 语句从开始进入终止逻辑,到终止逻辑完全完成,是有一个过程的。
到这里你就知道了,原来不是“说停就停的”。
接下来,我们**再看一个kill不掉的例子**,也就是我们在前面[第29篇文章](https://time.geekbang.org/column/article/78134)中提到的 innodb\_thread\_concurrency 不够用的例子。
首先执行set global innodb\_thread\_concurrency=2将InnoDB的并发线程上限数设置为2然后执行下面的序列
![](https://static001.geekbang.org/resource/image/32/6e/32e4341409fabfe271db3dd4c4df696e.png)
图2 kill query 无效的例子
可以看到:
1. sesssion C执行的时候被堵住了
2. 但是session D执行的kill query C命令却没什么效果
3. 直到session E执行了kill connection命令才断开了session C的连接提示“Lost connection to MySQL server during query”
4. 但是这时候如果在session E中执行show processlist你就能看到下面这个图。
![](https://static001.geekbang.org/resource/image/91/53/915c20e4c11b104d7bcf9d3457304c53.png)
图3 kill connection之后的效果
这时候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。
```
所以其实,即使是客户端退出了,这个线程的状态仍然是在等待中。那这个线程什么时候会退出呢?
答案是只有等到满足进入InnoDB的条件后session C的查询语句继续执行然后才有可能判断到线程状态已经变成了KILL\_QUERY或者KILL\_CONNECTION再进入终止逻辑阶段。
到这里,我们来小结一下。
**这个例子是kill无效的第一类情况线程没有执行到判断线程状态的逻辑。**跟这种情况相同的还有由于IO压力过大读写IO的函数一直无法返回导致不能及时判断线程的状态。
**另一类情况是,终止逻辑耗时较长。**这时候从show processlist结果上看也是Command=Killed需要等到终止逻辑完成语句才算真正完成。这类情况比较常见的场景有以下几种
1. 超大事务执行期间被kill。这时候回滚操作需要对事务执行期间生成的所有新数据版本做回收操作耗时很长。
2. 大查询回滚。如果查询过程中生成了比较大的临时文件加上此时文件系统压力大删除临时文件可能需要等待IO资源导致耗时较长。
3. DDL命令执行到最后阶段如果被kill需要删除中间过程的临时文件也可能受IO资源影响耗时较久。
之前有人问过我如果直接在客户端通过Ctrl+C命令是不是就可以直接终止线程呢
答案是,不可以。
这里有一个误解,其实在客户端的操作只能操作到客户端的线程,客户端和服务端只能通过网络交互,是不可能直接操作服务端线程的。
而由于MySQL是停等协议所以这个线程执行的语句还没有返回的时候再往这个连接里面继续发命令也是没有用的。实际上执行Ctrl+C的时候是MySQL客户端另外启动一个连接然后发送一个kill query 命令。
所以你可别以为在客户端执行完Ctrl+C就万事大吉了。因为要kill掉一个线程还涉及到后端的很多操作。
# 另外两个关于客户端的误解
在实际使用中,我也经常会碰到一些同学对客户端的使用有误解。接下来,我们就来看看两个最常见的误解。
**第一个误解是:如果库里面的表特别多,连接就会很慢。**
有些线上的库会包含很多表我见过最多的一个库里有6万个表。这时候你就会发现每次用客户端连接都会卡在下面这个界面上。
![](https://static001.geekbang.org/resource/image/7e/83/7e4666bfd580505180c77447d1f44c83.png)
图4 连接等待
而如果db1这个库里表很少的话连接起来就会很快可以很快进入输入命令的状态。因此有同学会认为是表的数目影响了连接性能。
从[第一篇文章](https://time.geekbang.org/column/article/68319)你就知道每个客户端在和服务端建立连接的时候需要做的事情就是TCP握手、用户校验、获取权限。但这几个操作显然跟库里面表的个数无关。
但实际上正如图中的文字提示所说的当使用默认参数连接的时候MySQL客户端会提供一个本地库名和表名补全的功能。为了实现这个功能客户端在连接成功后需要多做一些操作
1. 执行show databases
2. 切到db1库执行show tables
3. 把这两个命令的结果用于构建一个本地的哈希表。
在这些操作中,最花时间的就是第三步在本地构建哈希表的操作。所以,当一个库中的表个数非常多的时候,这一步就会花比较长的时间。
也就是说,**我们感知到的连接过程慢,其实并不是连接慢,也不是服务端慢,而是客户端慢。**
图中的提示也说了,如果在连接命令中加上-A就可以关掉这个自动补全的功能然后客户端就可以快速返回了。
这里自动补全的效果就是你在输入库名或者表名的时候输入前缀可以使用Tab键自动补全表名或者显示提示。
实际使用中,如果你自动补全功能用得并不多,我建议你每次使用的时候都默认加-A。
其实提示里面没有说,除了加-A以外quick(或者简写为-q)参数,也可以跳过这个阶段。但是,这个**quick是一个更容易引起误会的参数也是关于客户端常见的一个误解。**
你看到这个参数,是不是觉得这应该是一个让服务端加速的参数?但实际上恰恰相反,设置了这个参数可能会降低服务端的性能。为什么这么说呢?
MySQL客户端发送请求后接收服务端返回结果的方式有两种
1. 一种是本地缓存也就是在本地开一片内存先把结果存起来。如果你用API开发对应的就是mysql\_store\_result 方法。
2. 另一种是不缓存读一个处理一个。如果你用API开发对应的就是mysql\_use\_result方法。
MySQL客户端默认采用第一种方式而如果加上quick参数就会使用第二种不缓存的方式。
采用不缓存的方式时,如果本地处理得慢,就会导致服务端发送结果被阻塞,因此会让服务端变慢。关于服务端的具体行为,我会在下一篇文章再和你展开说明。
那你会说既然这样为什么要给这个参数取名叫作quick呢这是因为使用这个参数可以达到以下三点效果
* 第一点,就是前面提到的,跳过表名自动补全功能。
* 第二点mysql\_store\_result需要申请本地内存来缓存查询结果如果查询结果太大会耗费较多的本地内存可能会影响客户端本地机器的性能
* 第三点,是不会把执行命令记录到本地的命令历史文件。
所以你看到了quick参数的意思是让客户端变得更快。
# 小结
在今天这篇文章中我首先和你介绍了MySQL中有些语句和连接“kill不掉”的情况。
这些“kill不掉”的情况其实是因为发送kill命令的客户端并没有强行停止目标线程的执行而只是设置了个状态并唤醒对应的线程。而被kill的线程需要执行到判断状态的“埋点”才会开始进入终止逻辑阶段。并且终止逻辑本身也是需要耗费时间的。
所以如果你发现一个线程处于Killed状态你可以做的事情就是通过影响系统环境让这个Killed状态尽快结束。
比如如果是第一个例子里InnoDB并发度的问题你就可以临时调大innodb\_thread\_concurrency的值或者停掉别的线程让出位子给这个线程执行。
而如果是回滚逻辑由于受到IO资源限制执行得比较慢就通过减少系统压力让它加速。
做完这些操作后,其实你已经没有办法再对它做什么了,只能等待流程自己完成。
最后,我给你留下一个思考题吧。
如果你碰到一个被killed的事务一直处于回滚状态你认为是应该直接把MySQL进程强行重启还是应该让它自己执行完成呢为什么呢
你可以把你的结论和分析写在留言区,我会在下一篇文章的末尾和你讨论这个问题。感谢你的收听,也欢迎你把这篇文章分享给更多的朋友一起阅读。
# 上期问题时间
我在上一篇文章末尾,给你留下的问题是,希望你分享一下误删数据的处理经验。
**@苍茫 同学提到了一个例子**我觉得值得跟大家分享一下。运维的同学直接拷贝文本去执行SQL语句截断导致数据库执行出错。
从浏览器拷贝文本执行是一个非常不规范的操作。除了这个例子里面说的SQL语句截断问题还可能存在乱码问题。
一般这种操作如果脚本的开发和执行不是同一个人需要开发同学把脚本放到git上然后把git地址以及文件的md5发给运维同学。
这样就要求运维同学在执行命令之前确认要执行的文件的md5跟之前开发同学提供的md5相同才能继续执行。
另外,我要特别点赞一下@苍茫 同学复现问题的思路和追查问题的态度。
**@linhui0705 同学提到的“四个脚本”的方法,我非常推崇**。这四个脚本分别是:备份脚本、执行脚本、验证脚本和回滚脚本。如果能够坚持做到,即使出现问题,也是可以很快恢复的,一定能降低出现故障的概率。
不过,这个方案最大的敌人是这样的思想:这是个小操作,不需要这么严格。
**@Knight²º¹⁸ 给了一个保护文件的方法**,我之前没有用过这种方法,不过这确实是一个不错的思路。
为了数据安全和服务稳定,多做点预防方案的设计讨论,总好过故障处理和事后复盘。方案设计讨论会和故障复盘会,这两种会议的会议室气氛完全不一样。经历过的同学一定懂的。