如何使用PolarDB

64次阅读
没有评论

共计 3414 个字符,预计需要花费 9 分钟才能阅读完成。

这篇文章将为大家详细讲解有关如何使用 PolarDB- X 向量化引擎,文章内容质量较高,因此丸趣 TV 小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。

介绍

PolarDB- X 是阿里巴巴自研的云原生分布式数据库,采用了计算 - 存储分离的架构,其中计算节点承担着大量的表达式计算任务。这些表达式计算涉及到 SQL 执行的各个环节,对性能有着重要的影响。为此 PolarDB- X 引入向量化执行引擎,为表达式计算带来了几十倍的性能提升。

传统数据库执行器的缺陷

现代数据库系统的执行引擎,大多采用一次计算一行数据(Tuple-at-a-time)的处理方式,并且需要在运行时对数据类型进行解析和判断,来适应复杂的表达式结构。我们称之为“标量(scalar)表达式”。这种方式虽然易于实现、结构清晰,但是当需要处理的数据量增大时,它具有显著的缺陷:

为了适应复杂的表达式结构,计算一条表达式往往需要引入大量的指令;对于行式执行来说,处理单条数据需要算子树重新进行指令解释(instruction interpretation),从而带来了大量的指令解释开销。据论文《MonetDB/X100: Hyper-Pipelining Query Execution》统计,在 MySQL 执行 TPC- H 测试集的 Query1 时,指令解释就耗费了 90% 的执行时间。

此外,在最初的 Volcano 结构设计中,算子内部逻辑并没有避免分支预测(branch prediction)。错误的分支预测需要 CPU 终止当前的流水线,将 ELSE 语句中的指令重新载入,我们将这一过程称为 pipeline flush 或 pipeline break。频繁的分支预测错误会严重影响数据库的执行性能。

向量化执行系统

数据库向量化执行系统最早由论文《MonetDB/X100: Hyper-Pipelining Query Execution》提出,它有以下几个要点:

采用 vector-at-a-time 的执行模式,即以向量(vector)为数据组织单位。

使用向量化原语(vectorization primitives)来作为向量化算子的基本单位,从而构建整个向量化执行系统。原语中避免产生分支预测。

使用 code generation(代码生成)技术来解决静态类型带来的 code explosion(代码爆炸)问题。

向量化引擎为 PolarDB- X 的表达式计算带来了显著的性能提升。在下图中,横轴为向量大小,纵轴为吞吐量,不同标量表达式和向量化表达式的性能测试对比结果如下:

case 表达式性能测试对比结果如下:

整体流程

PolarDB- X 中,向量化表达式的执行分为以下几个阶段:

用户 SQL 经解析后,在 validator 中进行校验,推导和修正表达式的类型信息;这一阶段为向量化运算提供正确的、静态的类型信息;

在优化器形成执行计划之后,需要对表达式树进行表达式绑定,实例化对应的向量化原语,同时分配好向量下标,供运行时内存分配;

执行阶段,依据 Volcano 式的结构,自顶向下的触发执行向量化原语,并将向量作为运行时数据结构。

运行时结构

数据结构

在 PolarDB- X 向量化执行系统中,采用以下的数据结构来存放数据:

向量化表达式执行时,所有的数据都会存放在 batch 这一数据结构中。batch 由许多向量(vector)和一个 selection 数组而组成。其中,向量 vector 包括一个存储特定类型的数值列表(values)和一个标识 null 值位置的 null 数组组成,它们在内存中都是连续存储的。null 数组中的 bit 位以 0 和 1 来区分数值列表中的某个位置是否为空值。

我们可以用 vector(type, index) 来标识 batch 中一个向量。每个向量有其特定的下标位置 (index),来表示向量在 batch 中的顺序;类型信息(type)来指定向量的类型。在进行向量化表达式求值之前,我们需要遍历整个表达式树,根据每个表达式的操作数和返回值来分配好下标位置,最后根据下标位置统一为向量分配内存。

