MYSQL线程池总结(一)

     线程池是Mysql5.6的一个核心功能,对于服务器应用而言,不管是web应用服务仍是DB服务,高并发请求始终是一个绕不开的话题。当有大量请求并发访问时,必定伴随着资源的不断建立和释放,致使资源利用率低,下降了服务质量。线程池是一种通用的技术,经过预先建立必定数量的线程,当有请求达到时,线程池分配一个线程提供服务,请求结束后,该线程又去服务其余请求。 经过这种方式,避免了线程和内存对象的频繁建立和释放,下降了服务端的并发度,减小了上下文切换和资源的竞争,提升资源利用效率。全部服务的线程池本质都是位了提升资源利用效率,而且实现方式也大致相同。本文主要说明Mysql线程池的实现原理。html

    在Mysql5.6出现之前,Mysql处理链接的方式是One-Connection-Per-Thread,即对于每个数据库链接,Mysql-Server都会建立一个独立的线程服务,请求结束后,销毁线程。再来一个链接请求,则再建立一个链接,结束后再进行销毁。这种方式在高并发状况下,会致使线程的频繁建立和释放。固然,经过thread-cache,咱们能够将线程缓存起来,以供下次使用,避免频繁建立和释放的问题,可是没法解决高链接数的问题。One-Connection-Per-Thread方式随着链接数暴增,致使须要建立一样多的服务线程,高并发线程意味着高的内存消耗,更多的上下文切换(cpu cache命中率下降)以及更多的资源竞争,致使服务出现抖动。相对于One-Thread-Per-Connection方式,一个线程对应一个链接,Thread-Pool实现方式中,线程处理的最小单位是statement(语句),一个线程能够处理多个链接的请求。这样,在保证充分利用硬件资源状况下(合理设置线程池大小),能够避免瞬间链接数暴增致使的服务器抖动。mysql

调度方式实现web

     Mysql-Server同时支持3种链接管理方式,包括No-Threads,One-Thread-Per-Connection和Pool-Threads。No-Threads表示处理链接使用主线程处理,不额外建立线程,这种方式主要用于调试;One-Thread-Per-Connection是线程池出现之前最经常使用的方式,为每个链接建立一个线程服务;Pool-Threads则是本文所讨论的线程池方式。Mysql-Server经过一组函数指针来同时支持3种链接管理方式,对于特定的方式,将函数指针设置成特定的回调函数,链接管理方式经过thread_handling参数控制,代码以下:sql

1 if (thread_handling <= SCHEDULER_ONE_THREAD_PER_CONNECTION) 2  one_thread_per_connection_scheduler(thread_scheduler, 3                                       &max_connections, 4                                       &connection_count); 5 else if (thread_handling == SCHEDULER_NO_THREADS) 6  one_thread_scheduler(thread_scheduler); 7 else                                 
8   pool_of_threads_scheduler(thread_scheduler, &max_connections,&connection_count); 

 链接管理流程数据库

  1. 经过poll监听mysql端口的链接请求
  2. 收到链接后,调用accept接口,建立通讯socket
  3. 初始化thd实例,vio对象等
  4. 根据thread_handling方式设置,初始化thd实例的scheduler函数指针
  5. 调用scheduler特定的add_connection函数新建链接

下面代码展现了scheduler_functions模板和线程池对模板回调函数的实现,这个是多种链接管理的核心。缓存

struct scheduler_functions { uint max_threads; uint   *connection_count; ulong *max_connections; bool (*init)(void); bool (*init_new_connection_thread)(void); void (*add_connection)(THD *thd); void (*thd_wait_begin)(THD *thd, int wait_type); void (*thd_wait_end)(THD *thd); void (*post_kill_notification)(THD *thd); bool (*end_thread)(THD *thd, bool cache_thread); void (*end)(void); };
static scheduler_functions tp_scheduler_functions= { 0,                                    // max_threads
 NULL, NULL, tp_init, // init
  NULL,                              // init_new_connection_thread
  tp_add_connection,          // add_connection
  tp_wait_begin,                 // thd_wait_begin 
  tp_wait_end,                   // thd_wait_end
  tp_post_kill_notification,  // post_kill_notification 
  NULL,                             // end_thread
  tp_end                            // end
 };

