mysql的连接处理过程

  在mysqld_main函数中经过一系列的初始化后,mysql开始监听客户端的连接

mysqld_socket_acceptor->connection_event_loop();

查看mysqld_socket_acceptor:

Connection_acceptor<Mysqld_socket_listener> *mysqld_socket_acceptor= NULL;

  这是一个类模版Connection_acceptor通过Mysqld_socket_listener类进行的实例化,下面是Connection_acceptor的定义

template <typename Listener> class Connection_acceptor
{
  Listener *m_listener;

public:
  Connection_acceptor(Listener *listener)
  : m_listener(listener)
  { }

  ~Connection_acceptor()
  {
    delete m_listener;
  }

  /**
    Initialize a connection acceptor.

    @retval   return true if initialization failed, else false.
  */
  bool init_connection_acceptor()
  {
    return m_listener->setup_listener();
  }

  /**
    Connection acceptor loop to accept connections from clients.
  */
  void connection_event_loop()
  {
    Connection_handler_manager *mgr= Connection_handler_manager::get_instance();
    while (!abort_loop)
    {
      Channel_info *channel_info= m_listener->listen_for_connection_event();
      if (channel_info != NULL)
        mgr->process_new_connection(channel_info);
    }
  }

  /**
    Close the listener.
  */
  void close_listener()
  {
    m_listener->close_listener();
  }

};

  可以看出这个类是一个抽象接口封装了对于监听套接字的操作,connection_event_loop函数就是循环读取监听的套接字上的连接并且进行连接分配的函数。m_listener是一个指针,指向实际上获取连接的类Mysqld_socket_listener的实例,它的成员函数listen_for_connection_event()根据不同测操作系统的情况封装丢与套接字的监听操作,例如在linux下就使用poll函数进行操作。

  Connection_handler_manager是一个全局的单例模式,这个类用于管理获取的新连接如何进行处理,到底是使用每个连接一个线程,还是使用其他模式。这里暂时列出该类的几个与新建连接的关键成员和函数:

class Connection_handler_manager
{
    static Connection_handler_manager* m_instance;
    Connection_handler* m_connection_handler;
    static mysql_mutex_t LOCK_connection_count;
    static mysql_cond_t COND_connection_count;

public:
    static uint connection_count;          // Protected by LOCK_connection_count
    static ulong max_used_connections;     // Protected by LOCK_connection_count
    static ulong max_used_connections_time;// Protected by LOCK_connection_count
    
    //获取单例模式指向实例的指针
    static Connection_handler_manager* get_instance()
    {
        DBUG_ASSERT(m_instance != NULL);
        return m_instance;
    }
    
    //处理新连接
    void process_new_connection(Channel_info* channel_info);
    bool check_and_incr_conn_count();
}

  m_connection_handler是具体的连接处理者,其类型Connection_handler是一个虚基类,各种连接方式继承这个类具体实现如何处理连接,process_new_connection函数为接收新连接后进行处理的函数:

Connection_handler_manager::process_new_connection(Channel_info* channel_info)
{
  if (abort_loop || !check_and_incr_conn_count())
  {
    channel_info->send_error_and_close_channel(ER_CON_COUNT_ERROR, 0, true);
    delete channel_info;
    return;
  }

  if (m_connection_handler->add_connection(channel_info))
  {
    inc_aborted_connects();
    delete channel_info;
  }
}

  这个函数先靠abort_loop判断是否停止监控,这是一个定义在mysqld.cc中的volatile全局变量,可能被其他线程改变。check_and_incr_conn_count函数用来增加连接计数,当连接数大于最大连接数时,新增连接失败。但是值得注意的是当连接数等于最大连接时依然允许再建立一个连接,这个连接是为root用户管理使用的,在连接后会进行验证。因此实际上的最大连接数是:最大连接数+1。如果循环未停止且连接数未满,则调用m_connection_handler的add_connection添加连接,比较奇怪的是这里返回false表示正常?。这里就是一个典型的多态,具体的处理要看继承Connection_handler的类。这里以每连接每线程为例子,其实现Per_thread_connection_handler类如下:

