在“国产数据库硬核技术沙龙-TDSQL-A技术揭秘”系列分享中,5位腾讯云技术大咖分别从整体技术架构、列式存储及相关执行优化、集群数据交互总线、Fragment执行框架/查询分片策略/子查询框架以及向量化执行引擎等多方面对TDSQL-A进行了深入解读。没有观看直播的小伙伴,可要认真做笔记啦!今天带来本系列分享中最后一篇腾讯云数据库高级工程师胡翔老师主题为“TDSQL-A向量化执行引擎技术揭秘”的分享的文字版。
作为领先的分析型数据库,TDSQL-A是腾讯首款分布式分析型数据库,采用全并行无共享架构,具有自研列式存储引擎,支持行列混合存储,适应于海量OLAP关联分析查询场景。它能够支持2000台物理服务器以上的集群规模,存储容量能达到单数据库实例百P级。
一、TDSQL-A向量化执行引擎
1.1 背景
要优化数据库的查询执行效率,就要充分地利用CPU、缓存等资源。但在现实中,硬件发展带来的能力提升并没有在实际应用中得到体现。右图统计了不同的数据库操作的CPU利用率,可以看到像seq scan、index scan这些基本的数据库操作,实际上并没有有效地利用好CPU,利用率还是很低。根本原因在于没有按照最高效使用CPU的方式来设计和实现实际的应用系统。所以我们必须了解当代CPU的主要特征。
当前CPU主要具有以下五个特征:
●流水线。可以允许一个指令周期内执行多个命令,具有多个核心的CPU还支持超标量流水线,允许并发执行多个流水线,进一步提高CPU的计算能力。
●乱序执行。可以允许不具有依赖关系的指令并发执行,避免因为等待某个指令而阻塞运行。
●分支预测。CPU会对分支进行预测并根据预测选取下一步执行的路径,提前加载指令和数据,但如果分支预测失败,也会导致流水线中断。
●分层存储。CPU周围设置了寄存器、L1/L2/L3缓存、内存和磁盘等多级存储,数据越靠近CPU,计算速度越快,反之,如果频繁地从内存或者磁盘读取数据,会导致CPU把较多的时间浪费到IO上,计算效率减低。
●SIMD等新硬件能力。SIMD即单指令多数据流,一次操作完成多组操作数的计算,可以进一步提高计算效率。像SIMD等新硬件提供了更强的执行能力。
我们针对CPU的这些特征,提出了几个数据库查询性能的优化方向:
首先,可以通过向量化批量计算提高CPU流水线和乱序执行的执行效率;其次,编写CPU计算友好的程序,比如通过减少上下依赖、减少分支、预取数据到缓存等方式,让CPU集中于计算任务;最后,还可以通过SIMD来对计算密集型的简单程序进行改造,加速计算效率。
1.2 向量化计算
顾名思义,向量化计算就是按照向量的方式计算,也就是一次计算多对操作数。
按照实现方式的不同,向量化主要分为以下三种类型:
●自动向量化。编译器可以识别出循环内哪些操作可以写成向量化执行的方式,但要求必须是简单的循环,不能包含条件、跳转等语句,不能包含前后依赖关系,硬件也必须要支持SIMD指令。
●添加编译器提示。通过使用一些关键字或者预编译指令,强制进行向量化。
●显式向量化。通过CPU提供的SIMD指令集来手工编写向量化执行的代码。
三种方式中,第一种是最为简单也是应用最广泛的方式,只需要遵循一定的代码编写规则即可,不会影响原来代码的逻辑性和可读性,性能加速效果也不错。而另外两种方式对编程人员的要求很高,需要结合编译器和硬件能力来做深度优化。
1.3 列存储
这里再次介绍一下列存储,因为列存储跟向量化密切相关,向量化计算就是基于列存储来构建。
我们先来了解下数据库存储。数据库存储主要分为两类:行存储和列存储。
行存储中,每一行的元组的每一列实际上是连续存储的,这样的优点是易于添加或者修改一个元组,但在读取数据时可能会额外读到不需要的列,比较适合于包含大量高并发增删改查事务的OLTP场景。
列存储中,每一列是单独存储的,这样就可以只读取需要的列,但缺点是元组的写入需要操作多个文件,比较适合于包含大数据量读取和复杂计算的OLAP场景。
采用列存储的好处有很多。除了上面提到的数据裁剪能力之外,列存储还可以通过压缩算法带来更高的压缩比,也可以通过字典里列排序或者稀疏索引加速数据的查找效率。另外,这种列式的存储组织形式还为上层计算的性能优化提供了很大的便利,特别是在向量化查询和延迟物化等方面。
1.4 向量化查询执行引擎
这部分主要介绍的是,如何结合前面提到的向量化和列存储技术,来对查询执行引擎进行向量化加速计算。
传统查询执行引擎采用火山模型,按照一次处理一个元组的方式,逻辑非常简单,便于开发实现,但是效率比较低,主要原因有以下三点:
首先,CPU把大部分时间都花在遍历查询操作树上,而不是在真正处理数据。一次处理一个Tuple的处理速度可能非常快,但是处理完之后就需要调用下层算子获取下一个tuple,这就导致函数调用的次数比较多,这样就进而会浪费掉CPU的很多时间。其次,数据和指令的缓存命中率低。频繁的函数调用导致寄存器需要保存更多的信息,而且实现时可能会为了通用性的考虑,对接口进行封装,这就会导致复杂度的提升,执行越复杂就会导致缓存利用率越低。最后,这种传统的方式无法利用新硬件提供的SIMD能力进行进一步优化。
与之相比,向量化查询执行引擎仍然采用火山模型,但是按照一次处理一组元组的方式,实现批量读取和批量处理,大大减少了函数调用开销,CPU可以把更多的时间集中到实际的计算上,效率会更高。
按照向量化计算的方式,对一组数据做简单的循环计算,同时数据按照列组织形式表示成列向量,每个列向量对应的一整块连续数据,进而可以批量读入缓存以及进行批量处理,这就可以大大提高指令、数据的缓存命中率,进而提高CPU的执行效率。
以上图中的例子为例。这是一个带where子句的聚合运算语句,左边是行存储非向量化查询执行过程,右边是列存储向量化查询执行过程。基于列存储,我们只需要获取id和agg列的数据。基于向量化查询执行引擎,每层算子获取的都是表示成列向量的一组元组,并对每个列向量进行批量计算。
1.5 向量化执行实例
下面通过一个聚合计算的例子来进一步介绍向量化执行的具体步骤。
这个例子使用两个列进行分组,并对每个组内进行count(*)计算。整个流程包含两个步骤:一是构建hash table并在每个hash entry上计算聚合结果;二是遍历hash table,计算最终的聚合结果。
向量化改造之后,一些具体步骤可以通过简单的循环来进行批量处理。
首先,根据输入的向量在分组列上批量计算Hash值;其次,根据上一步计算的Hash值批量获取Hash bucket值;然后,批量处理输入向量内的每个元组,在Hash table内查找匹配的Hash entry或者创建新的Hash entry,如果发生哈希冲突,按照Open addressing的处理方式,继续对下一个位置进行匹配处理;接着根据上一步获取的对应每个输入向量的Hash entry,批量计算Agg结果并更新到对应的Hash entry上(count++);最后,扫描Hash table的每一个Hash entry,计算最终的Agg结果(count计算不需要此步骤),将结果组织成向量形式返回给上层算子。
1.6 向量化执行效果
接下来看一下向量化执行的效果。下面给出了一些测试用例,主要包含多种不同类型的Agg和Join场景,涵盖了定长和变长列。
蓝色是行存,橙色是原列存,灰色是列存向量化。测试了1G/10G/100G的结果,可以看出列存向量化的执行时间最短。数据量越大,原列存和列存向量化效果越明显。最好的情况下,列存向量化运行时间是原列存的1/2,列存向量化运行时间是行存的1/8。
1.7 下一步计划
最后介绍关于向量化的下一步计划,主要有以下四方面:
●Just-in-Time编译优化。对函数调用进行展开,减少函数调用,比较适合于复杂的表达式或者算子计算。
●SIMD指令加速。适合于简单的线性计算,可以利用现代CPU的SSE、AVX指令让一条指令实现512bit数据计算。
●Hash Agg/Hash Join等算法进一步向量化优化。
●列存扫描等存储相关部分进一步优化。
本文由博客一文多发平台 OpenWrite 发布!