浅析OceanBase数据库的向量化执行引擎

devtools/2024/11/13 15:26:58/
本篇博客是偏数据库系统概念性的内容,不会深入到 OceanBase 中各个算子和表达式的在向量化中的详细设计和实现。

背景

为了提升OceanBase社区版用户解决问题的效率,OceanBase官方不久前推出了《OceanBase 从入门到实践》系列课程。在第七期直播课程后,我们收到了不少用户的提问,表示在查阅学习资料或笔记时,对其中提到的诸如“rowset=16”或“rowset=256”这样的信息存在理解上的困惑,不清楚这其具体含义。如下代码中的示例。这里的 rowset 信息和 OceanBase 执行引擎向量化执行技术相关,这篇博客就正好作为 AP 性能专题的内容之一,在解答这个用户问题的同时,也为大家介绍一下 OceanBase 执行引擎向量化执行技术。

obclient [test]> create table t1(c1 int, c2 int);
Query OK, 0 rows affected (0.203 sec)obclient [test]> explain select count(*) from t1 where c1 = 1000;
+------------------------------------------------------------------------------------+
| Query Plan                                                                         |
+------------------------------------------------------------------------------------+
| =================================================                                  |
| |ID|OPERATOR         |NAME|EST.ROWS|EST.TIME(us)|                                  |
| -------------------------------------------------                                  |
| |0 |SCALAR GROUP BY  |    |1       |4           |                                  |
| |1 |└─TABLE FULL SCAN|t1  |1       |4           |                                  |
| =================================================                                  |
| Outputs & filters:                                                                 |
| -------------------------------------                                              |
|   0 - output([T_FUN_COUNT_SUM(T_FUN_COUNT(*))]), filter(nil), rowset=16            |
|       group(nil), agg_func([T_FUN_COUNT_SUM(T_FUN_COUNT(*))])                      |
|   1 - output([T_FUN_COUNT(*)]), filter([t1.c1 = 1000]), rowset=16                  |
|       access([t1.c1]), partitions(p0)                                              |
|       is_index_back=false, is_global_index=false, filter_before_indexback[false],  |
|       range_key([t1.__pk_increment]), range(MIN ; MAX)always true                  |
+------------------------------------------------------------------------------------+
14 rows in set (0.033 sec)

执行引擎">火山模型执行引擎

向量化执行引擎是提升 AP 能力的利器之一,也为 2021 年 OceanBase TPCH 打榜夺冠做出了重要贡献。不过如果要学习向量化引擎,首先需要了解一下传统的数据库执行引擎模型 —— 火山模型。

火山模型(Volcano Model)也被称为迭代模型(Iterator Model),是最著名的查询执行模型,早在 1990 年就在论文《Volcano, an Extensible and Parallel Query Evaluation System》中被提出。大多数关系型数据库都采用了这种模型,例如 Oracle、MySQL、DB2、SQLServer 等等。

在火山模型中,一个查询计划会被分解为多个算子(Operator)。每个算子就是一个迭代器,都要实现一个 next() 接口,通常包括三个步骤:

    • 调用儿子算子的 next() 方法,获取儿子算子返回的计算结果;
    • 对儿子算子返回的计算结果,执行当前算子的计算动作,得到计算结果;
    • 返回一行计算结果给父亲算子。

说明:
   论文里算子的的 next() 接口,在 OceanBase 的代码里叫 ObOperator::get_next_row()。

通过火山模型,查询执行引擎可以优雅地将任意算子组装在一起,而不需要考虑每个算子的具体处理逻辑。查询执行时会由查询树自顶向下嵌套调用 get_next_row() ,数据则自底向上地被拉取处理。所以,这种处理方式也称为拉取执行模型(Pull Based)。为了更好地理解火山模型的拉取执行过程,这里继续看上面这个聚合计算的例子:

select count(*) from t1 where c1 = 1000;

说明:

图中的 Tuple 是一个元组,也就是下层算子向上层算子返回的一个计算结果行。

如上图所示:

    • 步骤 1 - 步骤 3,聚合算子 Aggregate 先通过调用 get_next_row() 方法触发下面这些算子逐层调用孩子算子的 get_next_row() 方法。
    • 步骤 4 - 步骤 6,TableScan 算子获取到存储层的数据,吐行给 Filter 算子,Filter 算子完成过滤条件 c1 = 1000 的计算,吐结果行给负责聚合计算的 Aggregate 算子。
    • 步骤 7,Aggregate 算子通过反复调用 next 方法获取到需要的一批数据,最终完成聚合计算,返回计算结果。

OceanBase向量化开关关掉,就可以看到类似于上图的执行计划树。