//sql/conn_handler/connection_handler_impl.h
	class Per_thread_connection_handler : public Connection_handler
	{
	  Per_thread_connection_handler(const Per_thread_connection_handler&);
	  Per_thread_connection_handler&
		operator=(const Per_thread_connection_handler&);
	
	  /**
		Check if idle threads to handle connection in
		thread cache. If so enqueue the new connection
		to be picked by the idle thread in thread cache.
	
		@retval false if idle pthread was found, else true.
	  */
	  bool check_idle_thread_and_enqueue_connection(Channel_info* channel_info);
	
	  /**
		List of pending channel info objects to be picked by idle
		threads. Protected by LOCK_thread_cache.
	  */
	  static std::list<Channel_info*> *waiting_channel_info_list;
	
	  static mysql_mutex_t LOCK_thread_cache;
	  static mysql_cond_t COND_thread_cache;
	  static mysql_cond_t COND_flush_thread_cache;
	
	public:
	  // Status variables related to Per_thread_connection_handler
	  static ulong blocked_pthread_count;	 // Protected by LOCK_thread_cache.
	  static ulong slow_launch_threads;
	  // System variable
	  static ulong max_blocked_pthreads;
	
	  static void init();
	  static void destroy();
	
	  /**
		Wake blocked pthreads and wait until they have terminated.
	  */
	  static void kill_blocked_pthreads();
	
	  /**
		Block until a new connection arrives.
	  */
	  static Channel_info* block_until_new_connection();
	
	  Per_thread_connection_handler() {}
	  virtual ~Per_thread_connection_handler() { }
	
	protected:
	  virtual bool add_connection(Channel_info* channel_info);
	
	  virtual uint get_max_threads() const;
	};

  这个类是用于在one thread per connection的模式下管理连接与线程,在这里可以详细探究下。首先,mysql的one thread per connection模式下,每个连接占用一个线程,mysql会缓存一部分线程以供重用,最大数量由该类中的max_blocked_pthreads控制(这个变量在mysqld.cc中的init_common_variables中按照用户设置进行初始化)

  线程缓存的实现实际上使用了类似阻塞队列的方式,可以将这个看成一个近似生产者消费者的模型,waiting_channel_info_list中为待消费的元素,这里就是保存连接信息的channel_info的指针。当一个线程处理完自己的任务时,调用block_until_new_connection函数:

Channel_info* Per_thread_connection_handler::block_until_new_connection()
{
  Channel_info *new_conn= NULL;
  mysql_mutex_lock(&LOCK_thread_cache);
  if (blocked_pthread_count < max_blocked_pthreads &&
      !kill_blocked_pthreads_flag)
  {
    /* Don't kill the pthread, just block it for reuse */
    DBUG_PRINT("info", ("Blocking pthread for reuse"));

    /*
      mysys_var is bound to the physical thread,
      so make sure mysys_var->dbug is reset to a clean state
      before picking another session in the thread cache.
    */
    DBUG_POP();
    DBUG_ASSERT( ! _db_is_pushed_());

    // Block pthread
    blocked_pthread_count++;
    while (!abort_loop && !wake_pthread && !kill_blocked_pthreads_flag)
      mysql_cond_wait(&COND_thread_cache, &LOCK_thread_cache);
    blocked_pthread_count--;

    if (kill_blocked_pthreads_flag)
      mysql_cond_signal(&COND_flush_thread_cache);
    else if (!abort_loop && wake_pthread)
    {
      wake_pthread--;
      DBUG_ASSERT(!waiting_channel_info_list->empty());
      new_conn= waiting_channel_info_list->front();
      waiting_channel_info_list->pop_front();
      DBUG_PRINT("info", ("waiting_channel_info_list->pop %p", new_conn));
    }
  }
  mysql_mutex_unlock(&LOCK_thread_cache);
  return new_conn;
}

  从最外层的if语句可以看出,当被阻塞以缓存起来的线程数量未达到最大值,且kill_blocked_pthreads_flag标志未被设置时(当关闭时,该标志被设置,用来结束所有被阻塞的线程),线程将阻塞在条件变量COND_thread_cache上:

while (!abort_loop && !wake_pthread && !kill_blocked_pthreads_flag)
      mysql_cond_wait(&COND_thread_cache, &LOCK_thread_cache);

  当有新连接的到来时,会检查是否有被缓存的线程,有的话就会使用COND_thread_cache唤醒阻塞在上面的线程,由代码可以看出若唤醒的原因是由于新连接的到来的话,则被唤醒的线程会去队列中取出一个待处理的连接并且返回这个连接,然后进行处理。

else if (!abort_loop && wake_pthread)
{
    wake_pthread--;
    DBUG_ASSERT(!waiting_channel_info_list->empty());
    new_conn= waiting_channel_info_list->front();
    waiting_channel_info_list->pop_front();
    DBUG_PRINT("info", ("waiting_channel_info_list->pop %p", new_conn));
    }

  接下来看如何加入新连接,从名字就可以看出,加入新连接的函数为 add_connection,上面已经提到过这个函数是继承自虚基类Connection_handler的一个虚函数,连接管理类Connection_handler_manager的process_new_connection函数调用 add_connection 来实际添加连接,下面来看看这个函数的主要代码

