本文集中讨论mariadb 5.5中线程池的基本原理和实现细节
简单对比一下两种线程调度方案
one_thread_per_connection
每个连接在其生命周期内都是由server从thread cache中取出一个(如果有可用的话)或者创建一个线程来为其服务,之后这个线程或者被放到thread cache(和thread cache size有关)或者被释放
threadpool
由一定数目的worker线程处理所有的连接,并发连接数可以大于worker线程数目,连接发送过来的event(处理请求)被放到队列中,worker线程不断从队列中取出event,调用do_command执行查询和返回结果
对于并发量大的业务场景,one_thread_per_connection将会在短时间内创建大量的线程,一方面造成系统资源的大量消耗,另一方面大量并发的线程会加剧系统锁竞争和线程上下文切换代价,从而使得系统性能下降,threadpool调度方案能够利用较少数目的线程处理大量并发的连接,能够极大提高系统系性能,尤其适用于高并发、短事务场景
实现细节:
1、整个连接池内部被分成N个小的group,N默认为cpu的个数,可以通过参数thread_pool_size设置,group之间通过round robin的方式分配连接,group内通过竞争方式处理连接,一个worker线程只属于一个group
2、每个group有一个动态的listener,worker线程在循环取event时,发现队列为空时会充当listener通过epoll的方式监听数据,并将监听到的event放到group中的队列
3、延时创建线程,group中的活动线程数为0或者group被阻塞时,worker线程会被创建,worker线程被创建的时间间隔会随着group内已有的线程数目的增加而变大
4、worker线程,
数目动态变化,这也是相对于5.1版本的一个改进,并发较大时会创建更多的worker线程,
当从队列中取不到event时work线程将休眠,超过thread_pool_idel_timeout后结束生命
5、timer线程,它会每隔一段时间做两件事情:1)检查每个group是否被阻塞,判定条件是:group中的队列中有event,但是自上次timer检查到现在还没有worker线程从中取出event并处理;2)kill超时连接并做一些清理工作
线程池调用接口:
点击(此处)折叠或打开
- static scheduler_functions tp_scheduler_functions=
- {
- ...
- 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
- };
以上函数接口在被调用时都会传入一个THD,其中包含了其网络连接的所有信息,如:socket fd
tp_init
线程池初始化工作,对每个group初始化pollfd,启动timer线程
tp_add_connection
根据thd的thread_id采用round robin方式将其分配到一个group中,将thd封装到connection_t的一个结构体中,放到group中的队列中,入队时会判断group中的活动worker线程数(thread_group->active_thread_count)是否大于0,否则将唤醒或者创建一个worker线程(wake_or_create_thread)
worker_main
worker线程函数,逻辑很简单,循环从队列中取get_event,然后调用handle_event
点击(此处)折叠或打开
- ...
- for(;;)
- {
- connection = get_event(&this_thread, thread_group, &ts);
- if (!connection)
- break;
- this_thread.event_count++;
- handle_event(connection);
- }
- ...
get_event
从队列中取event,取不到则充当listener通过epoll从网络监听事件,如果最终还是没能取到event,休眠一段时间,直到被唤醒或者超时退出
handle_event
做两件事情:1)连接验证;2)处理连接请求直到连接空闲,为空闲连接设置一个超时期限,之后将连接的socket fd绑定到group中的epollfd
timer_thread
做了两件事情:1)检查是否有group被阻塞,阻塞将调用wake_or_create_thread;2)检查是否有连接超时,超时将调用tp_post_kill_notification
点击(此处)折叠或打开
- ...
- for (;;)
- {
- /* Check stalls in thread groups */
- for(i=0; i< array_elements(all_groups);i++)
- {
- if(all_groups[i].connection_count)
- check_stall(&all_groups[i]);
- }
-
- /* Check if any client exceeded wait_timeout */
- if (timer->next_timeout_check <= timer->current_microtime)
- timeout_check(timer);
- }
- ...
wake_or_create_thread
唤醒或者创建线程,被三处调用:
1)connection加入到queue中时,group中的队列中没有活动的worker线程:
tp_add_connection
|->queue_put
|->wake_or_create_thread
2)timer线程检查是否有group阻塞,满足两个条件中的任一个即被调用:group中没有listener并且队列为空;group被阻塞
timer_thread
|->check_stall
|->wake_or_create_thread
3)事务被阻塞(如:锁等待)通过MYSQL_CALLBACK接口调用,等待之前会确定group中有活动的worker线程,否则唤醒或者创建worker线程
tp_wait_begin
|->wait_begin
|->
|->wake_or_create_thread
更多的细节就不一一在此详述了,深入研究还得:有码有真相
几个和线程池相关的参数:
thread_pool_size:线程池中group的数目
thread_pool_max_thread:最大worker线程数目
thread_pool_idle_timeout:连接超时
thread_pool_stall_limit:timer线程工作的时间间隔
thread_pool_oversubscribe:一个group中的最大同时活动的worker线程数目
参考:
mariadb 5.5.28源码