了解和使用MySQL线程池,看这篇文章就够了。

最近出现屡次因为上层组件异常致使DB雪崩的状况,笔者将部分监控DB启用了线程池功能,在使用线程池的过程当中不断深刻学习的同时,也遇到了很多问题。服务器

本文就来详细讲述一下MySQL线程池相关的知识,以帮助广大DBA快速了解MySQL的线程池机制,快速配置MySQL的线程池以及里面存在的一些坑。其实我想说,了解和使用MySQL线程池,看这篇文章就够了。架构

1、为什么要使用MySQL线程池

在介绍为何要使用线程池以前,咱们都知道随着DB访问量愈来愈大,DB的响应时间也会随之愈来愈大,以下图:并发

 

而DB的访问大到必定程度时,DB的吞吐量也会出现降低,而且会愈来愈差,以下图所示:高并发

 

那么是否有什么方式,能实现随着DB的访问量愈来愈大,DB始终表现出最佳的性能呢?相似下图的表现:性能

 

 

答案就是今天要重点介绍的线程池功能。总结一下,使用线程池的理由有两个:学习

一、减小线程重复建立与销毁部分的开销,提升性能测试

线程池技术经过预先建立必定数量的线程,在监听到有新的请求时,线程池直接从现有的线程中分配一个线程来提供服务,服务结束后这个线程不会直接销毁,而是又去处理其余的请求。这样就避免了线程和内存对象频繁建立和销毁,减小了上下文切换,提升了资源利用率,从而在必定程度上提升了系统的性能和稳定性。优化

二、对系统起到保护做用线程

线程池技术限制了并发线程数,至关于限制了MySQL的runing线程数,不管系统目前有多少链接或者请求,超过最大设置的线程数的都须要排队,让系统保持高性能水平,从而防止DB出现雪崩,对底层DB起到保护做用。orm

可能有人会问,使用链接池可否也达到相似的效果?

也许有的DBA会把线程池和链接池混淆,但其实二者是有很大区别的:链接池通常在客户端设置,而线程池是在DB服务器上配置;另外链接池能够起到避免了链接频繁建立和销毁,可是没法控制MySQL活动线程数的目标,在高并发场景下,没法起到保护DB的做用。比较好的方式是将链接池和线程池结合起来使用。

 

2、MySQL线程池介绍

 

MySQL线程池简介

为了解决one-thread-per-connection(每一个链接一个线程)存在的频繁建立和销毁大量线程以及高并发状况下DB雪崩的问题,实现DB在高并发环境依然能保持较高的性能。

Oracle和MariaDB都推出了ThreadPool方案,目前Oracle的Thread pool实现为Plugin方式,而且只添加到在Enterprise版本中,Percona移植了MariaDB的Thread pool功能,并作了进一步的优化。本文的环境就基于Percona MySQL 5.7版本。

MySQL线程池架构

MySQL的Thread pool(线程池)被划分为多个group(组),每一个组又有对应的工做线程,总体的工做逻辑仍是比较复杂,下面我试图经过简单的方式来介绍MySQL线程池的工做原理。

一、架构图

首先来看看Thread Pool的架构图。

 

二、Thread Pool的组成

从架构图中能够看到Thread Pool由一个Timer线程和多个Thread Group组成,而每一个Thread Group又由两个队列、一个listener线程和多个worker线程构成。下面分别来介绍各个部分的做用:

  • 队列(高优先级队列和低优先级队列)

用来存放待执行的IO任务,分为高优先级队列和低优先级队列,高优先级队列的任务会优先被处理。

什么任务会放在高优先级队列呢?

事务中的语句会放到高优先级队列中,好比一个事务中有两个update的SQL,有1个已经执行,那么另一个update的任务就会放在高优先级中。这里须要注意,若是是非事务引擎,或者开启了Autocommit的事务引擎,都会放到低优先级队列中。

还有一种状况会将任务放到高优先级队列中,若是语句在低优先级队列停留过久,该语句也会移到高优先级队列中,防止饿死。

  • listener线程

listener线程监听该线程group的语句,并肯定当本身转变成worker线程,是当即执行对应的语句仍是放到队列中,判断的标准是看队列中是否有待执行的语句。

若是队列中待执行的语句数量为0,而listener线程转换成worker线程,并当即执行对应的语句。若是队列中待执行的语句数量不为0,则认为任务比较多,将语句放入队列中,让其余的线程来处理。这里的机制是为了减小线程的建立,由于通常SQL执行都很是快。

  • worker线程

worker线程是真正干活的线程。

  • Timer线程

Timer线程是用来周期性检查group是否处于处于阻塞状态,当出现阻塞的时候,会经过唤醒线程或者新建线程来解决。

具体的检测方法为:经过queue_event_count的值和IO任务队列是否为空来判断线程组是否为阻塞状态。

每次worker线程检查队列中任务的时候,queue_event_count会+1,每次Timer检查完group是否阻塞的时候会将queue_event_count清0,若是检查的时候任务队列不为空,而queue_event_count为0,则说明任务队列没有被正常处理,此时该group出现了阻塞,Timer线程会唤醒worker线程或者新建一个wokrer线程来处理队列中的任务,防止group长时间被阻塞。

