Fork me on GitHub

​美团关于 Apache Doris 存储层向量化改造的设计与实现

导读: 本文将为大家介绍 Apache Doris 存储层向量化的改造,最初的改造目的是提升 Doris 的查询性能,这个改造就是利用向量化的一些特性去做查询加速。

全文将围绕以下五个部分展开:

  • Apache Doris 引擎介绍
  • 向量化编程介绍
  • Apache Doris 存储层概览
  • Apache Doris 存储层向量化改造
  • 总结

分享嘉宾|王博 美团 OLAP开发工程师
编辑整理|张建闯 BOSS直聘
出品平台|DataFunSummit


01 Apache Doris 引擎介绍

图片

首先介绍一下 Apache Doris 在目前数据分析产品中的定位,主要是两个路径。

① 数据导入路径

比如上面的 Spark、Flink 以及一些实时的场景,可以支持实时导入,也可以支持左侧的离线导入,还有一些是下方的关系型数据库或二进制数据导入。慢的可能是分钟级,快的可能是秒级的数据导入,以及实时的端到端的数据同步,还有传统的 T+1 的批量生产方式,目前用户更青睐于数据的实时导入以及关系型数据库的二进制导入。

② 查询路径

主要就是报表分析,Doris 目前定位于 OLAP 数据库,主要针对分析场景,延迟一般在 3~5 秒以内,时间再长可能稳定性就难以保证了。

综上,目前 Apache Doris 的定位还是一个 MPP 架构,提供亚秒级查询的 OLAP 数据库,同时支持离线和实时数据导入,支持 SQL,相对 Flink 和 Spark 需要去配置一些定时的任务而言,开发成本还是非常低的。

02 向量化编程介绍

图片

第二部分来讲一下向量化编程是什么。

这张图来源于 CMU 大学,向量化编程是数据库里常见的一个操作,比如列的加减、比较、读写操作,可以把它们抽象成两个向量。例如图中 x 列和 y 列,进行累加,得出 z 的结果,这种计算方式就叫做单指令单数据。如果想算一个结果集,只能根据行去算,每一行都需要发送一次指令,这个就是原来 Doris 处理数据的方式,需要发送的指令会更多。

图片

新版的特点是可以批量算,之前是一行一行地去计算,现在新的算法可以对数据进行一批一批地去计算。

CPU 有一个 SIMD 寄存器,SIMD 就是单指令多数据,也就是一条指令可以算一批的数据,可以一次把这一个向量的数据批量加一,这个性能是比较快的,尤其对于数据分析的系统来说,对求 sum,min,max 非常合适,OLAP 数据库是偏吞吐和数据密集型的,对 SIMD 依赖是比较重的。

图片

概括一下,向量化编程主要有两个要点:

① 计算机能够提供 SIMD 指令和寄存器,硬件首先能支持;

② 对现有算法做些改造,代码逻辑可以利用 SIMD 指令做批量计算。

目前比较新的硬件都是可以支持比较全量的 SIMD 指令集。

03 Apache Doris 存储层概览

图片

存储层主要是把数据读上来,发送给执行节点,在这个过程中还做了一些更细粒度的拆分,比如磁盘上的数据加载到内存需要做的一些反序列化的操作,转成执行层的数据结构,有一个解码的过程。另外,这个数据要可以被算子执行。归并的意思是对数据做一定的排序,对数据做进一步的逻辑上的处理。物理算子主要就是对数据进行最后的计算,生成最终计算结果。

图片

写入对数据结构是有影响的,Doris 的写入会分多个批次,一次导入会写一个文件,持续的写入会生成多个文件,比如文件 1,2,3,4...n, 文件内容可能第一个文件 key=1,value=3,第二个文件 key=1,value=2,文件太多则需要压缩,文件 1 和文件 2 在 Compaction 压缩过程中是有逻辑的,比如表是主键更新的表,需要根据主键对值进行更新,这是数据写入的逻辑。因为 Compaction 不一定是很及时的,所以在查询的时候需要做一些排序和归并,这就是存储层除了做数据的读取、解压之外,还要做些归并的原因,归并就是因为部分表模型需要排序的逻辑。

图片

举一个查询的例子, 比如一个非常简单的查询 Select key,value from table,如果在磁盘上有两个文件,每个文件数据结构是 key 和 value 两列,做归并就是把 value 列的这两个值进行覆盖,这就是查询逻辑,Compaction 的逻辑非常类似,在数据归并完之后就交给物理算子进行计算了。

