当前位置: 首页 > 科技观察

MySQL线程池内幕

时间:2023-03-17 01:23:29 科技观察

摘要在MySQL中,线程池是指一种用于管理处理MySQL客户端连接任务的线程的机制。我厂使用的percona版本已经集成了线程池,只需要通过下面的参数就可以开启。thread_handling=pool-of-threads本文在介绍MySQL线程池核心参数的基础上进一步介绍了线程池的内部实现机制。线程池简介线程池简介在继续了解MySQL线程池之前,我们首先要了解为什么引入线程池可以帮助MySQL提升性能。线程池除了性能还有哪些作用?如果把线程看作是系统资源,那么线程池本质上就是对系统资源的管理。对于操作系统来说,线程的创建和销毁都会消耗系统资源。频繁的创建和销毁线程势必会给系统带来不必要的资源浪费,尤其是在高负载的情况下。这部分开销严重影响系统的资源利用效率,进而影响系统的性能和吞吐量。另一方面,过多的线程创建会造成系统资源的超负荷消耗,同时带来线程间相对频繁的上下文切换。系统资源是宝贵的。我认为性能和资源利用率是密切相关的:资源利用率和性能趋于同向发展,好的资源利用率通常会带来更好的性能。线程池技术一方面可以减少重复创建和销毁线程的开销,从而更好地利用创建的线程资源。另一方面,它也可以控制线程的创建和系统的负载。在某些情况下,它可以保护系统。如何理解MySQL线程池是学习MySQL线程池的好方法,通过学习和掌握MySQL的参数,深入理解每个参数的含义以及这些参数对MySQL的影响。另外,在了解MySQL基本实现原理的基础上,进一步思考MySQL线程池的不足和可以改进的地方,有利于更好地理解MySQL线程池技术。线程池核心参数MySQL线程池向用户开放了一些参数。用户可以修改这些参数来影响线程池的行为。下面分别介绍这几个核心参数。参数thread_pool_size指的是线程组的大小。默认为CPU核心数。线程池初始化时,会根据这个编号生成线程组,每个线程组会初始化一个poolfd句柄。thread_pool_stall_limitTimer线程迭代间隔,默认500ms。thread_pool_oversubscribe用于计算线程组是否太活跃或太忙,即系统的负载水平。用于决定是否创建新的工作线程,以及在某个场景下是否处理任务。默认值为3。thread_pool_max_threads允许线程池中的最大线程数,默认为10000。thread_pool_idle_timeoutworkerthread***空闲时间,如果worker线程超过这个数仍然空闲,就会退出。此值默认为60秒。参数thread_pool_high_prio_mode可以用来控制任务队列的使用,可以取三个值:transactionsstatementsnone值为statements时,线程组只使用优先级队列;当值为none时,只使用普通队列;当值为事务时,与thread_pool_high_prio_tickets参数一起生效,该参数用于控制任务放入优先级队列的最大次数。thread_pool_high_prio_tickets当thread_pool_high_prio_mode=transactions时,每个连接的任务最多放入优先级队列thread_pool_high_prio_tickets,每次放入时递减,直到小于等于0,放入普通队列。默认值为4294967295。MySQL线程池的整体结构与JAVA线程池不同。JAVA线程池是工作线程,而MySQL线程池有线程组的概念。线程组的内部层次是工作线程。我们先来看看MySQL的线程池。一般架构:线程池架构上图可以大致看出线程池的内部结构。线程组是我们比较关注的一个比较大的组件。线程组中各个组件的相互协调构成了线程组。每个线程组很好的工作构成了一个线程池。至于线程池的内部,我觉得值得了解的内容主要有以下几个方面:线程组Worker线程CheckStall机制任务队列Listener线程对于上面列举的几个方面,我们后面会介绍。线程组MySQL线程池在初始化时根据宿主机的CPU核数设置thread_pool_size,即线程池中线程组的个数。每个线程组初始化后,会通过底层IO库为其分配一个网络特定的句柄。IO库可以通过这个句柄监听绑定到它的socket句柄上准备好的IO任务。线程组的结构体定义如下:structthread_group_t{mysql_mutex_tmutex;connection_queue_tqueue;//低优先级任务队列connection_queue_thigh_prio_queue;//高优先级任务队列worker_list_twaiting_threads;//表示当前线程在没有线程时进入等待队列taskworker_thread_t*listener;//读取网络任务线程pthread_attr_t*pthread_attr;intpollfd;//特殊句柄intthread_count;//线程组中的线程数intactive_thread_count;//当前活动线程intconnection_count;//分配给的连接当前线程组intwaiting_thread_count;//表示当前线程正在执行命令处于等待状态/*Statsforthedlockdetectiontimerroutine.*/intio_event_count;//待处理任务数,从句柄中获取intqueue_event_count;//网络任务数从队列中移除意味着网络任务处理完毕ulonglonglast_thread_creation_time;//最后创建Intshutdown_pipe[2];boolshutdown;boolstalled;}MY_ALIGNED(512);线程池由多个线程组组成,线程池的细节基本都在线程组中。工作线程线程组中有0个或多个线程,这与Netty有些不同。Netty中有固定的线程轮流训练IO事件。worker线程只负责处理IO任务,listener只是MySQL线程池中的一个角色,每个线程的角色可以是listener或者worker。当工作线程为监听器时,负责从poolfd中读取就绪的IO任务。当它处于worker角色时,它负责处理这些IO任务。我们需要区分工作线程State的以下几种状态:活动状态:当工作线程处于正在处理任务的状态,没有被阻塞时,意味着工作线程会消耗CPU,增加系统的负载。如果工作线程将自己设置为侦听器,则不计入线程组的活动线程状态数。空闲状态:由于没有任务处理而留下的空闲状态。等待状态:如果工作线程在命令执行过程中由于IO、锁、条件、睡眠等原因需要等待,会通知线程池,将这些工作线程记录为等待状态。在一个线程组中,线程数有如下关系:thread_count=active_thread_count+waiting_thread_count+waiting_threads.length+listener.lengththread_count表示线程组中的线程总数,active_thread_count表示当前正在工作的线程数notblocked,waiting_thread_count代表的是任务期间阻塞的工作线程数,waiting_threads代表空闲线程列表。在MySQL线程池中,线程组中忙碌的线程数是active_thread_count和waiting_thread_count之和,因为此时这些线程无法处理新的任务,所以被认为是忙碌的。如果处于忙碌状态的线程数大于某个值,则线程组太忙(toomanyactive),将用于决定正常优先级的任务是否能及时处理。这个值定义为:thread_pool_oversubscribe+1的默认值为4。如果active_thread_count的个数大于等于某个值(上面的算法是4),线程组就被认为太活跃(toobusy),也就是说CPU负载可能太满了。该指标用于决定线程组是否继续执行对于普通优先级任务,以上逻辑可以用一句话概括:在正常工作的情况下,当工作线程取回任务时,如果线程组太active(太多active),即使有任务,工作线程也不会执行任务。如果不是太忙(toobusy),就会考虑高优先级的任务,低优先级的任务只有在线程组不是太忙(toobusy)的时候才会执行。注意上面的情况,所以线程池对系统负载有一定的保护作用,那么问题来了,如果有一些耗时任务(比如查询耗时),会不会导致后续的任务被推迟?有时候会觉得SQL写的很好,但是LongSQL莫名其妙?这就是下面要介绍的CheckStall机制。CheckStallMechanism如果后面的IO任务被前面的任务耗时过长影响了怎么办?这将不可避免地导致一些无辜的任务(或简单的INSERT操作,之所以以INSERT为例是因为INSERT通常非常快)受到影响,从而导致可能的延迟处理。线程池中有一个TimerThread,类似于我们很多系统中的TimeoutThread线程。这个线程会在一定的时间间隔迭代一次。迭代中所做的事情包括以下两部分:检查线程组的负载工作线程唤醒和创建。检查并处理超时的客户端连接。这里主要介绍第一部分的工作,即CheckStall机制。TimerThread定期检查线程组中的线程是否被阻塞(stall)。所谓阻塞就是有新的任务但是线程组中没有线程去处理。TimerThread通过queue_event_count判断线程组是否阻塞,IO任务队列是否为空。queue_event_count将在工作线程每次检索任务时累积。积累意味着任务正常处理,工作线程正常工作。每次check_stall后queue_event_count都会被清零,所以如果IO任务队列不为空,并且在一定时间间隔(stall_limit)后的下一次迭代中queue_event_count为空,则说明这段时间间隔内没有worker线程处理IO任务.然后CheckStall机制将尝试唤醒或创建工作线程。唤醒一个线程的逻辑很简单。如果waiting_threads中有空闲线程,就会唤醒一个空闲线程。否则,您需要尝试创建一个工作线程。创建线程可能不会成功。我们看创建线程的条件:如果没有空闲线程,也没有活动线程,则立即创建。这时候可能是因为没有工作线程或者工作线程被阻塞,或者存在潜在的死锁。否则,如果自上次创建以来的时间大于某个阈值,将创建线程。该阈值由线程组中的线程数决定。阈值与线程组线程数的关系如下:线程数Threshold<40<850*1000<16100*1000>=16200*1000阈值机制可以有效防止线程创建过多.这里还有一个问题,为什么阈值取决于线程池中的线程数呢?阈值可以依赖thread_pool_stall_limit的值吗?CheckStall机制可以被认为是一个专门做特殊事情的线程。毕竟线程组的内部逻辑比较混乱。任务队列任务队列是listener每次从poolfd训练出来的就绪任务。它分为优先级任务队列(high_prio_queue)和普通任务队列(queue)。优先队列中的IO任务会先被处理,然后才是普通队列中的任务被处理。那么什么样的任务会被认为是优先任务呢?官方列出了两个条件:连接在交易中。连接关联的prioritytickets值大于0,参数prioritytickets(thread_pool_high_prio_tickets)是为了防止高优先级任务一直被处理,而一些非高优先级任务处于饥饿状态很长时间。毕竟工作线程的创建也是有条件的。当high-priority优先级的任务每被放入高优先级队列时,prioritytickets的值就会减1,所以prioritytickets的值达到一定数量后必须小于等于0次,从而避免了最高优先级的问题。另外,队列的使用受参数thread_pool_high_prio_mode的影响,请参考参数thread_pool_high_prio_mode的介绍。io_event_count会在就绪IO任务轮转后放入队列时累加,当IO任务从队列中取出进行处理时会累加queue_event_count。Listener线程Listener主要从poolfd中训练其绑定的socket句柄的就绪IO事件。事件以任务的形式放入任务队列中,并进行相应的处理。如果监听器读取了一些IO任务怎么办?该怎么办?下面的回答是基于两个问题:监听器是否应该自己处理这些任务?或者把这些任务放到队列中让工作线程去处理?如果任务队列不为空,我们需要唤醒多少个工作线程?对于**问题,通常我们不想频繁的改变监听器的等待和唤醒状态,因为监听器刚刚被唤醒,所以我们更愿意让监听器利用它的时间片做一些工作。如果监听器自己不处理工作,就意味着必须唤醒其他线程来完成工作,这显然不太好。让监听器做任务的潜在问题是线程组可能在一段时间内无法及时处理网络任务。这不是主要问题,因为停顿将由定时器线程检查。但是一直依赖TimerThread也不好,因为stall_limit可能会设置很久。我们使用以下策略。如果任务队列不为空,此时我们可能有更多的任务网络任务,让其他线程处理任务,否则监听器自己处理任务。对于第二个问题,我们通常会为每个线程组维护一个活跃线程(活跃线程包括正在做任务的线程),所以唤醒一个工作线程的条件是当前活跃前景数为0,如果没有线程是唤醒,在只能依靠TimerThread检查摊位并唤醒它。由上可知,如果任务队列不为空,则可能没有线程及时处理任务,导致任务耗时,影响后续任务的执行。未来可能会通过放弃每个线程组的规则,只维护一个活动线程,防止网络任务长时间未处理。综上所述,使用MySQL线程池可以提高数据库的性能。设计者精心设计了线程池的创建和任务处理机制,但也带来了一些潜在的问题。最明显的是将耗时任务调度到其他任务。虽然存在不足,但用户还是可以通过掌握线程池的内部细节和深刻理解开启参数的含义,通过调整参数,在一定程度上优化MySQL线程池的使用。学以致用,到目前为止,你是否能够运用上面介绍的一些知识解决一些实际问题呢?