延迟物化

selection 数组的设计体现了延迟物化的思想,参考论文《Materialization Strategies in a Column-Oriented DBMS》。所谓延迟物化,就是尽可能地将物化(matrialization)这一过程后推,减少内存访问带来的开销。在执行表达式计算时,往往会先经过 Filter 表达式过滤一部分数据,再对过滤后的数据执行求值处理;每次过滤都会影响到 batch 中所有的向量。以上图中的 batch 为例,如果我们针对第 0 个向量设置 vector(int, 0) != 1 这一过滤条件,假设 vector(int, 0) 中有 90% 的数据满足该过滤条件(选择率 selectivity = 0.9),那么我们需要将 batch 中所有向量 90% 的数据重新物化到另一块内存中。而如果我们只记录满足该过滤条件的位置,存入 selection 数组,我们就可以避免这一物化过程。相应的,以后每次向量化求值过程中,都需要参考此 selection 数组。

向量化原语

向量化原语是向量化执行系统中的执行单位,它最大程度限制了执行期间的自由度。原语不用关注上下文信息,也不用在运行时进行类型解析和函数调用,只需要关注传入的向量即可。它是类型特定(Type-Specific)的,即一类原语只能处理特定类型。

向量化原语的主体是 Tight-Loop 的代码结构。在一个循环体内部,只需要进行取值和运算即可,没有任何的分支运算和函数调用。一个简单的向量化原语结构如下所示:

map_plus_double_col_double_col(int n,double*__restrict__ res,double*__restrict__ vector1, double*__restrict__ vector2,int*__restrict__ selection)
{ if (selection) {for(int j=0;j  j++) {int i = selection[j];
 res[i] = vector1[i] + vector2[i];
 } 
 } else {for(int i=0;i  i++)
 res[i] = vector1[i] + vector2[i];
 } 
}

注:* 左右滑动阅览

其运算过程利用了 selection 数组,逐步对向量进行取值、运算和存值,如下图所示:

向量化原语带来了以下优点:

Type-Specific 以及 Tight-Loop 的结构,大大减少了指令解释的开销;

避免分支预测失败和虚函数调用对 CPU 流水线的干扰,同时也能有利于 loop pipeline 优化【论文引用】

从向量中存取数据,有利于触发 cache prefetch,减少 cache miss 带来的开销。

我们为各种标量化表达式提供相应的原语实现,从而完成从标量到向量化的转变。例如将加法运算 plus(Object, Object) 针对不同操作数类型生成原语,包括 plus(double,double),plus(long, long) 等。

短路求值

在向量化原语的基础上,我们可以进一步对分支运算(也称为控制流运算 Control-Flow)进行短路求值(short-circuit calculation)优化,提升表达式计算的性能。

例如,case 表达式由 n 个 when 表达式、n- 1 个 then 表达式、1 个 else 表达式构成。对于表达式

select case when a   1 then a * 2 when b   1 then b * 2else a * b

具有以下树形结构:

由于标量化表达式按照 volcano 结构编排,并提供了统一的 next() 的接口,case 表达式必须执行完所有的子表达式 a 1,a*2,b 1,b* 2 和 a * b 之后,将全部结果汇总到一起,最后做 case 语义处理。这种执行方式不能根据 when 表达式的处理结果及时终止计算过程,而是对全部子表达式无差别执行。

引入向量化执行器以后,我们可以设计短路求值来优化此问题,每一个子表达式需要被提供合适的 selection 数组,从而正确选择列中合适的位置来进行向量运算。

关于如何使用 PolarDB- X 向量化引擎就分享到这里了,希望以上内容可以对大家有一定的帮助,可以学到更多知识。如果觉得文章不错,可以把它分享出去让更多的人看到。

正文完
 
丸趣
版权声明:本站原创文章,由 丸趣 2023-08-04发表,共计3414字。
转载说明:除特殊说明外本站除技术相关以外文章皆由网络搜集发布,转载请注明出处。
评论(没有评论)