在 MySQL 中有两个 kill 命令:一个是 kill query + 线程 id,表示终止这个线程中正在执行的语句;一个是 kill connection + 线程 id,这里 connection 可缺省,表示断开这个
线程的链接,固然若是这个线程有语句正在执行,也是要先中止正在执行的语句的。mysql
不知道你在使用 MySQL 的时候,有没有遇到过这样的现象:使用了 kill 命令,却没能断开这个链接。再执行 show processlist 命令,看到这条语句的 Command 列显示的是Killed。git
你必定会奇怪,显示为 Killed 是什么意思,不是应该直接在 show processlist 的结果里看不到这个线程了吗?sql
今天,咱们就来讨论一下这个问题。数据库
其实大多数状况下,kill query/connection 命令是有效的。好比,执行一个查询的过程当中,发现执行时间过久,要放弃继续查询,这时咱们就能够用 kill query 命令,终止这条
查询语句。后端
还有一种状况是,语句处于锁等待的时候,直接使用 kill 命令也是有效的。咱们一块儿来看下这个例子:浏览器
图 1 kill query 成功的例子缓存
能够看到,session C 执行 kill query 之后,session B 几乎同时就提示了语句被中断。这,就是咱们预期的结果。安全
可是,这里你要停下来想一下:session B 是直接终止掉线程,什么都无论就直接退出吗?显然,这是不行的。bash
我在第 6 篇文章中讲过,当对一个表作增删改查操做时,会在表上加 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 篇文章中提到的innodb_thread_concurrency 不够用的例子。
首先,执行 set global innodb_thread_concurrency=2,将 InnoDB 的并发线程上限数设置为 2;而后,执行下面的序列:
图 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,你就能看到下面这个图。
图 3 kill connection 以后的效果
这时候,id=12 这个线程的 Commnad 列显示的是 Killed。也就是说,客户端虽然断开了链接,但实际上服务端上这条语句还在执行过程当中。
在实现上,等行锁时,使用的是 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,须要等到终止逻辑完成,语句才算真正完成。
这类状况,比较常见的场景有如下几种:
以前有人问过我,若是直接在客户端经过 Ctrl+C 命令,是否是就能够直接终止线程呢?
答案是,不能够。
这里有一个误解,其实在客户端的操做只能操做到客户端的线程,客户端和服务端只能经过网络交互,是不可能直接操做服务端线程的。
而因为 MySQL 是停等协议,因此这个线程执行的语句尚未返回的时候,再往这个链接里面继续发命令也是没有用的。实际上,执行 Ctrl+C 的时候,是 MySQL 客户端另外启
动一个链接,而后发送一个 kill query 命令。
因此,你可别觉得在客户端执行完 Ctrl+C 就万事大吉了。由于,要 kill 掉一个线程,还涉及到后端的不少操做。
在实际使用中,我也常常会碰到一些同窗对客户端的使用有误解。接下来,咱们就来看看两个最多见的误解。
有些线上的库,会包含不少表(我见过最多的一个库里有 6 万个表)。这时候,你就会发现,每次用客户端链接都会卡在下面这个界面上。
图 4 链接等待
而若是 db1 这个库里表不多的话,链接起来就会很快,能够很快进入输入命令的状态。所以,有同窗会认为是表的数目影响了链接性能。
从第一篇文章你就知道,每一个客户端在和服务端创建链接的时候,须要作的事情就是 TCP握手、用户校验、获取权限。但这几个操做,显然跟库里面表的个数无关。
但实际上,正如图中的文字提示所说的,当使用默认参数链接的时候,MySQL 客户端会提供一个本地库名和表名补全的功能。为了实现这个功能,客户端在链接成功后,须要多
作一些操做:
1. 执行 show databases;
2. 切到 db1 库,执行 show tables;
3. 把这两个命令的结果用于构建一个本地的哈希表。
在这些操做中,最花时间的就是第三步在本地构建哈希表的操做。因此,当一个库中的表个数很是多的时候,这一步就会花比较长的时间。
也就是说,咱们感知到的链接过程慢,其实并非链接慢,也不是服务端慢,而是客户端慢。
图中的提示也说了,若是在链接命令中加上 -A,就能够关掉这个自动补全的功能,而后客户端就能够快速返回了。
这里自动补全的效果就是,你在输入库名或者表名的时候,输入前缀,可使用 Tab 键自动补全表名或者显示提示。
实际使用中,若是你自动补全功能用得并很少,我建议你每次使用的时候都默认加 -A。
其实提示里面没有说,除了加 -A 之外,加–quick(或者简写为 -q) 参数,也能够跳过这个阶段。可是,这个–quick 是一个更容易引发误会的参数,也是关于客户端常见的一个误解。
三、–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²º¹⁸ 给了一个保护文件的方法,我以前没有用过这种方法,不过这确实是一个不错的思路。
为了数据安全和服务稳定,多作点预防方案的设计讨论,总好过故障处理和过后复盘。方案设计讨论会和故障复盘会,这两种会议的会议室气氛彻底不同。经历过的同窗必定懂的。