线程池的相关参数服务器

  1. thread_handling:表示线程池模型。
  2. thread_pool_size:表示线程池的group个数,通常设置为当前CPU核心数目。理想状况下,一个group一个活跃的工做线程,达到充分利用CPU的目的。
  3. thread_pool_stall_limit:用于timer线程按期检查group是否“停滞”,参数表示检测的间隔。
  4. thread_pool_idle_timeout:当一个worker空闲一段时间后会自动退出,保证线程池中的工做线程在知足请求的状况下,保持比较低的水平。
  5. thread_pool_oversubscribe:该参数用于控制CPU核心上“超频”的线程数。这个参数设置值不含listen线程计数。
  6. threadpool_high_prio_mode:表示优先队列的模式。

线程池实现网络

上面描述了Mysql-Server如何管理链接,这节重点描述线程池的实现框架,以及关键接口。如图1多线程

 

                                                          图 1(线程池框架图)并发

      每个绿色的方框表明一个group,group数目由thread_pool_size参数决定。每一个group包含一个优先队列和普通队列,包含一个listener线程和若干个工做线程,listener线程和worker线程能够动态转换,worker线程数目由工做负载决定,同时受到thread_pool_oversubscribe设置影响。此外,整个线程池有一个timer线程监控group,防止group“停滞”。

关键接口

     1. tp_add_connection[处理新链接]

       1)  建立一个connection对象

       2)  根据thread_id%group_count肯定connection分配到哪一个group

       3)  将connection放进对应group的队列

       4)  若是当前活跃线程数为0,则建立一个工做线程

    2. worker_main[工做线程]

      1)  调用get_event获取请求

      2)  若是存在请求,则调用handle_event进行处理

      3)  不然,表示队列中已经没有请求,退出结束。

    3. get_event[获取请求]

     1)  获取一个链接请求

     2)  若是存在,则当即返回,结束

     3)  若此时group内没有listener,则线程转换为listener线程,阻塞等待

     4)  若存在listener,则将线程加入等待队列头部

     5)  线程休眠指定的时间(thread_pool_idle_timeout)

     6)  若是依然没有被唤醒,是超时,则线程结束,结束退出 

     7)  不然,表示队列里有链接请求到来,跳转1

   备注:获取链接请求前,会判断当前的活跃线程数是否超过了

   thread_pool_oversubscribe+1,若超过了,则将线程进入休眠状态。

  4. handle_event[处理请求]

    1)  判断链接是否进行登陆验证,若没有,则进行登陆验证

    2)  关联thd实例信息

    3)  获取网络数据包,分析请求

    4)  调用do_command函数循环处理请求

    5)  获取thd实例的套接字句柄,判断句柄是否在epoll的监听列表中

    6)  若没有,调用epoll_ctl进行关联

    7)  结束

  5.listener[监听线程]

   1)  调用epoll_wait进行对group关联的套接字监听,阻塞等待

   2)  若请求到来,从阻塞中恢复

   3)  根据链接的优先级别,肯定是放入普通队列仍是优先队列

   4)  判断队列中任务是否为空

   5)  若队列为空,则listener转换为worker线程

   6)  若group内没有活跃线程,则唤醒一个线程

  备注:这里epoll_wait监听group内全部链接的套接字,而后将监听到的链接

  请求push到队列,worker线程从队列中获取任务,而后执行。

 6. timer_thread[监控线程]

   1)  若没有listener线程,而且最近没有io_event事件

   2)  则建立一个唤醒或建立一个工做线程

   3)  若group最近一段时间没有处理请求,而且队列里面有请求,则

   4)  表示group已经stall,则唤醒或建立线程

   5)检查是否有链接超时 

  备注:timer线程经过调用check_stall判断group是否处于stall状态,经过调用timeout_check检查客户端链接是否超时。

 7.tp_wait_begin[进入等待状态流程]

   1)  active_thread_count减1,waiting_thread_count加1

   2)设置connection->waiting= true

   3)  若活跃线程数为0,而且任务队列不为空,或者没有监听线程,则

   4)  唤醒或建立一个线程

  8.tp_wait_end[结束等待状态流程]

   1)  设置connection的waiting状态为false

   2)  active_thread_count加1,waiting_thread_count减1

   备注:

   1)waiting_threads这个list里面的线程是空闲线程,并不是等待线程,所谓空闲线程是随时能够处理任务的线程,而等待线程则是由于等待锁,或等待io操做等没法处理任务的线程。

   2)tp_wait_begin和tp_wait_end的主要做用是因为汇报状态,即便更新active_thread_count和waiting_thread_count的信息。

 9. tp_init/tp_end

    分别调用thread_group_init和thread_group_close来初始化和销毁线程池 