三、Thread Pool的是如何运做的?

下面描述极简的Thread Pool运做,只是简单描述,省略了大量的复杂逻辑,请不要挑刺~

Step1:请求链接到MySQL,根据threadid%thread_pool_size肯定落在哪一个group;

Step2:group中的listener线程监听到所在的group有新的请求之后,检查队列中是否有请求还未处理。若是没有,则本身转换为worker线程当即处理该请求,若是队列中还有未处理的请求,则将对应请求放到队列中,让其余的线程处理;

Step3:group中的thread线程检查队列的请求,若是队列中有请求,则进行处理,若是没有请求,则休眠,一直没有被唤醒,超过thread_pool_idle_timeout后就自动退出。线程结束。固然,获取请求以前会先检查group中的running线程数是否超过thread_pool_oversubscribe+1,若是超过也会休眠;

Step4:timer线程按期检查各个group是否有阻塞,若是有,就对wokrer线程进行唤醒或者建立一个新的worker线程。

四、Thread Pool的分配机制

线程池会根据参数thread_pool_size的大小分红若干的group,每一个group各自维护客户端发起的链接,当客户端发起链接到MySQL的时候,MySQL会跟进链接的线程id(thread_id)对thread_pool_size进行取模,从而落到对应的group。

thread_pool_oversubscribe参数控制每一个group的最大并发线程数,每一个group的最大并发线程数为thread_pool_oversubscribe+1个。若对应的group达到了最大的并发线程数,则对应的链接就须要等待。这个分配机制在某个group中有多个慢SQL的场景下会致使普通的SQL运行时间很长,这个问题会在后面作详细描述。

MySQL线程池参数说明

关于线程池参数很少,使用show variables like 'thread%'能够看到以下图的参数,下面就一个一个来解析:

 

 

  • thread_handling

该参数是配置线程模型,默认状况是one-thread-per-connection,即不启用线程池;将该参数设置为pool-of-threads即启用了线程池。

  • thread_pool_size

该参数是设置线程池的Group的数量,默认为系统CPU的个数,充分利用CPU资源。

  • thread_pool_oversubscribe

该参数设置group中的最大线程数,每一个group的最大线程数为thread_pool_oversubscribe+1,注意listener线程不包含在内。

  • thread_pool_high_prio_mode

高优先级队列的控制参数,有三个值(transactions/statements/none),默认是transactions,三个值的含义以下:

transactions:对于已经启动事务的语句放到高优先级队列中,不过还取决于后面的thread_pool_high_prio_tickets参数。

statements:这个模式全部的语句都会放到高优先级队列中,不会使用到低优先级队列。

none:这个模式不使用高优先级队列。

  • thread_pool_high_prio_tickets

该参数控制每一个链接最多语序多少次被放入高优先级队列中,默认为4294967295,注意这个参数只有在thread_pool_high_prio_mode为transactions的时候才有效果。

  • thread_pool_idle_timeout

worker线程最大空闲时间,默认为60秒,超过限制后会退出。

  • thread_pool_max_threads

该参数用来限制线程池最大的线程数,超过该限制后将没法再建立更多的线程,默认为100000。

  • thread_pool_stall_limit

该参数设置timer线程的检测group是否异常的时间间隔,默认为500ms。

 

3、MySQL线程池的使用

 

线程池的使用比较简单,只须要添加配置后重启实例便可。

具体配置以下:

#thread pool

thread_handling=pool-of-threads

thread_pool_oversubscribe=3

thread_pool_size=24

performance_schema=off

#extra connection

extra_max_connections = 8

extra_port = 33333

备注:其余参数默认便可

以上具体的参数在前面已作详细说明,下面是配置中须要注意的两个点:

一、之因此添加performance_schema=off,是因为测试过程当中发现Thread pool和PS同时开启的时候会出现内存泄漏问题(后文会详细叙述);

二、添加extra connection是防止线程池满的状况下没法登陆MySQL,所以特地用管理端口,以备紧急的状况下使用;

重启实例后,能够经过show variables like '%thread%';来查看配置的参数是否生效。

 

4、使用中遇到的问题

 

在使用线程池的过程当中,我遇到了几个问题,这里也顺便作个总结:

内存泄漏问题

DB启用线程池后,内存飙升了8G左右,以下图:

 

 

不但启用线程池后内存飙升了8G左右,并且内存还在持续增加,很明显启用线程池后存在内存泄漏问题了。

