Percona的线程池 基本上是从Mariadb中引入,其实现思路也比较简单,就是在线程调度器那增加了一组新的回调函数。线程池可以有效改善在大并发下的性能;
Thread pool的原理在Percona的这篇博客描述的很生动;其实就是限制同时运行的线程数,让大家不要一起挤进来,有序运行负载。线程池的目的不是为了提高性能,而是为了保持性能的稳定。
在使用线程池的场景下,就不是通常的一个连接一个线程(one-thread-per-connection),你可以创建几千甚至上万个Socket连接,MySQL只会创建有限个线程来
以下从不同的点来介绍下相关的代码,这里没有太多的深入,也没有性能测试数据,只是阅读时带着疑问做的一些笔记;
Q:如何控制同时运行的并发线程数
参数thread_pool_size的命令可能会让大家产生误解,它不是指的线程池的大小,而是线程组的大小。类似所有创建的线程都在某一个group里,group的编号从1~thread_pool_size,每个group里的线程数可以通过参数thread_pool_oversubscribe来控制(默认为3),如果把thread_pool_oversubscribe设置为1,那么thread_pool_size就被严格限制为能够同时活跃的最大线程数。
thread_pool_size默认值为CPU核心数,最大为128(MAX_THREAD_GROUPS),在启动时,就会把128个Group对应的结构体(all_groups)初始化好。每个group(编号小于等于thread_pool_size)会创建一个epoll对象;
Q:如何处理新连接
当客户端发起一个连接请求时,会被main线程捕捉到(handle_connections_sockets),然后调用tp_add_connection将新的连接加入到某个线程池的group中(根据thread_id%thread_pool_size),每个连接创建一个connection_t结构体对象(这个connection结构体里包含了连接的相关信息,例如是否登录,thd,是否正在等待,tickets等等),
创建好的connection被加入到其所属group的队列中(thread_group->queue),如果当前该group没有active的线程(thread_group->active_thread_count == 0)尝试去唤醒一个工作线程或者新建一个线程(wake_or_create_thread):a.首先尝试唤醒当前空闲的线程(空闲的线程对象在thread_group->waiting_threads中),如果有的话,则将其从waiting_threads中移除,并发送信号 ; b.否则表示现在该group内还没有线程, 需要创建一个新的worker线程(create_worker);
在创建worker线程时,一个比较有趣的代码段:
if (tp_stats.num_worker_threads >= (int)threadpool_max_threads
&& thread_group->thread_count >= 2)
{
err= 1;
max_threads_reached= true;
goto end;
}
实际上thread_pool_max_threads不是完全严格的限制总共的线程数,只有当当前线程数大于该max值,且当前group已有2个及以上worker线程时才拒绝新建线程
从上一步可以保证有一个活跃线程被唤醒来处理新连接的登录请求,worker线程的回调函数为worker_main,也就是处理socket请求的真正函数逻辑。
Q:如何处理新请求
worker线程调用worker_main,在一个循环内干两件事儿:
#get_event
顾名思义,该函数的目的就是为了获取一个事件,大约有如下的流程:
a.如果当前活跃线程数大于thread_pool_oversubscribe,并且该group的stall状态为false(何时设置?),暗示这时候该group的活跃线程数太多了(oversubscribed)。
b.如果oversubscribed为false,则取从队列中取connection_t对象(queue_get),先从高优先级队列(thread_group->high_prio_queue)取,如果没有的话,再从普通队列(thread_group->queue)取。如果存在的话,取得该对象返回
c.如果当前group里没有正在监听的线程(这时候没有任何请求),则把当前worker线程设置为监听线程,进入epoll监听socket请求
对于监听到的新请求,如果当前group里的没有event,,则由监听线程自己来处理监听到的第一个任务,剩下的任务放到队列中,否则把任务加入到队列中,由其他worker线程来处理。
加入队列的规则:
1.当该连接的ticket没用完(初始值为thread_pool_high_prio_tickets, Percona引入) 并且事务是活跃时,将该请求加入到高优先级队列,将tickets—
2.否则,将ticket设为threadpool_high_prio_tickets,并加入到普通队列中;
这实际上和innodb的ticket类似,保证一个大事务不要占用worker线程太长时间。以免引起其他事务饿死。
然后会去唤醒别的worker线程
d.oversubscribed为false时,再次尝试一次通过epoll看看有没有新请求(io_poll_wait),有的话直接处理该请求,这里的等待时间为0
f.将该worker线程加入等待队列(thread_group->waiting_threads),进入condtion wait,等待被唤醒,等待时间为thread_pool_idle_timeout
超时线程将直接退出;如果是被唤醒的,则跳转到a
#handle_event
worker线程拿到一个新请求后,进入处理阶段,这里就比较简单了。
如果connection还没有认证(connection->logged_in为false),则调用threadpool_add_connection(connection->the) 进行账户认证
否则,调用threadpool_process_request处理新请求。
在处理完后,设置connection的超时时间,以决定是否在超时后断开链接(pool_timer.next_timeout_check会被设置成最小的值),具体则由timer线程来检查;
然后再次将该socket的fd加入到其group所属的epoll中。
Q:异常场景下的处理
后台有一个timer线程在初始化thread pool时(tp_init)被启动,线程回调函数为timer_thread,用于检查每个线程组是否被阻塞了(check_stall)。大体思路是,在一段时间内,如果任务队列不为空,且从上次检查到现在没有从队列中取出任务(即thread_group->queue_event_count为0),那么就回去尝试唤醒或新创建一个worker线程;
如果查询运行的时间都非常长,那么最终可能退化成thread-per-connection这种原始模式;
timer线程也负责检查connetion是否net timeout.
thread_pool_stall_limit在Percona版本里似乎暂时没用到,根据其语义应该是用来控制timer线程循环检查的间隔时间
wait_begin/wait_end在进入等待前调用(我们在写代码时要注意这一点),其中wait_begin会先确保要有活跃的线程来处理请求,然后才会去让当前线程进入等待;
BluePrint
(Percona开发中,当前是基于Percona 5.6.14, 你看到这篇博客时,下面的几点可能已经实现了):
提供三种控制模式:
当设置为transaction模式时,只有一个已经开启了事务的SQL才会进入到高优先级队列中;
当设置为statements模式时,单独的SQL总是进入高优先级队列;
当设置为none时,则总是不走优先队列
通过这种方式来让客户端自己选择;当然更进一步的,可以在grant 权限时为其设置一个模式
#2.timer-based priority escalation
基于计时器的优先调度? 语焉不详。猜测大概意思是在低优先级队列如果超过一定的时间,就自动转移到高优先队列。
#3.low priority queue throttling
描述了一种场景:大多数SQL在等待某个事务释放行锁,而该事务所对应的事件由于oversubscribe了,无法被调度的问题;
从代码 (尚未正式发布)来看,定义了一种线程忙的状态(就是active的线程加上wait的线程大于thread_pool_oversubscribe)。这种状态下不允许低优先级队列中的任务执行;那么对于上述场景,已经开启的事务拥有ticket且处于活跃状态,能够被优先更快的执行掉。