-- 关掉向量化开关,让后面的 SQL 默认走类似于火山模型的单行计算模式
alter system set _rowsets_enabled = false;-- 大家可以发现,在下面这个计划里,看不到类似于上面那个计划里的 rowset 值了
explain select count(*) from t1 where c1 = 1000;
+------------------------------------------------------------------------------------+
| Query Plan                                                                         |
+------------------------------------------------------------------------------------+
| =================================================                                  |
| |ID|OPERATOR         |NAME|EST.ROWS|EST.TIME(us)|                                  |
| -------------------------------------------------                                  |
| |0 |SCALAR GROUP BY  |    |1       |6           |                                  |
| |1 |└─TABLE FULL SCAN|t1  |1       |6           |                                  |
| =================================================                                  |
| Outputs & filters:                                                                 |
| -------------------------------------                                              |
|   0 - output([T_FUN_COUNT_SUM(T_FUN_COUNT(*))]), filter(nil)                       |
|       group(nil), agg_func([T_FUN_COUNT_SUM(T_FUN_COUNT(*))])                      |
|   1 - output([T_FUN_COUNT(*)]), filter([t1.c1 = 1000])                             |
|       access([t1.c1]), partitions(p0)                                              |
|       is_index_back=false, is_global_index=false, filter_before_indexback[false],  |
|       range_key([t1.__pk_increment]), range(MIN ; MAX)always true                  |
+------------------------------------------------------------------------------------+
14 rows in set (0.010 sec)

OceanBase 的计划只有两个算子,比上面这张图上的看上去更简单些。因为 OceanBase 中的每一个算子都包含了 Filter 的功能,所以不需要独立的 Filter 算子。可以在上面的计划里看到,TABLE SCAN 算子里也有 filter([t1.c1 = 1000]) 的信息。计划里的 SCALAR GROUP BY 算子就是负责不需要进行 group by 分组的聚合计算,对应图中的 Aggregate 算子。

火山模型的优点是处理逻辑清晰,每个算子只要关心自己的处理逻辑即可,耦合性低。但是它的缺点也非常明显,主要是两点:

    • get_next_row() 虚函数在每个算子处理每行数据时都要调用一次,调用次数过多,造成 CPU 资源的浪费。在 OLAP 查询数据量大的场景中,问题尤为明显。
    • 数据以行为单位进行处理,不利于充分发挥现代 CPU 的特性。

向量化执行引擎及其优势">向量化执行引擎及其优势

向量化模型最早是在 MonetDB-X100(Vectorwise)系统的论文 《MonetDB/X100: Hyper-Pipelining Query Execution》中提出的,不同于传统的火山模型按行迭代的方式,向量化引擎采用按批迭代方式,可以在算子间一次传递一批数据。向量化引擎被提出后,因为可以有效提高 CPU 利用率并充分发挥现代 CPU 的特性,很快就被广泛应用到了现代数据库引擎设计中。 

如上图所示,与数据库传统的火山模型迭代类似,向量化模型也是通过 PULL 模式从算子树的根节点层层拉取数据。区别于 get_next_row() 调用一次传递一行数据,向量化引擎通过 get_next_batch() 一次传递一批数据,并尽量保证该批数据在内存上紧凑排列。

减少虚函数调用的开销

向量化引擎第一个显而易见的优势,就是大幅减少了框架函数的调用次数。假设一张表有 1 亿行数据,按火山模型的处理方式,每个算子均需要执行 1 亿次 get_next_row() 的迭代才能完成查询。如果使用向量化引擎,假设一个向量的大小为 1024 行数据,那么在同样的查询中,get_next_batch() 的调用次数会小于 10 万次( 1 亿 / 1024 = 97657 ),大大降低了虚函数调用次数,节省了 CPU 的开销。

在本文开头的用户问题里,计划中的 rowset 就是表示一个 batch(或者叫一个向量)中有多少行数据。

-- 打开向量化开关
alter system set _rowsets_enabled = true;-- 设置每个向量的大小是 16 行
alter system set _rowsets_max_rows = 16;-- 计划中可以看到执行时的向量值大小(rowset=16)
explain select count(*) from t1 where c1 = 1000;
+------------------------------------------------------------------------------------+
| Query Plan                                                                         |
+------------------------------------------------------------------------------------+
| =================================================                                  |
| |ID|OPERATOR         |NAME|EST.ROWS|EST.TIME(us)|                                  |
| -------------------------------------------------                                  |
| |0 |SCALAR GROUP BY  |    |1       |4           |                                  |
| |1 |└─TABLE FULL SCAN|t1  |1       |4           |                                  |
| =================================================                                  |
| Outputs & filters:                                                                 |
| -------------------------------------                                              |
|   0 - output([T_FUN_COUNT_SUM(T_FUN_COUNT(*))]), filter(nil), rowset=16            |
|       group(nil), agg_func([T_FUN_COUNT_SUM(T_FUN_COUNT(*))])                      |
|   1 - output([T_FUN_COUNT(*)]), filter([t1.c1 = 1000]), rowset=16                  |
|       access([t1.c1]), partitions(p0)                                              |
|       is_index_back=false, is_global_index=false, filter_before_indexback[false],  |
|       range_key([t1.__pk_increment]), range(MIN ; MAX)always true                  |
+------------------------------------------------------------------------------------+
14 rows in set (0.021 sec)

