海山数据库(He3DB)RDS MySQL线程池方案详解

92 阅读19分钟

前言

对于应用开发人员来说肯定听说过连接池,却不一定听说过线程池,虽然二者都是池化的概念,但还是有所不同的:

连接池面向的是

**

数据库连接

**

,是针对数据库

Client侧的优化。连接池可将数据库连接数固定在一定范围内,避免业务端创建过多连接、达到server端最大连接数导致后续连接失败的问题;同时,由于保留了一定数量的连接,当业务端新请求到达时可直接复用、无需新建连接,节省业务侧建连时间。

线程池面向的是

**

数据库内的工作线程

**

,是针对数据库

Server侧的优化。线程池将工作线程数量固定在一定范围内(当连接数过多时会涉及排队),可避免并发过高时频繁上下文切换、缓存失效等问题,有效提升CPU利用率及数据库吞吐。

背景

社区版

MySQL

的连接处理方法默认是

one-thread-per-connection

,即为每个连接创建一个工作

线程,简称

**


线程(

Per_thread

)模式

**

。这种模式存在如下弊端:

由于系统的资源是有限的,随着连接数的增加,资源的竞争也

**


增加,连接的响应时间也随之增加

**

,如

response time

图所示。

在资源未耗尽时,数据库整体吞吐随着连接数增加。一旦连接数超过了某个耗尽系统资源的临界点,由于各线

**

程互相竞争,

CPU

时间片在大量线程间频繁调度,不同线程上下文频繁切换,徒增


系统开销,数据库整体吞吐


反而会下降

**

,如下图所示:

问:如何避免在连接数暴增时,因资源竞争而导致系统吞吐下降的问题呢?

MariaDB & Percona

中给出了简洁的答案:

**

线程池

**

线程池的原理在

percona blog

中有生动的介绍,其大致可类比为早高峰期间大量汽车想通过一座大

桥,如果采

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

Percona

的实

[

](www.percona.com/doc/percona…)

移植自

MariaDB

,并在此基础上

支持

优先级队列,是现在主流的开源线程池方案。其基本原理为:

预先创建一定数量的工作线程(

worker

线程)。在线程池监听线程(

listener

线程)从现有

连接中监听到新请求时,

判断当前请求是否属于高优先级队列,若属于,则放入高优先级队列,反之,则放入低优先级队列;之后,由工作线程按照先高优后低优的顺序来处理请求

。工作线程在服

务结束之后不销毁线程(处于

idle

状态一段时间后会退出

),

而是保留在线程池中继续等待下一个请求来临。

MariaDB vs Percona

Percona

的实

[

](www.percona.com/doc/percona…)

移植自

MariaDB

,并在此基础上添加了一些功能。特别

Percona

5.5-5.7

版本添加了优先级

调度。而

MariaDB 10.2

也支持了优先级调度,和

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

内核研发工程师