MySQL进阶垫脚石:线程长时间处于killed状态怎么破?

一、背景

MySQL中使用kill命令去杀死连接时,如果使用show processlist会发现线程会处于killed状态一段时间,而不是立即杀掉。一些情况下,killed状态可能会存在很久,甚至可能会一直存在直到发送第二次kill命令才能杀掉连接。下面从 MySQL 执行kill命令代码流程(基于5.7版本的 MySQL )简单分析下出现这种现象的原因。

二、源码分析

1、MySQL 执行流程简介

MySQL 的启动入口函数为mysqld中的main函数,主要流程会启动一个线程去listen端口,accept tcp连接,并创建一个connection并与具体的线程绑定,去处理来自客户端的消息。

执行流程:

MySQL进阶垫脚石:线程长时间处于killed状态怎么破?_第1张图片
日常执行kill流程,一般是通过mysql命令行客户端新起一个连接,通过show processlist找到需要kill掉的连接的conncetion_id,然后发送kill命令。

注:kill + 连接id 默认是kill connection,代表断开连接,如果是kill query + 连接id则只是终止本次执行的语句,连接还会继续监听来自client的命令。(具体执行区别可以参考下面KILL工作流程1中(1)、(2)部分)

2、KILL工作流程

概念:
  • connection: socket连接,默认有一个max_connection,实际上可以接受max_connection + 1个连接,最后一个连接是预留给SUPER用户的。
  • pthread: mysql的工作线程,每个connection建立时都会分配一个pthread,connection断开后pthread仍旧可以被其他connection复用。
  • thd: 线程描述类,每个connection对应一个thd,其中包含很多连接状态的描述,其中thd->killed字段标识线程是否需要被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,则不会关闭连接)不再接收新的命令。客户端报下面这个错就是在这步之后:

img

另外会执行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命令主要有两种方式:

  • 主动检查: c onne ction1在一些 代 码处会去主动检查killed状态 ;
  • 被通过信号量唤醒: connection1在执行某些命令时(如引擎层去做一些操作)&#

你可能感兴趣的:(数据库,mysql,数据库,java,多线程,redis)