充分发挥现代 CPU 的特性

数据紧密排列,提升 Cache 友好性

OceanBase向量化执行过程中,会尽量保证该批数据在内存上紧凑排列,产生的中间数据会以列的形式进行组织。比如我们一个 batch 是 256 行数据, 那我们内存排布中 c1 的 256 行数据就会都连续存放, 然后 c2 的 256 行数据也连续存放, 计算 concat(c1, c2) 表达式还是 256 行一起计算,最后将结果放入到该表达式预分配的结果值的内存空间中。

由于中间数据连续,CPU 就可以通过预取指令快速把即将进行计算的数据提前加载到 L2 cache 中,以此减少 memory stall 的现象,提升 CPU 的利用率。在算子函数内部,函数也不再是一次处理一行数据,而通过批量处理连续数据的方式进行计算,可以提升 CPU DCache 和 ICache 的友好性,减少 Cache Miss。

减少分支预测失败对 CPU 流水线的破坏

论文《DBMSs On A Modern Processor: Where Does Time Go?》中介绍了分支预测失败对数据库性能的影响。由于 CPU 中断了流水执行,重新刷新流水线,因此分支预测失败对数据库处理性能的影响很大。SIGMOD13 的论文《Micro Adaptivity in Vectorwise》也对分支在不同选择率下的执行效率有详细论述,见下图。

由于数据库 SQL 引擎逻辑比较复杂,在火山模型下,条件判断逻辑出现频率普遍较高。

// 单行计算流程的伪代码,处理 256 行数据,要进行 256 次 if 的判断:
for (auto row_no : 256) {get_next_row() {if (A) {eval_func_A();} else if (B) {eval_func_B();}}
}

但在向量化执行时,可以在算子和表达式内部,最大限度地避免条件判断。例如避免在 for 循环内部出现 if 判断,从而避免分支预测失败对 CPU 流水线的破坏,大幅提升 CPU 的处理能力。

// 向量化计算流程伪代码,处理 256 行数据,只需要进行 1 次 if 的判断:
get_next_batch() {if (A) {for (auto row_no : 256) {eval_func_A();}} else if (B) {for (auto row_no : 256) {eval_func_B();}}
}

利用 SIMD 指令加速计算

由于向量化引擎处理内存连续数据,因此向量引擎可以很方便的把一批数据装载到向量寄存器中。然后通过 SIMD 指令,替换传统的标量(Scalar)算法,进行向量(Vector)计算。SIMD 指令可以使 CPU 同时对一批数据并行执行相同的计算动作,从而减少这批数据计算所需要的 CPU 的周期数。

上图右侧展示了典型的 SIMD 计算,两组四个连续存储的数据元素被并行计算,CPU 会根据 SIMD 指令,对每对相应的数据元素(A1 和 B1,A2 和 B2,A3 和 B3,A4 和 B4)同时执行相同的运算。四个并行计算的结果也会被连续存储。

假设我们的处理器支持 4 个元素的 SIMD 乘法操作,那就意味着它有特殊的寄存器(通常称为向量寄存器),可以同时存储四个整型数。因为 OceanBase向量化执行过程中数据会紧密存储,所以就可以按照如下流程编写 SIMD 代码:

  • 数据加载(_mm_loadu_si128):首先,我们将向量 A1 A2 A3 A4 和 B1 B2 B3 B4 的各个元素加载到两个 SIMD 寄存器中。
  • 单一指令乘法(_mm_mullo_epi32):接着,我们使用单一的 SIMD 乘法指令,同时对两个寄存器中的所有元素执行乘法操作。
  • 数据存储(_mm_storeu_si128):最后,我们将结果从 SIMD 寄存器存储回为结果分配的内存中,得到结果向量。
// SSE(针对x86架构)的 C++ 伪代码示例,使用 SIMD 技术进行整型向量的逐元素相乘
#include <immintrin.h> // 包含 SSE 指令集头文件// 函数用于计算两个整型向量的逐元素相乘
void simdIntVectorMultiply(const int* vec1, const int* vec2, int* result, size_t length) {// 确保向量长度是 4 的倍数,因为 SSE 寄存器一次处理 4 个 32 位整数assert(length % 4 == 0);// 使用 SSE 指令进行优化的循环for (size_t i = 0; i < length; i += 4) {// 加载四个整数到 xmm 寄存器(128位)__m128i vec1_simd = _mm_loadu_si128(reinterpret_cast<const __m128i*>(vec1 + i));__m128i vec2_simd = _mm_loadu_si128(reinterpret_cast<const __m128i*>(vec2 + i));// 执行向量乘法__m128i product_simd = _mm_mullo_epi32(vec1_simd, vec2_simd);// 存储结果回内存_mm_storeu_si128(reinterpret_cast<__m128i*>(result + i), product_simd);}
}