bool Per_thread_connection_handler::add_connection(Channel_info* channel_info)
{
  int error= 0;
  my_thread_handle id;
  //省略调试信息
  ···
  if (!check_idle_thread_and_enqueue_connection(channel_info))
    DBUG_RETURN(false);

  /*
    There are no idle threads avaliable to take up the new
    connection. Create a new thread to handle the connection
  */
  channel_info->set_prior_thr_create_utime();
  error= mysql_thread_create(key_thread_one_connection, &id,
                             &connection_attrib,
                             handle_connection,
                             (void*) channel_info);
#ifndef DBUG_OFF
handle_error:
#endif // !DBUG_OFF

  if (error)
  {
    //错误处理
    ···
  }

  Global_THD_manager::get_instance()->inc_thread_created();
  DBUG_PRINT("info",("Thread created"));
  DBUG_RETURN(false);
}

  精简代码后逻辑比较清晰,程序先调用check_idle_thread_and_enqueue_connection查看是否有可用的被缓存起来的线程,如果有就直接将channelinfo插入队列并唤醒阻塞的线程,如果没有则新建一个连接处理的线程。check_idle_thread_and_enqueue_connection代码如下:

bool Per_thread_connection_handler::check_idle_thread_and_enqueue_connection(Channel_info* channel_info)
{
  bool res= true;

  mysql_mutex_lock(&LOCK_thread_cache);
  if (Per_thread_connection_handler::blocked_pthread_count > wake_pthread)
  {
    DBUG_PRINT("info",("waiting_channel_info_list->push %p", channel_info));
    waiting_channel_info_list->push_back(channel_info);
    wake_pthread++;
    mysql_cond_signal(&COND_thread_cache);
    res= false;
  }
  mysql_mutex_unlock(&LOCK_thread_cache);

  return res;
}

  这个函数较为简单,当有空余线程的时候,就将待处理的连接加入队列并且向条件变量发送信号并唤醒一个线程进行处理。这里wake_pthread的含义可以认为是还需要多少个线程去取连接,wake_pthread自增后会去唤醒一个阻塞的线程,待取得一个连接后wake_pthread再自减,所以只有当blocked_pthread_count > wake_pthread的时候才会有空闲的线程,否则需要新建线程:

error= mysql_thread_create(key_thread_one_connection, &id,
                             &connection_attrib,
                             handle_connection,
                             (void*) channel_info);

  这里就是具体处理连接的入口,下面是精简之后的handle_connection函数:

extern "C" void *handle_connection(void *arg)
{
  Global_THD_manager *thd_manager= Global_THD_manager::get_instance();
  Connection_handler_manager *handler_manager=
    Connection_handler_manager::get_instance();
  Channel_info* channel_info= static_cast<Channel_info*>(arg);
  bool pthread_reused MY_ATTRIBUTE((unused))= false;

  if (my_thread_init())
  {
    //错误处理 PS:mysql有些函数错误处理有点奇葩,中有的函数返回false表示正确,true表示错误
    ···
  }
  
  for (;;)
  {
    THD *thd= init_new_thd(channel_info);
    if (thd == NULL)
    {
      //错误处理
      break; // We are out of resources, no sense in continuing.
    }
    ···
    thd_manager->add_thd(thd);

    if (thd_prepare_connection(thd))
      handler_manager->inc_aborted_connects();
    else
    {//当连接存活时,循环处理命令
      while (thd_connection_alive(thd))
      {
        if (do_command(thd))
          break;
      }
      end_connection(thd);
    }
    //做关闭连接后的善后工作
    close_connection(thd, 0, false, false);
    thd->get_stmt_da()->reset_diagnostics_area();
    thd->release_resources();
    ERR_remove_state(0);
    thd_manager->remove_thd(thd);
    Connection_handler_manager::dec_connection_count();
    delete thd;

    if (abort_loop) // Server is shutting down so end the pthread.
      break;
    //阻塞在这里等待新连接(当被缓存的线程数未达到最大值的时候)
    channel_info=Per_thread_connection_handler::block_until_new_connection();
    if (channel_info == NULL)
      break;
    pthread_reused= true;
  }
  //退出线程
  my_thread_end();
  my_thread_exit(0);
  return NULL;
}

  这里首先获取了线程管理的单例类Global_THD_manager,和连接的管理类Connection_handler_manager。Global_THD_manager将各个线程的THD结构体串联在一个链表中统一管理,这里先不展开。在使用每连接每线程的模式时,一个THD结构对应一个线程,但并不总是这样,例如使用线程池的话就并非如此。为了不混淆,之后所称的线程其实指一个开始处理的连接,线程结构体为其对应的THD结构体。若要指代真正的线程,会使用真实线程或者物理线程的说法。

  随后进行了线程初始化,主要是初始化了一些线程局部变量,用于DEBUG。之后的for循环就是一个线程的主要逻辑所在

  循环中首先创建了这个线程的结构体THD,这个结构体保存了一个线程的上下文信息,非常重要,经常作为一些函数的参数。例如一个THD对应一个客户端连接时,THD结构体里就包含了连接的所有信息,权限,帐号,事务状态等等。随后这个THD被加入了Global_THD_manager管理的链表中。
  在对连接进行验证(例如权限验证)后,进入循环:

