MySQL中使用kill命令去杀死连接时,如果使用show processlist会发现线程会处于killed状态一段时间,而不是立即杀掉。一些情况下,killed状态可能会存在很久,甚至可能会一直存在直到发送第二次kill命令才能杀掉连接。下面从 MySQL 执行kill命令代码流程(基于5.7版本的 MySQL )简单分析下出现这种现象的原因。
1、MySQL 执行流程简介
MySQL 的启动入口函数为mysqld中的main函数,主要流程会启动一个线程去listen端口,accept tcp连接,并创建一个connection并与具体的线程绑定,去处理来自客户端的消息。
日常执行kill流程,一般是通过mysql命令行客户端新起一个连接,通过show processlist找到需要kill掉的连接的conncetion_id,然后发送kill命令。
注:kill + 连接id 默认是kill connection,代表断开连接,如果是kill query + 连接id则只是终止本次执行的语句,连接还会继续监听来自client的命令。(具体执行区别可以参考下面KILL工作流程1中(1)、(2)部分)
2、KILL工作流程
为方便说明,假设有两个连接connection1, connection2, 对应上述流程,则是connection1在do_command或者listen socket event中时,通过connection2发送kill命令,中断connection1的执行流程。
kill connection之后,对应此连接的pthread可能会被新连接复用(具体后面会分析)。
1)执行kill命令的线程发起kill
以connection2的执行流程来分析kill的执行过程,跟踪do_command之后的代码堆栈可以看到:
* frame #0: 0x00000001068a8853 mysqld`THD::awake(this=0x00007fbede88b400, state_to_set=KILL_CONNECTION) at sql_class.cc:2029:27
frame #1: 0x000000010695961f mysqld`kill_one_thread(thd=0x00007fbed6bc9c00, id=2, only_kill_query=false) at sql_parse.cc:6479:14
frame #2: 0x0000000106946529 mysqld`sql_kill(thd=0x00007fbed6bc9c00, id=2, only_kill_query=false) at sql_parse.cc:6507:16
frame #3: 0x000000010694e0fa mysqld`mysql_execute_command(thd=0x00007fbed6bc9c00, first_level=true) at sql_parse.cc:4210:5
frame #4: 0x0000000106945d62 mysqld`mysql_parse(thd=0x00007fbed6bc9c00, parser_state=0x000070000de2f340) at sql_parse.cc:5584:20
frame #5: 0x0000000106942bf0 mysqld`dispatch_command(thd=0x00007fbed6bc9c00, com_data=0x000070000de2fe78, command=COM_QUERY) at sql_parse.cc:1491:5
frame #6: 0x0000000106944e70 mysqld`do_command(thd=0x00007fbed6bc9c00) at sql_parse.cc:1032:17
frame #7: 0x0000000106ad3976 mysqld`::handle_connection(arg=0x00007fbee220b8d0) at
connection_handler_per_thread.cc:313:13
frame #8: 0x000000010749e74c mysqld`::pfs_spawn_thread(arg=0x00007fbee15dcf90) at pfs.cc:2197:3
frame #9: 0x00007fff734b6109 libsystem_pthread.dylib`_pthread_start + 148
frame #10: 0x00007fff734b1b8b libsystem_pthread.dylib`thread_start + 15
核心代码为awake函数(为方便,分为3段分析):
① 设置线程killed flag状态
if (this->m_server_idle && state_to_set == KILL_QUERY)
{ /* nothing */ }
else
{
killed= state_to_set;
}
如果当前线程处于idle状态(代表命令已执行完),而且kill级别只是终止查询,而不是kill整个连接,那么不会去设置thd -> killed状态,防止影响下一次正常的请求。
(认为需要被kill的查询已经执行结束了,不需要再做操作了)
② 关闭socket连接&中断引擎等待
if (state_to_set != THD::KILL_QUERY && state_to_set != THD::KILL_TIMEOUT)
{
if (this != current_thd)
{
shutdown_active_vio();
}
/* Send an event to the scheduler that a thread should be killed. */
if (!slave_thread)
MySQL_CALLBACK(Connection_handler_manager::event_functions, post_kill_notification, (this)); //post_kill
}
if (state_to_set != THD::NOT_KILLED)
ha_kill_connection(this);
之后会首先关闭socket连接(注如果是kill query,则不会关闭连接)不再接收新的命令。客户端报下面这个错就是在这步之后:
另外会执行ha_close_connection,这里实际是将处于innodb层等待状态的线程唤醒,具体代码在ha_innodb.cc中innobase_kill_connection里会调用lock_trx_handle_wait方法。
trx: 一个mysql线程对应的innodb的事务描述类。
③ 通过信号量通知处于wait状态的线程
if (is_killable)
{
mysql_mutex_lock(&LOCK_current_cond);
if (current_cond && current_mutex)
{
DBUG_EXECUTE_IF("before_dump_thread_acquires_current_mutex",
{
const char act[]=
"now signal dump_thread_signal wait_for go_dump_thread";
DBUG_ASSERT(!debug_sync_set_action(current_thd,
STRING_WITH_LEN(act)));
};);
mysql_mutex_lock(current_mutex); mysql_cond_broadcast(current_cond); mysql_mutex_unlock(current_mutex);
}
mysql_mutex_unlock(&LOCK_current_cond);
这里看到除了设置connection1的thd->killed状态外,还会获取current_mutex锁,唤醒wait条件变量current_cond的线程(connection2)。注意上述②和③中唤醒的对象不同:
ha_close_connection唤醒的是本次对应的innodb事务中的锁(trx->lock.wait_lock),对应的一般是在innodb层事务中等待的某个行锁。mysql_cond_broadcast(current_cond ) 则是唤醒thd中的锁,等待锁是通过THD::enter_cond()进入(如open table时获取表锁,或者sleep等)
具体可参考下面本地debug复现部分的分析。
为什么在发送信号量之前先关闭socket连接?
不关闭socket连接,并发情况下有什么问题?代码中提及了一种case,假设connection1运行已经过了主动检查flag的点,之后connection2调用awake设置flag及发送信号量唤醒,然后connection1进入到socket read中,那么相当于这次信号量会丢失,connection1就会一直阻塞在read中,所以需要关闭socket 连接中断read。BUG#37780
相当于是通过io中断解决信号量丢失的情况。
所以如果connection1在其他阶段发生信号量丢失(如connection2先broadcast,connection1再wait),就需要发送第二次kill命令才能唤醒。
(sql_class.cc 2090,但是注意KILL_CONNECTION是不会重复进入awake的)
注: 一般出现这种情况是,connection2修改了killed状态,但是由于cpu缓存一致性等问题,connection1看不到killed状态,然后通过了主动检查点,进入了wait状态。
2)被kill线程响应kill命令
被kill线程感知(响应)kill命令主要有两种方式: