池化技术,包括线程池、连接池、内存池、对象池等。作用就是提前保存大量的资源,或将用过的资源保存起来,等下一次需要使用该资源时再取出来重复使用。
线程池:通过预先创建一定数量的线程,当有请求达到时,线程池分配一个线程提供服务,请求结束后,该线程又去服务其他请求。避免线程和内存对象的频繁创建和释放,降低服务端的并发度,减少上下文切换和资源的竞争,提高资源利用效率。
连接池,主要是数据库连接池,参考JDBC与数据库连接池。
线程池和连接池是两个不同的概念。
连接池一般在客户端设置,指客户端创建预先创建一定的连接,利用这些连接服务于客户端所有的DB请求。如果某一个时刻,空闲的连接数小于DB的请求数,则需要将请求排队,等待空闲连接处理。通过连接池复用连接,避免连接的频繁创建和释放,减少请求的平均响应时间,在请求繁忙时,通过请求排队,可以缓冲应用对DB的冲击。
线程池实现在server端,通过创建一定数量的线程服务DB请求,相对于one-conection-per-thread
的一个线程服务一个连接的方式,线程池服务的最小单位是语句,即一个线程可以对应多个活跃的连接。线程池,可以将 server 端的服务线程数控制在一定的范围,减少系统资源的竞争和线程上下文切换带来的消耗,同时也避免出现高连接数导致的高并发问题。线程池具有线程复用、控制最大并发数、管理线程、保护系统4个优点。保护系统:无论系统目前有多少连接或请求,超过最大设置的线程数的都需要排队,让系统保持高性能水平,从而防止DB出现雪崩,对底层DB起到保护作用。
通常来说,比较好的方式是将连接池和线程池结合起来使用。
线程池是MySQL 5.6的一个核心功能。MySQL处理连接的方式有3种(在5.6以前只有前两种):
MySQL Server通过thread_handling参数来选择使用哪种方式:
if (thread_handling <= SCHEDULER_ONE_THREAD_PER_CONNECTION)
one_thread_per_connection_scheduler(thread_scheduler,&max_connections, &connection_count);
else if (thread_handling == SCHEDULER_NO_THREADS)
one_thread_scheduler(thread_scheduler);
else
pool_of_threads_scheduler(thread_scheduler, &max_connections,&connection_count);
线程池被划分为多个group(组),每个组又有对应的工作线程,
每一个绿色的方框代表一个group,group数目由thread_pool_size
参数决定。每个group包含一个优先队列和普通队列,一个listener线程和若干个工作线程,listener线程和worker线程可以动态转换,worker线程数目由工作负载决定,同时受到thread_pool_oversubscribe
设置影响。此外,整个线程池有一个timer线程监控group,防止group停滞(stall)。
各个部分的作用:
ThreadPool运作简单描述,省略大量的复杂逻辑:
thread_id % thread_pool_size
确定落在哪个group;thread_pool_idle_timeout
后就自动退出,线程结束。当然,获取请求之前会先检查group中的running线程数是否超过thread_pool_oversubscribe + 1
,如果超过也会休眠;通过poll监听MySQL端口的连接请求,收到连接后,调用accept接口,创建通信socket初始化thd实例,vio对象等。根据thread_handling方式设置,初始化thd实例的scheduler函数指针,调用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
};
使用show variables like 'thread%'
命令,可得到所有参数:
thread_handling
:线程池模型,默认是one-thread-per-connection
,即不启用线程池;pool-of-threads
,即启用线程池thread_pool_size
:线程池group个数,一般设置为当前CPU核心数目。理想情况下,一个group一个活跃的工作线程,达到充分利用CPU的目的thread_pool_max_threads
:用来限制线程池最大线程数,超过该限制后将无法再创建更多的线程,默认为100000thread_pool_stall_limit
:用于timer线程定期检查group是否停滞异常的时间间隔,默认为500msthread_pool_idle_timeout
:worker线程最大空闲时间,默认为60秒,超出timeout后会退出。保证线程池中的工作线程在满足请求的情况下,保持比较低的水平thread_pool_oversubscribe
:该参数用于控制,group中的最大线程数,CPU核心上超频的线程数。这个参数设置值不含listen线程计threadpool_high_prio_mode
:表示高优先队列的模式,可选项:
thread_pool_high_prio_tickets
参数thread_pool_high_prio_tickets
:控制每个连接最多语序多少次被放入高优先级队列中,默认为4294967295,只有在thread_pool_high_prio_mode
为transactions时才有效tp_add_connection
:处理新连接
worker_main
:工作线程
get_event
:获取请求
备注:获取连接请求前,会判断当前的活跃线程数是否超过thread_pool_oversubscribe+1,若超过,则将线程进入休眠状态。
handle_event
:处理请求
listener
:监听线程
备注:这里epoll_wait监听group内所有连接的套接字,然后将监听到的连接请求push到队列,worker线程从队列中获取任务,然后执行。
timer_thread
:监控线程
备注:timer线程通过调用check_stall判断group是否处于stall状态,通过调用timeout_check检查客户端连接是否超时。
tp_wait_begin
:进入等待状态流程
备注:
tp_init
,tp_end
引入线程池可解决多线程高并发的问题,但也带来隐患。假设,A,B两个事务被分配到不同的group中执行,A事务已经开始且持有锁,但由于A所在的group比较繁忙,导致A执行一条语句后,不能立即获得调度执行;而B事务依赖A事务释放锁资源,虽然B事务可以被调度起来,但由于无法获得锁资源,导致仍然需要等待,这就是所谓的调度死锁。由于一个group会同时处理多个连接,但多个连接不是对等的。比如,有的连接是第一次发送请求;而有的连接对应的事务已经开启,并且持有部分锁资源。为了减少锁资源争用,后者显然应该比前者优先处理,以达到尽早释放锁资源的目的。因此在group里面,可以添加一个优先级队列,将已经持有锁的连接,或已经开启的事务的连接发起的请求放入优先队列,工作线程首先从优先队列获取任务执行。
假设一种场景,某个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
累计值,避免上述问题。