线程池与链接池

     链接池一般实如今Client端,是指应用(客户端)建立预先建立必定的链接,利用这些链接服务于客户端全部的DB请求。若是某一个时刻,空闲的链接数小于DB的请求数,则须要将请求排队,等待空闲链接处理。经过链接池能够复用链接,避免链接的频繁建立和释放,从而减小请求的平均响应时间,而且在请求繁忙时,经过请求排队,能够缓冲应用对DB的冲击。线程池实如今server端,经过建立必定数量的线程服务DB请求,相对于one-conection-per-thread的一个线程服务一个链接的方式,线程池服务的最小单位是语句,即一个线程能够对应多个活跃的链接。经过线程池,能够将server端的服务线程数控制在必定的范围,减小了系统资源的竞争和线程上下文切换带来的消耗,同时也避免出现高链接数致使的高并发问题。链接池和线程池相辅相成,经过链接池能够减小链接的建立和释放,提升请求的平均响应时间,并能很好地控制一个应用的DB链接数,但没法控制整个应用集群的链接数规模,从而致使高链接数,经过线程池则能够很好地应对高链接数,保证server端能提供稳定的服务。如图2所示,每一个web-server端维护了3个链接的链接池,对于链接池的每一个链接实际不是独占db-server的一个worker,而是可能与其余链接共享。这里假设db-server只有3个group,每一个group只有一个worker,每一个worker处理了2个链接的请求。

 

                                        图 2(链接池与线程池框架图)

线程池优化

1.调度死锁解决

     引入线程池解决了多线程高并发的问题,但也带来一个隐患。假设,A,B两个事务被分配到不一样的group中执行,A事务已经开始,而且持有锁,但因为A所在的group比较繁忙,致使A执行一条语句后,不能当即得到调度执行;而B事务依赖A事务释放锁资源,虽然B事务能够被调度起来,但因为没法得到锁资源,致使仍然须要等待,这就是所谓的调度死锁。因为一个group会同时处理多个链接,但多个链接不是对等的。好比,有的链接是第一次发送请求;而有的链接对应的事务已经开启,而且持有了部分锁资源。为了减小锁资源争用,后者显然应该比前者优先处理,以达到尽早释放锁资源的目的。所以在group里面,能够添加一个优先级队列,将已经持有锁的链接,或者已经开启的事务的链接发起的请求放入优先队列,工做线程首先从优先队列获取任务执行。

2.大查询处理

    假设一种场景,某个group里面的链接都是大查询,那么group里面的工做线程数很快就会达到thread_pool_oversubscribe参数设置值,对于后续的链接请求,则会响应不及时(没有更多的链接来处理),这时候group就发生了stall。经过前面分析知道,timer线程会按期检查这种状况,并建立一个新的worker线程来处理请求。若是长查询来源于业务请求,则此时全部group都面临这种问题,此时主机可能会因为负载过大,致使hang住的状况。这种状况线程池自己无能为力,由于源头多是烂SQL并发,或者SQL没有走对执行计划致使,经过其余方法,好比SQL高低水位限流或者SQL过滤手段能够应急处理。可是,还有另一种状况,就是dump任务。不少下游依赖于数据库的原始数据,一般经过dump命令将数据拉到下游,而这种dump任务一般都是耗时比较长,因此也能够认为是大查询。若是dump任务集中在一个group内,并致使其余正常业务请求没法当即响应,这个是不能容忍的,由于此时数据库并无压力,只是由于采用了线程池策略,才致使了请求响应不及时,为了解决这个问题,咱们将group中处理dump任务的线程不计入thread_pool_oversubscribe累计值,避免上述问题。

参考文档

http://ourmysql.com/archives/1303

http://blog.chinaunix.net/uid-28364803-id-3431242.html

http://www.atatech.org/articles/31833

https://dev.mysql.com/doc/refman/5.6/en/connection-threads.html

相关文章
相关标签/搜索