数据库内核月报

数据库内核月报 - 2021 / 11

MySQL · 源码阅读· MySQL 如何响应 KILL

Author: 臻成

MySQL 如何响应 kill

我们使用 MySQL 时经常会遇到需要中止一条查询的时候, 本文讨论 kill 命令的效果及其实现。

三种不同的 kill 命令

从 MySQL 官网上我们可以看到 kill 命令格式如下:

KILL [CONNECTION | QUERY] processlist_id

首先说明要做 kill, 然后指定 kill 的级别:

下面我们针对这两种情况进行一些简单的测试, 看看实际效果.

kill query

我们先制造一个简单的可以 kill 的情况, 为了避免需要太多数据, 我们选择制造一个被锁住的情况, 然后 kill 被锁住的连接. 首先连接数据库, 使用当前连接 (之后称为 C1) 创建一张简单的表:

 -- IN C1
 CREATE TABLE `t1` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `k` int(10) unsigned NOT NULL DEFAULT '0',
  `c` char(120) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '',
  `pad` char(60) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '',
  PRIMARY KEY (`id`),
  KEY `k_1` (`k`)
) ENGINE=InnoDB;

随意写入一些数据. 然后我们执行

-- IN C1
lock tables t1 write;

此时再创建一个新的连接 (之后称为 C2), 执行

-- IN C2
SELECT COUNT(*) FROM t1;

此时会观测到:

+----------+-----------------+-----------+--------+---------+-------+---------------------------------+-------------------------+
| Id       | User            | Host      | db     | Command | Time  | State                           | Info                    |
+----------+-----------------+-----------+--------+---------+-------+---------------------------------+-------------------------+
| 123      | AAA             | localhost | XXX    | Query   |     0 | starting                        | show processlist        |
| 124      | AAA             | localhost | XXX    | Query   |     2 | Waiting for table metadata lock | select count(*) from t1 |
+----------+-----------------+-----------+--------+---------+-------+---------------------------------+-------------------------+

执行 kill

-- IN C1
KILL QUERY 124;

此时 C2 会显示 Query execution was interrupted, 同时 C2 的连接仍然存在可以继续执行查询.

kill connection

类似上面的操作, 只不过把 kill 操作变为

-- IN C1
KILL CONNECTION 124;

-- IN C1
KILL 124;

此时 C2 报错为 Lost connection to MySQL server during query, 124 连接也一同消失了.

Ctrl+C

Ctrl+C 在我们做测试时经常发生, 我们使用 MySQL 客户端以可交互的方式连接 MySQL, 之后我们发出一条 SQL , 但发现一些问题, 希望停下当前的查询, 重新执行. 此时因为之前在执行查询的 session 已经被占用了, 因此 MySQL 客户端会新建立一个临时的 connection, 将 kill query 的命令发送给 MySQL 服务, 之后再回收掉临时的 connection. 这条命令在 MySQL 服务端看起来和 kill query 看起来是一样的.

两种不同的 kill 响应

kill 标记

当执行 kill 命令时, 会根据命令中指定的 process id, 查找对应的连接结构体, 并设置 kill 标记, 查询执行过程中, 会在各种位置检测 THD 上的 kill 标记, 如果发现 kill 标记被设置, 那么中止执行, 并做清理退出. 省略了很多检查部分的代码, 主要的 kill 路径代码如下:

static uint kill_one_thread(THD *thd, my_thread_id id, bool only_kill_query) {
  THD *tmp = NULL;
  uint error = ER_NO_SUCH_THREAD;
  Find_thd_with_id find_thd_with_id(id);

  // ...
  tmp = Global_THD_manager::get_instance()->find_thd(&find_thd_with_id);
  // ...
  tmp->awake(only_kill_query ? THD::KILL_QUERY : THD::KILL_CONNECTION);
  // ...
}

void THD::awake(THD::killed_state state_to_set) {
  // ...
  if (this->m_server_idle && state_to_set == KILL_QUERY) { /* nothing */
  } else {
    killed = state_to_set;
  }
  // ...
}

执行过程中响应

等待中响应

如果查询此时等待在某个 condition_variable 上, 那么短时间内可能很难唤醒, 如果出现了死锁的情况, 那么就更不可能唤醒了, 因此, kill 实现了针对等待的特殊响应, 其主要思路是

为什么有时候无法 kill

有时候 kill 命令发出了, show processlist 可以看到 session 状态已经变为 Killed 状态了, 但是查询仍在执行, 这多数情况是由于这个 session 的查询未运行到检查点, 没有发现自己已经被 kill 了, 这个查询可能在引擎层内部执行比较复杂的工作, 也可能在读写临时表进行繁重的 IO. 此时一般只能继续等待程序运行到检查点, 发现 kill 状态后, 就会退出了.