总的来说,数据的查询流程就是先去读盘,然后解码,再按照表模型去做归并,有些明细表是不需要归并的,主要就是这三个关键点。

04 Apache Doris 存储层向量化改造

Doris 存储层主要做了下面这两个工作:

① 数据的读取,比如谓词的下推优化

② 数据的输出,比如根据表模型做数据的归并

Doris 存储层向量化改造具体需要做下面这些事情:

① 对代码进行梳理,找出可向量化的代码逻辑,因为 SIMD 指令只能执行部分的逻辑,比如累加、比较、批量的读和写,它不是所有的逻辑都可以用 SIMD 的,只适用于数据密集的场景。

② 找出来之后,需要对这些模块做改造,比如对这些代码使用 SIMD 指令做替换。

③ 做 SIMD 改造的核心目的是希望提升性能,所以对于没有办法做向量化改造的逻辑需要去思考是否有其他方法做优化。

图片

上图中黄色的部分是待调研的,绿色的是已经完成的,我们这里主要看一下读取数据是如何优化的。

(1)有索引

Doris 支持一些索引结构,比如前缀索引,所以这里可以进行一些数据的裁剪,具体实现是采用 Bitmap,对数据裁剪完成之后剩下一批行号,有了行号就可以减少数据的 seek,读取的数据少了,就可以提升性能。这里考虑到以下几个问题:

① 索引本身是否有代价,比如索引本身的数据结构是否有优化空间。

② 读取数据的时候又可以进行一个分类,读取定长类型和变长类型的数据,这两个处理逻辑是不一样的。

  • 定长类型包括 int、float、double,可以使用 SIMD 指令做批量读
  • 变长类型一般是字符串类型,无法做 SIMD 优化,这种优化是尝试消除一些不必要的拷贝,另一个是把字符串映射成数值类型,这样就能使用 SIMD 优化了

③ 读取数据的另一个优化方向是,读取位置的优化。

  • 从 Cache 读,因为 Cache 也是一个数据结构,所以这里也会有可以优化的地方
  • 从磁盘读,磁盘读取是否能够做一些优化

(2)无索引

没有索引就需要读取所有的数据,所以这里就和有索引读取数据的问题一样了,这里可以考虑一个问题,就是是否也可以生成一个 Bitmap,事实上这里是没必要,因为读取 Bitmap 也是有一定的开销的。

这是一个思考的逻辑,从一个点开始一步步向下,逐步分析哪里可以做优化。

图片

对刚才的内容做一些细化,比如索引的代价,索引是可以提高性能的,可以减少数据的读取量,但是考虑具体实现的话, 我们使用 roaringBitmap 实现, 读取索引也是有一些开销的。

图片

另一个比较重要的点就是在处理数据的时候需要更关注定长和变长的数据类型,变长类型的读写开销是要大于定长类 型的,就是字符串要大于 int、float 这些定长类型,不光是读,比较操作也是一样的 ,所以我们针对变长类型尽量去转成整数来做。

图片

这就是刚才说的字典,Doris 存储层目前只有文件的字典,还没有全局的,文件的字典,我们暂时可以利用文件的字典做一些程度的优化,这个目前已经提到社区了。

图片

这里主要看一下读完数据之后,还是要做一些额外的优化,就是 谓词下推 ,比如把一些谓词下推到存储层来做,可以减少访问的数据量,这里主要是用延迟物化来做的,原来的逻辑是可以只读一次的,有了延迟物化之后,其实数据需要读取两次,这里有个问题就是延迟物化的代价,只读一次其实还是要做谓词计算,就又回到这两个变长类型和定长类型的问题了。

图片

这里来解释一下延迟物化到底是什么 ,这里没有列出官方的定义,而是从 Doris 已经实现的存储层优化来说的,以图片中的 SQL 为例:

  • 什么不是延迟物化:也就是原方式,就是只读一次,比如 SQL 中 a 列和 b 列的数据一次性全部读取出来。
  • 什么是延迟物化:就是先读 b 列,做谓词计算,获得行号,再读 a 列,逻辑上看来好像读取数据的量更少了,但事实上并不一定,目前 Doris 中是写死的,就是先读谓词,再读非谓词,实测并不是所有场景都很快。

