diff --git a/docs/2014/01/10/advanced-programming-in-the-unix-environment-getopt/index.html b/docs/2014/01/10/advanced-programming-in-the-unix-environment-getopt/index.html index adb70358..8c67f7c3 100644 --- a/docs/2014/01/10/advanced-programming-in-the-unix-environment-getopt/index.html +++ b/docs/2014/01/10/advanced-programming-in-the-unix-environment-getopt/index.html @@ -555,7 +555,7 @@
-本文首发于 2023-05-04 22:07:40
本文主要从功能层面对比 percona-server、mariadb、华为鲲鹏 BoostKit 数据库使能套件、阿里云 AliSQL、腾讯 TXSQL、MySQL 企业版线程池方案,都基于 MySQL 8.0。
-至于源码层面,腾讯、阿里云、MySQL 企业版不开源,percona 借鉴了 mariadb 早期版本的实现,而华为鲲鹏同时借鉴了 mariadb 和 percona 的实现,但考虑到线程池代码只有 2000 行左右,相对简单,本文就不做过多阐述了。
- - -本文主要从功能层面对比 percona-server、mariadb、阿里云 AliSQL、腾讯 TXSQL、MySQL 企业版线程池方案,都基于 MySQL 8.0。
+至于源码层面,腾讯、阿里云、MySQL 企业版不开源,percona 借鉴了 mariadb 早期版本的实现,但考虑到线程池代码只有 2000 行左右,相对简单,本文就不做深入阐述。
-版本:
+
MariaDB 10.9,
Percona-Server-8.0.32-24,
华为鲲鹏 8.0.25版本:
MariaDB 10.9,
Percona-Server-8.0.32-24
社区版的 MySQL 的连接处理方法默认是为每个连接创建一个工作线程的one-thread-per-connection
(Per_thread)模式。这种模式存在如下弊端:
当连接数上升时,在线程池的帮助下数据库整体吞吐维持在一个较高水准,如图所示。
+当连接数上升时,在线程池的帮助下,将数据库整体吞吐维持在一个较高水准,如图所示。
线程池采用一定数量的工作线程来处理连接请求,线程池在查询相对较短且工作负载受 CPU 限制的情况下效率最高,通常比较适应于 OLTP 工作负载的场景。如果工作负载不受 CPU 限制,那么您仍然可以通过限制线程数量来为数据库内存缓冲区节省内存。
线程池的不足在于当请求偏向于慢查询时,工作线程阻塞在高时延操作上,难以快速响应新的请求,导致系统吞吐量反而相较于传统 one-thread-per-connection 模式更低。
@@ -422,10 +415,13 @@由于市面上的线程池方案大多都借鉴了 percona、mariadb 的方案,因此,首先介绍下 percona 线程池的工作机制,再说明其他方案相较于 percona 做了什么改进。
线程池的基本原理为:预先创建一定数量的工作线程(worker 线程)。在线程池监听线程(listener 线程)从现有连接中监听到新请求时,从工作线程中分配一个线程来提供服务。工作线程在服务结束之后不销毁线程(处于 idle 状态一段时间后会退出),而是保留在线程池中继续等待下一个请求来临。
下面我们将从线程池架构、新连接的创建与分配、listener 线程、worker 线程、timer 线程等几个方面来介绍 percona 线程池的实现。
@@ -433,9 +429,9 @@thread_pool_size
设置),从而充分利用 CPU。线程组之间通过线程ID % 线程组数
的方式分配连接,线程组内通过竞争方式处理连接。
线程池中还有一个服务于所有线程组的timer 线程,负责周期性(检查时间间隔为threadpool_stall_limit
毫秒)检查线程组是否处于阻塞状态。当检测到阻塞的线程组时,timer 线程会通过唤醒或创建新的工作线程(wake_or_create_thread
函数)来让线程组恢复工作。
创建新的工作线程不是每次都能创建成功,要根据当前的线程组中的线程数是否大于线程组中的连接数,活跃线程数是否为 0,以及上一次创建线程的时间间隔是否超过阈值(这个阈值与线程组中的线程数有关,线程组中的线程数越多,时间间隔越大)。
- +线程组内部由多个 worker 线程、0 或 1 个动态 listener 线程、高低优先级事件队列(由网络事件 event 构成)、mutex、epollfd、统计信息等组成。如下图所示:
- +worker 线程:主要作用是从队列中读取并处理事件。
active_thread_count=0
,唤醒一个工作线程。
高低优先级队列:为了提高性能,将队列分为优先队列和普通队列。这里采用引入两个新变量thread_pool_high_prio_tickets
和thread_pool_high_prio_mode
。由它们控制高优先级队列策略。对每个新连接分配可以进入高优先级队列的 ticket。
新连接接入时,线程池按照新连接的线程 id 取模线程组个数来确定新连接归属的线程组(thd->thread_id() % group_count
)。这样的分配逻辑非常简洁,但由于没有充分考虑连接的负载情况,繁忙的连接可能会恰巧被分配到相同的线程组,从而导致负载不均衡的现象,这是 percona 线程池值得被优化的点。
选定新连接归属的线程组后,新连接申请被作为事件放入低优先级队列中,等待线程组中 worker 线程将高优先级事件队列处理完后,就会处理低优先级队列中的请求。
-listener 线程是负责监听连接请求的线程,每个线程组都有一个listener 线程。
+listener 线程是负责监听连接请求的线程,每个线程组都有一个listener 线程。
percona 线程池的 listener 采用epoll实现。当 epoll 监听到请求事件时,listener 会根据请求事件的类型来决定将其放入哪个优先级事件队列。将事件放入高优先级队列的条件如下(见函数connection_is_high_prio
),只需要满足其一即可:
mode == TP_HIGH_PRIO_MODE_STATEMENTS
)-上图来源于腾讯数据库技术公众号
worker 线程是线程池中真正干活的线程,正常情况下,每个线程组都会有一个活跃的 worker 线程。
+worker 线程是线程池中真正干活的线程,正常情况下,每个线程组都会有一个活跃的 worker 线程。
worker 在理想状态下,可以高效运转并且快速处理完高低优先级队列中的事件。但是在实际场景中,worker 经常会遭遇 IO、锁等等待情况而难以高效完成任务,此时任凭 worker 线程等待将使得在队列中的事件迟迟得不到处理,甚至可能出现长时间没有 listener 线程监听新请求的情况。为此,每当 worker 遭遇 IO、锁等等待情况,如果此时线程组中没有 listener 线程或者高低优先级事件队列非空,并且没有过多活跃 worker,则会尝试唤醒或者创建一个 worker。
为了避免短时间内创建大量 worker,带来系统吞吐波动,线程池创建 worker 线程时有一个控制单位时间创建 worker 线程上限的逻辑,线程组内连接数越多则创建下一个线程需要等待的时间越长。
当线程组活跃 worker 线程数量大于等于too_many_active_threads+1
时,认为线程组的活跃 worker 数量过多。此时需要对 worker 数量进行适当收敛,首先判断当前线程组是否有 listener 线程:
【尝试获取一个未进入队列的事件】描述错误,此处需要改为【尝试从队列获取事件】。
+-上图来自于腾讯数据库技术公众号
timer 线程每隔threadpool_stall_limit
时间进行一次所有线程组的扫描(check_stall
)。
timer 线程每隔threadpool_stall_limit
时间进行一次所有线程组的扫描(check_stall
)。
当线程组高低优先级队列中存在事件,并且自上次检查至今没有新的事件被 worker 消费,则认为线程组处于停滞状态。
wait_begin/wait_end
(尝试唤醒或创建新的 worker 线程)被上层函数忘记调用的场景。timer 线程为了尽量减少对正常工作的线程组的影响,在check_stall
时采用的是try_lock
的方式,如果加不上锁则认为线程组运转良好,不再去打扰。
timer 线程除上述工作外,还负责终止空闲时间超过wait_timeout
秒的客户端。
timer 线程除上述工作外,还负责终止空闲时间超过wait_timeout
秒的客户端。
下面是 Percona 的实现:
check_stall
函数:
1 | check_stall |
information_schema
中新增了四张表(THREAD_POOL_GROUPS
、THREAD_POOL_QUEUES
、THREAD_POOL_STATS
、THREAD_POOL_WAITS
),便于监控线程池状态。核心功能与 percona 线程池方案相同,优先级调度算法 及 避免低优先级队列语句饿死的策略 也相同,但额外做了一些改进:
-thread_pool_dedicated_listener
,即支持固定 listener 功能。thread_pool_toobusy
:表示线程组是否过于忙碌的线程数阈值。当线程组中活跃的工作线程数+锁或 IO 等待中的工作线程数>该阈值加 1 时,认为线程组过于忙碌,不再处理低优先级的任务,等待当前执行的任务和高优先级队列中的任务被处理,直到线程组回到非忙碌的状态。information_schema
中新增了四张表(THREAD_POOL_GROUPS
、THREAD_POOL_QUEUES
、THREAD_POOL_STATS
、THREAD_POOL_WAITS
),便于监控线程池状态。简单来说,华为鲲鹏的线程池方案 = Percona + MariaDB + NUMA 亲和 + 插件化。
---详细说明见官方手册(参考链接 [5.a])。
-
接下来重点介绍下华为鲲鹏独有的优化(Percona 和 MariaDB 没有实现)。
-依赖:
-1 | # Ubuntu |
参数说明:thread_pool_sched_affinity
-是否支持命令行:是
-是否支持配置文件:是
-是否支持动态修改:是
-参数范围:Global
-参数类型:Bool
-默认值:OFF
-允许值:OFF、ON
-线程池插件默认关闭线程组与 numa 亲和。
-本配置功能使用的限制条件为 mysqld 进程可使用整机所有 numa,未使用 numactl 等方式限制 mysqld 进程的可使用 cpu 范围。
-本配置功能开启时,thread_pool_size 配置的数量的线程组将与服务器上的 numa 轮询亲和,即例如整机 numa 数为a
,numa 编号为0 ~ a-1
,则第 n 个线程组将会与第n%a
(n 对 a 的余数)个 numa 进行绑定。与 numa 亲和的线程组上创建的线程都会与该 numa 亲和。通过线程与 numa 亲和,使数据与 session 关联性大的类型的业务的跨 numa 内存访问概率降低,从而提升性能。
在连接数很大,高负载时,对于一些事务取得了锁等资源时,可优先处理。
-原先的处理逻辑(percona/mariadb 也是)是此类连接发生可读事件后,会被线程组加到优先队列中,等待空闲 worker 线程优先处理。
-进一步优化逻辑,需要优先处理的 session 不将当前 worker 还给线程池,继续独占当前 worker 线程,类似每线程每连接的模式,独占 worker 线程专用于处理该优先连接之后的所有语句,直到该连接释放了优先资源转为普通连接,例如该连接事务执行结束释放锁资源。
-优先 session 连接的判断逻辑如下图。
- -以下描述序号对应上图的数字标记点。
-相对于默认模式的线程池参数配置,使用小线程组数模式的线程池参数配置时,每个线程组上可以创建更多的 active 线程数,使长查询的连接绑定到某个线程组时,该长查询的连接对该线程组的时延影响可以更小或无明显时延差异。
-同时使用小线程组数模式时,对于部分场景(例如 OLTP writeonly)在连接数非常大(例如 8192 个连接)时,仍然可以保持 90%左右的曲线峰值。
-小线程组数模式相对于默认模式(使用默认参数),就是参数配置的优化使用,在高并发连接数时,可以更好保持峰值性能的配置模式,相关配置说明如下表:
-参数名称 | -默认模式配置 | -小线程组数模式 | -
---|---|---|
thread_pool_size | -默认为 CPU 逻辑核数,或手动配置为 1-3 倍 CPU 逻辑核数 | -配置为 4 倍 NUMA 数(TPCH 场景测试经验值) | -
thread_pool_dedicated_listener | -默认为 OFF,listener 线程可转为 worker 线程 | -配置为 ON,listener 线程只负责网络事件等待,不转为 worker 线程 | -
thread_pool_oversubscribe | -默认为 3 | -配置该值=基线版本最优性能时的连接数/thread_pool_size 的配置值 | -
thread_pool_toobusy | -默认为 13 | -配置该值=thread_pool_oversubscribe | -
AliSQL 线程池也一定程度借鉴了 Percona 的机制,但也有自己的特色:
thread_handling
在Per_thread 和 Thread_pool 模式中来回切换后,我们需要考虑的问题主要有以下几个:
Per_thread 模式下,每个用户连接对应一个handle_connection
线程,handle_connection
线程既负责用户网络请求的监听,又负责请求的处理。
Thread_pool 模式下,每个 thread_group 都用epoll
来管理其中所有用户连接的网络事件,监听到的事件放入事件队列中,交予 worker 处理。
不论是哪种模式,在处理请求的过程中(do_command
)切换都不是一个好选择,而在完成一次 command 之后,尚未接到下一次请求之前是一个较合适的切换点。
不论是哪种模式,在处理请求的过程中(do_command)切换都不是一个好选择,而在完成一次 command 之后,尚未接到下一次请求之前是一个较合适的切换点。
do_command
)之后判断thread_handling
是否发生了变化。thread_id % group_size
选定目标 thread_group,将当前用户连接迁移至 Thread_pool 的目标 thread_group 中,后续该用户连接的所有网络事件统一交予 thread_group 的 epoll 监听。在完成连接迁移之后,handle_connection 线程即可完成退出或者缓存至下一次 Per_thread 模式处理新连接时复用(此为原生 mysql 支持的逻辑,目的是避免 Per_thread 模式下频繁地创建和销毁 handle_connection 线程)。do_command
)之后判断thread_handling
是否发生了变化。thread_id % group_size
选定目标 thread_group,将当前用户连接迁移至 Thread_pool 的目标 thread_group 中,后续该用户连接的所有网络事件统一交予 thread_group 的 epoll 监听。在完成连接迁移之后,handle_connection 线程即可完成退出或者缓存至下一次 Per_thread 模式处理新连接时复用(此为原生 mysql 支持的逻辑,目的是避免 Per_thread 模式下频繁地创建和销毁 handle_connection 线程)。Per_thread_connection_handler::add_connection
函数调用。threadpool_process_request
)后,将用户线程网络句柄重新挂载到 epoll(start_io
)之前判断 thread_handling 是否发生了变化。如需切换则先将网络句柄从 epoll 中移除以及将连接的信息从对应 thread_group 中清除。由于 Per_thread 模式下每个连接对应一个 handle_connection 线程,还需为当前用户连接创建一个 handle_connection 线程,后续当前用户连接的网络监听和请求处理都交予该 handle_connection 线程处理。由于 thread_handling 可能随时动态变化,为了使得新连接能被新 thread_handling 处理,需要在新连接处理接口Connection_handler_manager::process_new_connection
中,读取最新的 thread_handling,利用其相应的连接管理方法添加新连接。
group_efficiency
表示一定的时间周期内,线程组处理完的 event 总数占(工作队列存量 event 数+新增 event 数)的比例。此信息策略的优势在于能够直观反映出线程组一定时间周期内的工作效率,不足在于对于运转良好的线程组也可能存在误判:当时间周期选择不合适时,运转良好的线程组可能存在时而 group_efficiency 小于 1,时而大于 1 的情况。 在明确了度量线程组负载的方法之后,我们接下来讨论如何均衡负载。我们需要考虑的问题主要如下:
1) 负载均衡算法的触发条件
-负载均衡操作会将用户连接从一个线程组迁移至另一个线程组,在非必要情况下触发用户连接的迁移反而会导致用户连接的性能抖动。为尽可能避免负载均衡算法错误触发,我们需要为触发负载均衡算法设定一个负载阈值 M,以及负载比例 N。只有线程组的负载阈值大于 M,并且其与参与均衡负载的线程组的负载比例大于 N 时,才需要启动负载均衡算法平衡负载。
+负载均衡操作会将用户连接从一个线程组迁移至另一个线程组,在非必要情况下触发用户连接的迁移反而会导致用户连接的性能抖动。为尽可能避免负载均衡算法错误触发,我们需要为触发负载均衡算法设定一个负载阈值 M,以及负载比例 N。只有线程组的负载阈值大于 M,并且其与参与均衡负载的线程组的负载比例大于 N 时,才需要启动负载均衡算法平衡负载。
2) 负载均衡的参数对象
Q:当线程组触发了负载均衡算法后,该由哪些线程组参与平衡高负载线程组的负载呢?
很容易想到的一个方案是我们维护全局的线程组负载动态序列,让负载最轻的线程组负责分担负载。但是遗憾的是为了维护全局线程组负载动态序列,线程组每处理完一次任务都可能需要更新自身的状态,并在全局锁的保护下更新其在全局负载序列中的位置,如此一来对性能的影响势必较大,因此全局线程组负载动态序列的方案并不理想。
@@ -731,7 +647,7 @@如前文所述,线程池采用 epoll 来处理网络事件。当 epoll 监听到网络事件时,listener 会将网络事件放入事件队列或自己处理,此时相应用户连接不会被 epoll 监听。percona 线程池需要等到请求处理结束之后才会使用 epoll 重新监听用户连接的新网络事件。percona 线程池这样的设计通常不会带来问题,因为用户连接在请求未被处理时,也不会有发送新请求的需求。但特殊情况下,如果用户连接在重新被 epoll 监听前自行退出了,此时用户连接发出的断连信号无法被 epoll 捕捉,因此在 mysql 服务器端无法及时退出该用户连接。这样带来的影响主要有两点:
+如前文所述,线程池采用 epoll 来处理网络事件。当 epoll 监听到网络事件时,listener 会将网络事件放入事件队列或自己处理,此时相应用户连接不会被 epoll 监听。percona 线程池需要等到请求处理结束之后才会使用 epoll 重新监听用户连接的新网络事件。percona 线程池这样的设计通常不会带来问题,因为用户连接在请求未被处理时,也不会有发送新请求的需求。但特殊情况下,如果用户连接在重新被 epoll 监听前自行退出了,此时用户连接发出的断连信号无法被 epoll 捕捉,因此在 mysql 服务器端无法及时退出该用户连接。这样带来的影响主要有两点:
show threadpool status
,可展示 25 个线程池状态变量。由于腾讯 TXSQL、Percona 官方手册都没有性能数据,因此仅列出其他几种方案的性能结果。
-本小节内容来源于官网手册。
MariaDB 官网是基于 5.5 版本线程池测试的,也就是不支持高低优先级队列的版本。
-采用 Sysbench 0.4,以 pitbull (Linux, 24 cores) 的情况来说明在不同场景下的 QPS 情况。
+采用 Sysbench 0.4,以pitbull (Linux, 24 cores) 的情况来说明在不同场景下的 QPS 情况。
4456 |
484 |
MySQL 企业版 | MariaDB | Percona | -华为鲲鹏 | 腾讯 TXSQL | 阿里云 AliSQL | 插件 | 非插件 | 非插件 | -插件 | 非插件 | -- | +据传是插件方式 |
---|---|---|---|---|---|
版本 | @@ -955,7 +868,6 @@5.5 版本引入,10.2 版本完善 | 5.5-5.7/8.0 | 5.7/8.0 | -5.7/8.0 | 5.6/5.7/8.0 |
否 | 是 | 是 | -是 | 否 | 否 | 插件式,不支持 | 不支持 | 不支持 | -插件式,不支持 | 支持 | 支持 | @@ -982,7 +892,6 @@设定高低优先级,且低优先级事件等待一段时间可升为高优先级队列 | 设定高低优先级,且限制每个连接在高优先级队列中的票数 | 设定高低优先级,且限制每个连接在高优先级队列中的票数 | -设定高低优先级,且限制每个连接在高优先级队列中的票数 | 控制事务、非事务语句的比例 |
不支持 | 不支持 | 不支持 | -不支持 | 支持 | - | - | 不支持 | 不支持 | -不支持 | 支持 | - | @@ -1008,7 +915,6 @@- | 2 个状态变量 | 2 个状态变量 | -4 张状态信息表 | 27 个状态变量 | 8 个状态变量 | @@ -1017,7 +923,6 @@- | - | MariaDB | -Percona + MariaDB 10.2 及之后版本 | Percona | MariaDB 5.5 | @@ -1026,7 +931,6 @@Windows/Unix | Windows/Unix/MacOS | Windows/Unix | -Unix | - | - | @@ -1035,7 +939,6 @@