while (thd_connection_alive(thd))
{
    if (do_command(thd))
        break;
}
end_connection(thd);

  这里就是对一个连接的命令的处理,循环接收连接的指令并执行,直到连接被killed或者执行出现某些错误再退出。再退出循环后进行连接善后工作并如同上文所说的一样调用Per_thread_connection_handler::block_until_new_connection()尝试将该线程缓存起来供下一个连接使用。最后当线程退出时,进行一些清理工作。
  do_command函数就是接收执行客户端命令的函数,将网络协议以及错误处理等精简后,得到其主要逻辑的代码如下:

//sql/sql_parse.cc
bool do_command(THD *thd)
{
  NET *net= NULL;
  enum enum_server_command command;
  COM_DATA com_data;
  rc= thd->get_protocol()->get_command(&com_data, &command);
  return_value= dispatch_command(thd, &com_data, command);
}

  可以看到,每一次循环从网络读取指令数据和类型,然后给dispatch_command进行具体的命令执行,当一个连接新创建或者没有任何请求的时候线程就会阻塞在这里。到此,就进入了mysql的命令分发流程。dispatch_command函数体非常的长,这里只保留部分主要逻辑。

//sql/sql_parse.cc
bool dispatch_command(THD *thd, const COM_DATA *com_data,
                      enum enum_server_command command)
{
  bool error= 0;
  Global_THD_manager *thd_manager= Global_THD_manager::get_instance();
  thd->set_command(command);
  thd->enable_slow_log= TRUE;
  thd->lex->sql_command= SQLCOM_END; /* to avoid confusing VIEW detectors */
  thd->set_time();
  
  thd_manager->inc_thread_running();
  switch (command) {
  case COM_INIT_DB:
  ···
  case COM_REGISTER_SLAVE:
  ···
  ···
  case COM_QUERY:
  {
     ···
     if (alloc_query(thd, com_data->com_query.query,
                    com_data->com_query.length))
        break;
     ···
     mysql_parse(thd, &parser_state);
     ···
  }
}

  这个函数的主要逻辑就是一个巨无霸switch case 语句,根据命令的类型来进行不同的处理。这里注意COM_QUERY,虽然他的名字有“QUERY”,看上去像处理查询的,实际上所有对于数据库的访问操作都是通过这里进入的,如常见的DDL、DML语句......将代码简化后的逻辑为:

//sql/sql_pasrse.cc
void mysql_parse(THD *thd, Parser_state *parser_state)
{
//为查询缓存作初始化
  mysql_reset_thd_for_next_command(thd);
  lex_start(thd);
  
  if (query_cache.send_result_to_client(thd, thd->query()) <= 0)
  {
    ···
    err= parse_sql(thd, parser_state, NULL);
    ···
    error= mysql_execute_command(thd, true);
    ···
  }
  ···
}

  这里只列出了主要的入口,其他的细节暂时不清楚....这个函数先判断查询的语句是否在查询缓存里面(只有完全一样的语句才能匹配,甚至包括空格和大小写),若在则直接发送给客户端,不然就对语句进行语法解析,然后真正的执行它。执行函数为mysql_execute_command

int
mysql_execute_command(THD *thd, bool first_level)
{
  //保存解析后的语法树
  LEX  *const lex= thd->lex;
  //需要访问哪些表
  TABLE_LIST *all_tables;
  all_tables= lex->query_tables;
  ···
    switch (lex->sql_command) 
    {

      case SQLCOM_SHOW_STATUS:
      ···
      case SQLCOM_INSERT:
      case SQLCOM_REPLACE_SELECT:
      case SQLCOM_INSERT_SELECT:
      {
        DBUG_ASSERT(first_table == all_tables && first_table != 0);
        DBUG_ASSERT(lex->m_sql_cmd != NULL);
        res= lex->m_sql_cmd->execute(thd);
        break;
      }
      ···
      case SQLCOM_SHOW_PROCESSLIST:
      ···
    }
}

  这里就是最终对于各个语句具体的操作的入口,这又是一个巨大的switch case语句,其中的每个case就是我们熟悉的指令,对于其中每个指令的执行,就不再深入了,到这里已经完成了对于连接处理的跟踪。

posted @ 2018-03-30 14:51  兔晓侠  阅读(1900)  评论(0编辑  收藏  举报