网上也有很多的人遇到这个问题,确认是percona的bug致使(https://jira.percona.com/browse/PS-3734),只有开启Performance_Schema和ThreadPool的时候才会出现,解决办法是关闭Performance_Schema,具体操做方法是在配置文件添加performance_schema=off,而后重启MySQL就OK。

下面是关闭PS后的内存使用状况对比:

 

备注:目前Percona server 5.7.21-20版本已经修复了线程池和PS同时打开内存泄漏的问题,从我测试的状况来看问题也获得了解决,你们能够直接使用Percona server 5.7.21-20的版本,以下图。

拨测异常问题

启用线程池之后,至关于限制了MySQL的并发线程数,当达到最大线程数的时候,其余的线程须要等待,新链接也会卡在链接验证那一步,这时候会形成拨测程序链接MySQL超时,拨测返回错误以下:

 

拨测程序链接实例超时后,就会认为master已经出现问题。极端状况下,重试屡次都有异常后,就启动自动切换的操做,将业务切换到从机。

这种状况有两种解决办法:

一、启用MySQL的旁路管理端口,监控和高可用相关直接使用MySQL的旁路管理端口。

具体作法为:在my.cnf中添加以下配置后重启,就能够经过旁路端口登陆MySQL了,不受线程池最大线程数的影响:

extra_max_connections = 8

extra_port = 33333

备注:建议启用线程池后,把这个也添加上,方便紧急状况下进行故障处理。

二、修改高可用探测脚本,将达到线程池最大活动线程数返回的错误作异常处理,看成超过最大链接数的场景。(备注:超过最大链接数只告警,不进行自动切换)

慢SQL引入的问题

随着对拨测超时的问题的深刻分析,线程池满只是监控拨测出现超时的其中一种状况,还有一种状况是线程池并无满,线上的两个配置:

thread_pool_oversubscribe=3

thread_pool_size=24

按照上面的两个配置来计算的话,总共能并发运行24x(3+1)=96,可是根据屡次问题的追中,发现有屡次线程池并无达到96,也就是说总体的线程池并无满。那会是什么问题致使拨测失败呢?

鉴于线程池的结构和分配机制,经过前面线程池部分的描述,你们都知道了在内部是将线程池分红一个一个的group,咱们线上配置了24个group,而线程池的分配机制是对Threadid进行取模,而后肯定该线程是落在哪一个group。

出现超时的时候,有不少的load线程到导入数据。也就是说那个时候有部分线程比较慢的状况。那么会不会是某个group的线程满了,从而致使新分配的线程等待?

有了这个猜测之后,接下来就是来验证这个问题。验证分为两步:

一、抓取线上运行的processlist,而后对threadid取模,看看是否有多个load线程落在同一个group的状况;

 

二、在测试环境模拟这种场景,看看是否符合预期。

线上场景分析

先来看线上的场景,经过抓取拨测超时时间点的processlist,找出当时正在load的线程,根据threadid进行去模,并进行汇总统计后,得出以下结果:

 

 

能够看出,当时第4和第7个group的请求个数都超过了4个,说明是单个group满致使的拨测异常。固然,也会致使部分运行很快的SQL变慢。

测试环境模拟场景分析

为了构建快速重现环境,我将参数调整以下:

thread_pool_oversubscribe=1

thread_pool_size=2

经过上面参数的调整,能够计算出最大并发线程为2x(1+1)=4,以下图,当活动线程数超过4个后,其余的线程就必须等待:

 

我模拟线上环境的方法为开启1个线程的慢SQL,这时测试环境的线程池状况以下:

 

按照以前的推测,这时Group1的处理能力至关于Group2的处理能力的50%,若是以前的推论是正确的,那么分配在Group1上的线程就会出现阻塞。

好比此时来了20个线程请求,按照线程池的分配原则,此时Group1和Group2都会分到10个线程请求。若是全部的线程请求耗时都是同样的,那么分配到Group1上的线程请求总体处理时间应该是分配到Group2上总体处理时间的2倍。

我使用脚本,并发起12个线程请求,每一个线程请求都运行select sleep(2),那么在Group1和Group2都空闲的状况下,运行状况以下:

2018-03-18-20:23:53

2018-03-18-20:23:53

2018-03-18-20:23:53

2018-03-18-20:23:53

2018-03-18-20:23:55

2018-03-18-20:23:55

2018-03-18-20:23:55

2018-03-18-20:23:55

2018-03-18-20:23:57

2018-03-18-20:23:57

2018-03-18-20:23:57

2018-03-18-20:23:57

每次4个线程,总共运行了6秒。

接下来在Group1被1个长时间运行的线程沾满之后,看看测试结果是怎么样的:

2018-03-18-20:24:35

2018-03-18-20:24:35

2018-03-18-20:24:35

2018-03-18-20:24:37

2018-03-18-20:24:37

2018-03-18-20:24:37

2018-03-18-20:24:39

2018-03-18-20:24:39

2018-03-18-20:24:39

2018-03-18-20:24:41

2018-03-18-20:24:43

2018-03-18-20:24:45

从上面的结果中能够看出,在没有阻塞的时候,每次都是4个线程,然后面有1个线程长时间运行的时候,就会出现那个长时间线程对应的group出现排队的状况,最后虽然有3个空闲的线程,可是却只有1个线程在处理(标红部分结果)。

解决方法有两个:

一、将thread_pool_oversubscribe适当调大,这个办法只能缓解相似问题,没法根治;

二、找到慢的SQL,解决慢的问题。