图片

这里举几个 case,两个很极端的例子,但是能说明问题,就是假如这个表有 10 亿行数据,假设这个 b=1 只有一行,那么毫无疑问,第二次读取数据只需要读取一次,只需要 seek 一次,这个延迟物化一定是很快的;另一个情况就是如果 b=1 这个数据如果有 9 亿行,这就不一定了,因为延迟物化需要多 seek 一次,是有额外开销的,过滤可以抵消这个开销,**所以延迟物化可以应用的核心假设如下:**

  • 读数据的开销要大于 seek 数据的开销
  • 读数据开销如何衡量,变长类型要远大于定长类型的
  • seek 开销如何衡量,读数据会遍历行号,取出连续的部分,而延迟物化要读取两次,会多 seek
  • 谓词选择开销如何衡量,比如只seek一行就会很快

图片

以这个 SQL 为例:

select a from table where b=x

  • 如果条件 x 的选择性比较差,且很离散,也就是 seek 次数会比较多
  • 如果 a 和 b 列都是定长类型,seek 的开销会大于读开销,那么不做延迟物化会更优,即减少 seek 次数
  • 如果 a 和 b 列都是变长类型,读取数据的开销>seek开销,那么延迟物化可能更优

核心的关键点在于,我们需要基于谓词的选择性,以及运行时 SQL 的写法,算出一个代价,决定是否选择延迟物化。

图片

最后一部分就是数据输出如何优化,并不是所有的模型都需要做归并,比如明细模型,数据读出来之后就直接发到物理算子层了,不需要做归并,也不需要做聚合,另外就是主键和聚合模型,它们都要有归并的过程,这里有个问题就是使用的是标准库,是否有优化空间并不确定,需要后续研究。另外,聚合模型是否真的需要归并,因为目前是先排序,后聚合,再输出,那么是否可以不排序,直接聚合,然后再输出,是否有差异。还有,所有模型计算,会先攒一批的数据,再做批量的聚合,之前是来一行算一行,那样计算开销是更大的,相对而言 SIMD 是可以一次算一批的,这个是数据输出部分。

图片

目前由于是测试开发阶段,查询流程向量化基本上已经完成,目前主要在做稳定性的测试,所以目前能拿出来的测试主要是 SSB 的宽表性能测试,模型是明细表,不涉及归并的逻辑,这个测试可以分成两层看:

① 存储层性能提升是比较大的。

② SQL 也有所提升,不过这里还是有优化空间的,因为存储层性能提升了,而应用层反而好像还有一些性能下降,所以目前还是处于测试阶段,需要更多的线上环境打磨。

图片

小结一下向量化改造的工作:

① 我们期望和执行层程序做一个统一的数据结构,因为向量化改造是 Apache Doris对整体流程查询链路的改造,也就是会统一使用一个数据结构,所以要保证存储层和查询层的数据结构是对齐的,否则数据结构不一样会有一个转换的开销。

② 向量化改造其实是比较单纯的,比较容易想到哪些地方能够做向量化改造,比如谓词计算和数据拷贝,但是我们的第一版改造完成之后,发现性能并不是很符合预期,也就是向量化确实可以提升一部分性能,但是主要还是代码逻辑相关的优化。

③ 字典、延迟物化等其他的地方也需要考虑。

05 总结

图片

如何做性能优化?

1.了解你的代码,就是要了解你的代码在做什么事情,代码逻辑是根本,决定了执行逻辑和技术选型。

2.了解计算机的行为,要具体知道它到底在干什么,SIMD 本身也是计算机在发展过程中,提供的一种技术手段,这个技术是否能用于你的代码,还是取决于代码是否有这种逻辑,SIMD 也有自己的适用场景。

3.了解性能工具,可以帮助你做一些验证。

图片

这块功能目前正在做一些发板测试,欢迎有兴趣的同学来参与到我们社区,图中是GitHub 的地址和 Doris 公众号,以及开发者邮箱,欢迎大家来咨询和讨论。

图片

王博

美团 OLAP开发工程师

本科毕业后在百度外卖做数据报表开发;现在在美团做OLAP引擎开发,主要参与过Apache Doris的Spark Load开发以及向量化改造。


本文地址:https://www.6aiq.com/article/1666874991171
本文版权归作者和AIQ共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出