TPCH 性能测试

利用 TPCH 30T 数据集对 OceanBase 数据库进行 TPCH 测试,向量化执行模式相比单行执行模式,执行性能整体可以提升 2.48 倍。对于计算密集的 Q1 查询, 性能提升可以达到 10 倍以上。

在最新的 4.3 版本中,OceanBase 还对从 3.x 版本就已经支持的向量化执行引擎,进行了一次新的优化和重构,详细介绍和性能测试可以参考:这篇博客。

结语

这篇博客是 OceanBase 向量化执行引擎入门级的介绍,希望无论是 DBA 同学,还是内核研发同学,在阅读之后都能够有所收获。如果大家希望继续和作者讨论有关向量化执行的任何问题,都欢迎在这篇博客的评论区进行留言,我会第一时间对大家的问题进行回复。


http://www.ppmy.cn/devtools/114947.html

相关文章

visual studio2015安装番茄助手

VS2015安装使用番茄助手Visual Assist_vs2015番茄助手-CSDN博客 【VS和番茄助手的安装步骤】https://www.bilibili.com/video/BV1K24y1n7Xk?vd_sourcefad0750b8c666dbeaf016e547f99a602 【番茄助手VS2019安装】https://www.bilibili.com/video/BV13p4y1v7bG?vd_sourcefad0…

在Flask中实现日志记录

在Flask中实现日志记录是一个关键的功能&#xff0c;它有助于监控应用的运行情况、调试问题以及记录重要的运行信息。以下是在Flask中实现日志记录的详细步骤和最佳实践&#xff1a; 一、使用Python内置的logging模块 Flask应用通常会使用Python的logging模块来进行日志记录。…

NCNN 学习(2)-Mat

Mat 是 NCNN 中最重要数据结构之一&#xff0c;NCNN 的很多计算都会涉及到 Mat。 1 数据成员 Mat 的定义在 https://github.com/Tencent/ncnn/blob/master/src/mat.h。从代码中可以看到&#xff0c;Mat 有这样的几个主要数据成员&#xff1a; class NCNN_EXPORT Mat { publi…

JAVA毕业设计176—基于Java+Springboot+vue3的交通旅游订票管理系统(源代码+数据库)

毕设所有选题&#xff1a; https://blog.csdn.net/2303_76227485/article/details/131104075 基于JavaSpringbootvue3的交通旅游订票管理系统(源代码数据库)176 一、系统介绍 本项目前后端分离(可以改为ssm版本)&#xff0c;分为用户、管理员两种角色 1、用户&#xff1a; …

青柠视频云——如何开启HTTPS服务?

前言 由于青柠视频云的语音对讲会使用到HTTPS服务&#xff0c;这里我们说一下如何申请证书以及如何在实战中部署并且配置使用。 一、证书申请 1、进入控制台 我们拿阿里云的免费个人证书为例&#xff0c;首先登录阿里云&#xff0c;在控制台找到数字证书管理服务&#xff0c;进…

【有啥问啥】OpenAI o1的思考之前训练扩展定律、后训练扩展定律与推理扩展定律:原理与应用详解

OpenAI o1的思考之前训练扩展定律、后训练扩展定律与推理扩展定律&#xff1a;原理与应用详解 随着深度学习技术的不断发展&#xff0c;模型的规模和复杂度也迅速提升。研究人员发现了模型训练和推理过程中性能变化的规律&#xff0c;这些规律为我们提供了优化模型设计与训练的…

深度学习基本概念详解

一、什么是深度学习&#xff1f; 近年来&#xff0c;深度学习&#xff08;Deep Learning&#xff09; 作为人工智能领域的一个重要分支&#xff0c;取得了突飞猛进的发展。它通过模拟人脑神经网络的结构和功能&#xff0c;使用多层次的人工神经网络模型&#xff0c;从大量数据…

本地生活商城开发搭建 同城O2O线上线下推广

同城本地化商城目前如火如荼&#xff0c;不少朋友咨询本地生活同城平台怎么开发&#xff0c;今天商淘云与大家分享同城O2O线上商城的设计和开发。 本地生活商城一般会涉及到区域以及频道类&#xff0c;一般下单需要支持用户定位、商家定位&#xff0c;这样利于用户可以快速找到…