前言
对于应用开发人员来说肯定听说过连接池,却不一定听说过线程池,虽然二者都是池化的概念,但还是有所不同的:
连接池面向的是
**
数据库连接
**
,是针对数据库
Client侧的优化。连接池可将数据库连接数固定在一定范围内,避免业务端创建过多连接、达到server端最大连接数导致后续连接失败的问题;同时,由于保留了一定数量的连接,当业务端新请求到达时可直接复用、无需新建连接,节省业务侧建连时间。
线程池面向的是
**
数据库内的工作线程
**
,是针对数据库
Server侧的优化。线程池将工作线程数量固定在一定范围内(当连接数过多时会涉及排队),可避免并发过高时频繁上下文切换、缓存失效等问题,有效提升CPU利用率及数据库吞吐。
背景
社区版
MySQL
的连接处理方法默认是
one-thread-per-connection
,即为每个连接创建一个工作
线程,简称
**
每
线程(
Per_thread
)模式
**
。这种模式存在如下弊端:
由于系统的资源是有限的,随着连接数的增加,资源的竞争也
**
会
增加,连接的响应时间也随之增加
**
,如
response time
图所示。
在资源未耗尽时,数据库整体吞吐随着连接数增加。一旦连接数超过了某个耗尽系统资源的临界点,由于各线
**
程互相竞争,
CPU
时间片在大量线程间频繁调度,不同线程上下文频繁切换,徒增
系统开销,数据库整体吞吐
反而会下降
**
,如下图所示:
问:如何避免在连接数暴增时,因资源竞争而导致系统吞吐下降的问题呢?
MariaDB & Percona
中给出了简洁的答案:
**
线程池
**
。
线程池的原理在
中有生动的介绍,其大致可类比为早高峰期间大量汽车想通过一座大
桥,如果采
⽤
one-thread-per-connection
的方式则放任汽车自由行驶,由于桥面宽度有
限,最终将导致所有汽车寸步难
行。线程池的解决方案是限制同时行驶的汽车数,让桥面时刻保持
最大吞吐,尽快让所有汽车抵达对岸。
回归到数据库本身,
MySQL
默认的每线程模式,每个会话都会创建一个独占的线程。
**
当有大量的会话存在时,会导
致大量的资源竞争,同时,大量的系统线程调度和缓存失效也会导致性能急剧下降
**
。
线程池功能旨在解决以上问题,在存在大量连接的场景下,通过线程池实现
**
线程复用
**
:
当连接多、并发低时,通过连接复用,避免创建大量空闲线程,减少系统资源开销。
当连接多、并发高时,通过
**
限制同时运行的线程数,将其控制在合理的范围内,可避
免线程调度工作过多和大
**
量缓存失效,减少线程池间上下文切换和热锁争用
,从而对
OLTP
场景产生
积极影响。
当连接数上升时,在线程池的帮助下,将数据库整体吞吐维持在一个较高水
准,如下图所示:
适用场景
线程池采用一定数量的工作线程来处理连接请求,在查询相对较短且工作负载受
CPU
限制的情况下
效率最高,通常
适应于
OLTP
工作负载的场景:
对于大量连接的
OLTP
短查询场景有较大收益;
对于大量连接的只读短查询也有明显收益;
可有效避免大量连接高并发下数据库性能衰减。
如果工作负载不受
CPU
限制,那么仍然可以通过限制线程数量来节省数据库内存缓冲区所占用的内存。
线程池的不足在于当请求偏向于慢查询时,工作线程阻塞在高时延操作上,难以快速响应新的请求,导致系统吞吐
量反而相较于传统
one-thread-per-connection
模式更低。因此,不太适用于以下场景:
具有突发工作负载的场景
。在这种场景下,许多用户往往长时间处于非活跃状态,但个别时候又处于特别活跃
的状态,同时,对延迟的容忍度较低,因此,线程池节流效果不太理想。不过,即使在这种情况下,也可以通
过调整线程的退役频率(
thread_pool_idle_timeout
参数)来提高性能。
高并发、长耗时语句的场景
。在这种场景下,并发较多,且都是执行时间较长的语句,会导致工作线程堆积,
一旦达到上限,完全阻止后续语句的执行,比如最常见的数据仓库场景。当然这样的场景下,不管是否使用线
程池,数据库的表现都是不够理想的,
**
需要应用侧控制慢查询的
并发度
**
。
有较严重的锁冲突的场景
。如果处于锁等待的工作线程数超过总线程数,也会堆积起来,阻止无锁等待的处理
请求。比如某个会话执行
FLUSH TABLES WITH READ LOCK
语句获得全局锁后暂停
**
,
**
那么其他执行写操作的
客户端连接就会阻塞,当阻塞的数量超过线程池的上限时,整个
server
都会阻塞。当然
这样的场景下,不管
是否使用线程池,数据库的表现都是不够理想的,
**
需要应用侧
进行优化
**
。
极高并发的
Prepared Statement**
请求
**
。使用
Prepared Statement
时,会使用
MySQL Binary Protocol
,会
增加很多的网络开销,比如参数的绑定、结果集的返回,在极高请求压力下会给
epoll
监听进程带来一定的压
力,处于事务状态中时,可能会让普通请求得不到执行机会。
为了应对上述的阻塞问题,一般会允许配置
或
来管理连接。
总结一句话,
**
线程池更适合短连接或短查询的场景
**
。
行业方案
由于市面上的线程池方案大多都借鉴了
percona
、
mariadb
的方案,因此
,首先介绍下
percona
线程池的工作机
制,再说明其他方案相较于
percona
做了什么改进。
腾讯云
TXSQL
整合了
percona
的线程池方案,在此基础上实现了线程池的动态切换(动态开
启或关闭线程
池)、负载均衡优化(
percona
分配线程组时采用的轮询算法,
TXSQL
做了改进
)
。
阿里云
AliSQL
一定程度上也借鉴了
percona
的线程池方案,主要不同在于其采用了两层队列,第一层为网
络请求队列(区分为普通队列、高优先级队列
),
第二层为工作任务队列(区分为查询队列、更新队列、事
务队列)。
Percona
[
现
](www.percona.com/doc/percona…)
移植自
MariaDB
,并在此基础上
支持
了
优先级队列,是现在主流的开源线程池方案。其基本原理为:
预先创建一定数量的工作线程(
worker
线程)。在线程池监听线程(
listener
线程)从现有
连接中监听到新请求时,
判断当前请求是否属于高优先级队列,若属于,则放入高优先级队列,反之,则放入低优先级队列;之后,由工作线程按照先高优后低优的顺序来处理请求
。工作线程在服
务结束之后不销毁线程(处于
idle
状态一段时间后会退出
),
而是保留在线程池中继续等待下一个请求来临。
MariaDB vs Percona
[
现
](www.percona.com/doc/percona…)
移植自
MariaDB
,并在此基础上添加了一些功能。特别
是
Percona
在
5.5-5.7
版本添加了优先级
调度。而
也支持了优先级调度,和
Percona
的工作方式类似,只是细节有所不同。
MariaDB 10.2
版本的参数
thread_pool_priority=auto,high,low
对应于
Percona
的
MariaDB 10.2
版本中只有处于事务中的连接才是高优先级,而
Percona
中符合高优先级的情况包括:
1
)处
于事务中;
2
)持有表锁;
3
)持有
MDL
锁
;
4
)持有全局读锁;
5
)持有
backup
锁。
关于
**
避免低优先级队列语句饿死
**
的问题:
Percona
有一个
thread_pool_high_prio_tickets
参数,用于
**
指定每个连接在高优先级队列中的
tickets
数量
**
,而
MariaDB
没有相应参数。
MariaDB
有一个
thread_pool_prio_kickup_timer
参数,可
**
让低优先队列中的语句在等待指定时间
后移入高优先级队列
**
,而
Percona
没有相应参数。
MariaDB
有参数
thread_pool_dedicated_listener
、
thread_pool_exact_stats
,而
Percona
没
有。
thread_pool_dedicated_listener
:可用于
**
指定专有
listener
线程
**
,其只负责
epoll_wait
等待网
络事件,不会变为
worker
线程。默认为
OFF
,表示不固定
listener
。
thread_pool_exact_stats
:是否使用高精度时间戳。
MariaDB
(比如
10.9
版本)在
information_schema
中新增了四张表
(
THREAD_POOL_GROUPS
、
THREAD_POOL_QUEUES
、
THREAD_POOL_STATS
、
THREAD_POOL_WAITS
),便
于监控线程池状态。
MySQL
企业版
vs MariaDB
MySQL
企业版是在
5.5
版本引入的线程池,以插件的方式实现的。
相同点:
都具备线程池功能,都支持
thread_pool_size
参数。
·
都支持专有
listener
线程(
thread_pool_dedicated_listeners
参数)。
都支持高低优先级队列,且在避免低优先级队列事件饿死方面,二者采用了相同方案,即低优先级队列事件等
待一段时间(
thread_pool_prio_kickup_timer
参数)即可移入高优先级队列。
都使用相同的机制来探测处于停滞(
stall
)状态的线程,都提
供了
thread_pool_stall_limit
参数
(
MariaDB
单位是
ms
,
MySQL
企业版单位是
10ms
)。
不同点:
Windows
平台实现方式不同。
MariaDB
使用
Windows
自带的线程池,而
MySQL
企业版的实现用到了
WSAPoll()
函
数(为了便于移植
Unix
程序而提供
),
因此,
MySQL
企业版的实现将不能使用命名管道和共享内存。
MariaDB
为每个操作系统都使用最高效的
IO
多路复用机制。
Windows
:原生线程池
○
Linux
:
epoll
○
Solaris ( event ports )
FreeBSD and OSX kevent )
⽽
MySQL
企业版只在
Linux
上才使用优化过的
IO
多路复用机
制
epoll
,其他平台则用
poll
。
移动云方案
概述
核心功能与
percona
线程池方案
类似
,优先级调度算法
及
避免低优先级队列语句饿死的策略
也有所参考
,
除此之外,
额外做了
一些改进:
使用插件方式实现。
·
借鉴了
MariaDB
的实现,添加了参数
thread_pool_dedicated_listener
,即支持固定
listener
功能。
借鉴了
MariaDB
的实现,在
information_schema
中新增了四张表
(
THREAD_POOL_GROUPS
、
THREAD_POOL_QUEUES
、
THREAD_POOL_STATS
、
THREAD_POOL_WAITS
),便
于监控线程池状态。
一些优化点:
○
添加参数
thread_pool_toobusy
,表示线程组是否过于忙碌的线程数阈值。当线程组中活跃的工作线
程数
锁或
IO
等待中的工作线程数>该阈值加
1
时,认为线程组过于忙碌,不再处理低优先级的任务,等
待当前执行的任务和高优先级队列中的任务被处理,直到线程组回到非忙碌的状态。
**
该优化能避免
**
percona
的问题
**
——
极端高并发场景下,随着工作线
程的持续创建,退化为每线程模式
**
。
高优先级
session**
独占
worker
线程:在连接数很大,高
负载时,对于一些事务取得了锁等资源时,可
优先处理
**
。
. percona/mariadb
的处理逻辑是此类连接发生可读事件后,会被线程组加到优先队
列中,等待空闲
worker
线程优先处理。
.
移动云优化后的逻辑,
**
需要优先处理的
session
不将当前
worker
还给线程池,继续
独占当前
**
worker
线程
,类似每线程每连接的模式,独占
worker
线程专用于处理该优先连接之后的所有语
句,直到该连接释放了优先资源转为普通连接,例如该连接事务执行结束释放锁资源。
listener
线程调用
io_poll_wait
后,只要线程组不繁忙,
则按需批量唤醒或创建一批
worker
线程(根据
本次获得的
event
数量、活跃线程数来决定
worker
数量)。
设计方案
下面从线程池架构、新连接的创建与分配、
listener
线程、
worker
线程、
timer
线程等几个方面来介绍线程
池的实现。
1.
线程池的架构
线程池由
**
多个线程组(
thread group
)
**
和
timer**
线程
**
组成,如下图所示。
线程组的数量是线程池并发的上限,通常而言
**
线程组的数量
**
需要配置成
**
数据库实例的
CPU
核心数量
**
(通过参
数
thread_pool_size
设置
),
从而充分利用
CPU
。线程组之间通过
线程
ID %
线程组数
的方式分配连接,线程组
内通过竞争方式处理连接。
线程池中还有一个服务于所有线程组的
timer**
线程
**
,负责周期性(检查时间间隔为
threadpool_stall_limit
毫
秒)检查线程组是否处于阻塞状态。当检测到阻塞的线程组时,
timer
线程会通过唤醒或创建新的工作线程
来让线程组恢复工作。
创建新的工作线程
并
不是每次都能创建成功
的
,要根据当前的线程组中的线程数是否大于线程组中的连接数,活跃线程
数是否为
0
,以及上一次创建线程的时间间隔是否超过阈值(这个阈值与线程组中的线程数有关,
线程组中的线程
数越多,时间间隔越大)
等条件来决定
。
线程组内部由
**
多个
worker
线程、
0
或
1
个动态
listener
线程、高低优先级事件队列(由网络事件
event
构成)、
mutex
、
epollfd
、统计信息等组成
**
。如下图所示:
worker
线程
:主要作用是从队列中读取并处理事件。
·
如果该线程所在组中没有
listener
线程,则该
worker
线程将成为
listener
线程,通过
epoll
的方式监听数据,并
将监听到的
event
放到线程组中的队列。
worker
线程数目动态变化,并发较大时会创建更多的
worker
线程,当从队列中取不到
event
时,
work
线程将
休眠,超过一定时间后结束线程。
一个
worker
线程只属于一个线程组。
listener
线程
:当高低队列为空,
listen
er
线程会自己处理(无论这次获取到多少事务)。否则
listen
er
线程会把请求加
入到队列中,
**
如果此时
active_thread_count=0****
,
则
唤醒一个工作线程
**
。
高低优先级队列
:为了提高性能,将队列分为高优先队列和普通队列。这里采用引入两
个新变量
thread_pool_high_prio_tickets
和
thread_pool_high_prio_mode
。由它们控制高优先级队列策略。对每
个新连接分配可以进入高优先级队列的
ticket
。
2.
新连接的创建与分配
新连接接入时,线程池按照新连接的线程
id
取模线程组个数来确定新连接归属的线程组(
group_count
)。
选定新连接归属的线程组后,
**
新连接申请
**
被作为
**
事件
**
放入
**
低优先级队列
**
中,等待线程组中
worker
线程将
**
高优先级事
件队列
**
处理完后,就会处理低优先级队列中的请
求。
3. listener
线程
listener
线程是负责监听连接请求的线程,
**
每个线程组都有一个
listener
线程
**
。
线程池的
listener
采用
epoll
实现。当
epoll
监听到请求事件时,
listener
会根据
**
请求事件的类
型
**
来决定将其
放入哪个优先级事件队列。
**
将事件放入高优先级队列的条件如下
**
:
当前线程池的工作模式为
**
高优先级模式
**
,在此模式下只启用高优先级队列
。
当前线程池的工作模式为
**
事务模式
**
,在此模
式下
**
每个连接的
**event
最多被放入高优先级队
列
threadpool_high_prio_tickets
次。超过
threadpool_high_prio_tickets
次后,该连接的请求事件
只能被放入低优先级
,
同时,
也会重置票数。
**
以下条件
只需要满足其一即可
**
:
连接持有
**
表锁
**
连接持有
mdl**
锁
**
连接持有
**
全局读锁
**
连接持有
backup**
锁
**
被放入高优先级队列的事件可以优先被
worker
线程处理。
只有当高优先级队列为空且当前线程组不繁忙的时候
**
,
才处理低优先级队列中的事件
**
。线程组
繁忙
的判断条件是
**
当前组内活跃工作线程数
+
组内处于等待状态的线程数
**
大于
**
线程
组工作线程额定值
**
(
thread_pool_oversubscribe+1
)。
listener
线程将事件放入高低优先级队列后,如果
**
线程组的活跃
worker
数量为
**0
,则唤醒或创建新的
worker
线程来
处理事件。
线程池中
listener**
线程和
worker
线程是可以互相切换的
**
,详细的切换逻辑会在「
worker
线程」一节介
绍。
epoll
监听到请求事件时,如果高低优先级事件队列都为空,意味着此时线程组非常空闲,大概率不存在活跃
的
worker
线程。
listener
在此情况下会将除第一个事件外的所有事件按前述规则放入高低优先级事件队列,
**
然后退出监听任
务,亲自处理第一个事件
**
。
这样设计的好处在于当线程组非常空闲时,可以避免
listener
线程将事件放入队列,
唤醒或创建
worker
线程来
处理事件的开销,提高工作效率。
4. worker
线程
worker
线程是线程池中真正干活的线程,正常情况下,每个线程组都会有一个活跃
的
worker
线程。
worker
在理想状态下,可以高效运转并且快速处理完高低优先级队列中的事件。但是在实际场景中,
worker
经常
会遭遇
IO
、锁等等待情况而难以高效完成任务,此时任凭
worker
线程等待将使得在队列中的事件迟迟得不到处理,
甚至可能出现长时间没有
listener
线程监听新请求的情况。为此,每当
worker
遭遇
IO
、锁等等待情况,如
果此时线
程组中没有
listener
线程或者高低优先级事件队列非空,并且没有过多活跃
worker
,则会尝试唤醒或者创建
一个
worker
。
为了避免短时间内创建大量
worker
,带来系统吞吐波动,线程池创建
worker
线程时有一个控制单位时间
创建
worker
线程上限的逻辑,线程组内连接数越多则创建下一个线程需要
等待的时间越长。
**
在极端情况下,可能会出现
worker线程总数接近最大连接数(max_connections)的情况,相当于退化为每线程模式
**
。
当
**
线程组活跃
worker
线程数量
**
大于等于
too_many_active_threads+1
时,认为线程组的活跃
worker
数量过
多。
此时需要对
worker
数量进行
**
适当收敛
**
,首先判断当前线程组是否有
listener
线程:
如果没有
listener
线程,则将当前
worker
线程转化为
listener
线程。
如果当前有
listener
线程,则在进入休眠前尝试通过
epoll_wait
获取一个尚未进入队列的事件,成功获取到
后立刻处理该事件,否则进入休眠等待被唤醒,等待
threadpool_idle_timeout
时间后仍未被唤醒则销毁
该
worker
线程。
worker
线程与
listener
线程的切换如下图所示:
5. timer
线程
timer
线程每隔
threadpool_stall_limit
时间进行一次所有线程组的扫描。
当线程组高低优先级队列中存在事件,并且自上次检查至今
没有新的事件被
worker
消费,则认为线程组处于
**
停滞状
态
**
。
停滞的主要原因可能是长时间执行的非阻塞请求
。
timer
线程会通过唤醒或创建新的
worker
线程来让停滞的线程组
恢复工作。
timer
线程除上述工作外,
**
还负责终止空闲时间超过
wait_timeout
秒的客户端
**
。
性能结果
移动云优化后的线程池会将工作线程数控制在一定范围内,随着并发数的增加,性能基本与最高点持平,无明显下降趋势。
总结
最后,总结下本文中几种方案在使用方面的区别。
功能
MySQL
企业版
MariaDB
Percona
移动云
功
**
能
实
现
方
式
**
插件
非插件
非插件
插件
版
**
本
**
5.5
版本引入
5.5
版本引入,
10.2
版
本完善
5.5-5.7/8.0
5.7/8.0
借
**
鉴
方
案
**
-
-
MariaDB
Percona +
MariaDB 10.2
及之
后版本
动
态
开
关
线
程
池
插件式,不支持
不支持
不支持
插件式,不支持
优
**
先
级
处
理
策
略
**
设定高低优先级,且低
优先级事件等待一段时
间可升为高优先级队列
设定高低优先级,且低
优先级事件等待一段时
间可升为高优先级队列
设定高低优先级,
且限制每个连接在
高优先级队列中的
票数
设定高低优先级,
且限制每个连接在
高优先级队列中的
票数
监
**
控
**
-
2
个状态变量
2
个状态变量
4
张状态信息表
如果线程池阻塞了,怎么处理?
MySQL 8.0.14
以前的版本使用
admin_port
功能。
功能(
percona & mariadb
),
8.0.14
及之后版本官方支持了
参数
由于业内线程池方案基本都会参考
MariaDB
或
Percona
,因此,以
Percona
和
MariaDB
的参数为准,基于
MySQL 8.0
,总结其他方案是否有相同或类似参数。
注意:
MySQL
企业版核心方案与
MariaDB
类似,且关于差异点,官方描述较少,因此,不做对比。
监控
Percona
只有两个状态变量:
移动云借鉴了
MariaDB
的实现方式,在
information_schema
中增加
了四张
**
状态信息表
**
:
THREAD_POOL_GROUPS
查询线程组相关信息。
THREAD POOL_QUEUES
查询线程组队列中连接的信息。
THREAD_POOL_STATS
查询线程组状态信息的统计值,比如线程组由于
check_stall
创建的线程数、由
listener
线程
poll
到的任务数等。
THREAD_POOL_WAITS
提供线程组的
worker
线程在执行
SQL
语句时,各类等待原因的统计数据。
等待原因
有:
UNKNOWN
、
SLEEP
、
DISKIO
、
ROW_LOCK
、
GLOBAL_LOCK
、
META_DATA_LOCK
、
TABLE_LOCK
、
USER_LOCK
、
BINLOG
、
GROUP_COMMIT
、
SYNC
、
NET
。
参考链接
1. Percona
:
1. Thread pool - Percona Server for MySQL
2. SimCity outages, traffic control and Thread Pool for MySQL (percona.com)
3
.
线程池详解
2. MariaDB
:
1. Thread Pool in MariaDB - MariaDB Knowledge Base
3. MySQL
企业版:
1. MySQL :: MySQL 8.0 Reference Manual :: 5.6.3 MySQL Enterprise Thread Pool
作者
卢文双,中国移动云能力中心数据库产品部
- MySQL
内核研发工程师