大规模数据分析提效|行列混存格式下的读链路优化
引言
在大数据领域,行列混存是主要的存储格式。相较于行存或列存,行列混存在兼顾点查性能的同时,能够通过列裁剪来减少 I/O 和数据解码等开销,提升扫描性能。同时行列混存支持高效的压缩算法,通过将同一列的数据存储在一起提高局部数据的相似性,从而实现更高的压缩比。Relyt AI-ready Data Cloud 实现了类 Parquet 的行列混存格式,同时结合客户需求做了大量的优化工作。
今天,我们邀请质变科技AI-ready 数据云团队布道师北辰为大家分享话题《提升数据处理效率:行列混存格式下的读链路优化》,从数据裁剪和数据编解码两个方面,为大家介绍 Relyt 在读链路上所做的优化工作。
数据裁剪
提升查询链路性能,最直接的做法是减少扫描的数据量,而减少扫描数据量在查询执行的不同阶段有不同的优化手段。在有 ClusterKey 的场景下,Relyt 支持从 MetaService 获取文件列表时就将 ClusterKey 相关 Filter 带入,根据文件级别 ClusterKey 的 Min/Max 信息过滤掉不需要的文件。对于必须读取后才能进行过滤的数据,Relyt 会将部分 Filter 下推为 MetaFilter,在 FileReader 中从 Block 级别和 Page 级别进行粗糙级过滤;而对于延迟物化以及类似包含 DeleteVector 的场景,可以将 Selectivity 下推到解码链路;通过以上这些数据裁剪方式,Relyt 尽可能减少 I/O 和数据解码开销,提升扫描性能。
以下主要从 Block 裁剪、Page 裁剪和 Selectivity 下推三个方面进行介绍。
Block 裁剪
在一个行列混存文件中,Footer 中的文件 Meta 存储了 Block 级别的列级统计信息,主要包含 Min/Max、NullCount 等,其格式如下:
编辑
打开文件时,Relyt 首先会从文件 Footer 中获取 Block 级别列的统计信息,结合下推的 MetaFilter 进行表达式计算,得到每一列各自命中的 BlockIndex 。由于各列的数据在 Block 级别按行对齐,在计算最终命中的 Block 时只需要将各列命中的 BlockIndex 结果根据 Filter 逻辑关系取交并差即可,这给 Block 裁剪提供了极大的便利。以下述查询为例:
select * from test where id < 100 and dt between '2024-12-01' and '2024-12-04';
假设一个文件中一共有 N(N>4)个 Block,其中命中第 1、3 个 Block , 条件命中第 2、3 个 Block :
编辑
由于两个查询条件是 And 关系,所以最终命中第 3 个 Block,此时整个文件只需读一个 Block,极大减少了 IO。
此外,Relyt 针对文件 Footer 部分元数据做了轻量级 Cache,进一步减少打开文件的开销。
Page 裁剪
考虑到写入性能及数据压缩等多方面影响,Block 通常不会设置得很小(一般 64MB ~ 256MB),导致 Block 裁剪效果有限,不能充分减少 I/O 和不必要的数据解码。因此,Relyt 在 Block 裁剪的基础之上,利用 Page 级别统计信息实现了更细粒度的裁剪。
Relyt 中的 Page 是一个 Block 内的 ColumnChunk 按大小进一步切分的产物,Page 级别的统计信息存储在文件 Footer 与数据区之间的一块独立区域,统计信息除包含 Min/Max 等信息外,还记录了当前 Page 中第一行数据在 Block 内的行号、数据在文件中的偏移和长度等信息。各列间的 Page 由于无法保证按行对齐,使得裁剪过程更复杂。当利用 Page 级别统计信息进行过滤时,首先计算出各列命中的 Page,然后根据列间 Filter 逻辑关系计算出多列命中的公共行集,随后根据命中的行集得到各列命中的 Page,最后各列在命中的 Page 内根据行集读具体数据。以下面的查询为例:
select * from test where a = 'x' and b = 'y'
读 Block 时,条件命中第 1、3 个 Page , 命中第 0、2 个 Page。如下图所示,可以知道 Column a 命中的行区间为 [1000, 2200) 和 [3000,3990),Column b 命中的行区间为 [0,500) 和 [1500, 3000)。因此,命中的行区间为 [1500, 2200)。根据 [1500, 2200) 及各列的 Page 统计信息,可知该行号区间对应 Column a 第 1 个 Page 的后半部分,Column b 第 2 个 Page 的前半部分。Relyt 中 Page 是最小的解压单位,所以对于 Column a 和 Column b,对应 Page 都需要从 I/O 层完整加载上来,然后根据命中的行区间,各自完成 Page 内“掐头去尾”的工作,并且如果 Page 内多个命中的行集间存在空洞,还需要跳过这些空洞,将列间的数据按行对齐后输出。
编辑
在这个过程中,Relyt 针对 I/O 操作也做了大量优化,除了常见的 I/O 合并外,还实现了基于固定 Buffer 空间的环形滑动窗口以加载数据,减少了 I/O 操作时的拷贝开销。
Selectivity 下推
数据自身的删除和更新也会导致读过程中需要根据 DeleteVector 过滤掉不可见的数据。此外在延迟物化中,需要将部分列数据先行加载进行计算,然后根据计算命中行读取其他列数据。Relyt 在这些场景读数据时,会将数据的 Selectivity 下推到 TableScan,根据 Selectivity 信息裁剪 Block,并根据命中率等信息决定是否在 Block 内将 Selectivity 转换为命中行集走 Page 裁剪链路。在同一个 Page 内,Selectivity 还可直接应用于数据解码链路,在解码时直接跳过相关数据,减少数据解码开销。
数据编解码
提升读数据性能的下一步是优化数据编解码和压缩,Relyt 在这方面也做了大量优化工作。下面主要从数据解码、字典读优化、自适应编码三个方面展开。
解码优化
Relyt 采用 SIMD 指令减少解码和转换开销,同时 Relyt 在解码链路上采用了更精确的 BufferSize 估算方法,减少数据解码链路上 Buffer 反复扩缩带来的拷贝开销;部分链路热点采用减少分支判断、循环手动展开等方式,进一步优化数据解码性能。
字典读优化
字典编码是一种常见的数据编码方式,合理地使用字典编码可以有效降低存储空间,提高查询效率。Relyt 在读数据时,会根据数据类型及字典编码相关信息,动态决定是否将某列直接以字典编码的形式加载到内存。
业界通常做法:若某一列采用字典编码且读时也需按字典方式出数据,此时数据解码过程中会重新构建内存字典。由于字典编码是 ColumnChunk 级别,对于开启字典编码的列,写入过程中若发现数据不适合使用字典编码,则会将已经写入的数据按字典编码刷盘,后续数据使用明文写入,这会导致一个 Block 中同一列数据前部分 Page 按字典编码存储、后部分 Page 按明文存储,因此在读数据时需对未编码的 Page 重新进行字典编码,显而易见这种逆操作会极大降低性能。
Relyt 针对以上场景进行了优化,在读数据时,会同时考虑字典编码 Page 和非字典编码 Page,综合评估读的代价,决策是否以字典方式 or 非字典方式读;其次在读数据过程中,会根据收集到的统计信息直接读,尽量避免重新构建字典的开销。
自适应编码
选择合理的数据编码和压缩方式,需要综合考虑存储成本和查询性能,Relyt 通过自适应编码实现了用户透明。在新建表时,用户如果不指定列的数据编码方式,Relyt 会默认开启自适应编码。开启后,会针对表中每一列结合其数据类型,选择一种默认的编码和压缩方式,并在写入的过程中实时反馈,动态调整列的编码和压缩,实现局部数据最优。此外为减少局部数据的干扰,自适应编码和压缩结合了 AutoTableService 能力,实现全局视角的编码和压缩调优,使得落盘的数据文件在 Compaction 的过程中逐步使用最优的编码和压缩,降低存储成本和提升查询性能。
编辑
以字典编码为例,未开启自适应编码时,同一个 ColumnChunk 中可能部分 Page 采用字典编码,部分 Page 采用明文存储;开启自适应编码后,同一个 ColumnChunk 中所有的 Page 都采用同一种编码方式;在经过 AutoTableService 对文件进行整理后,同一列的不同 ColumnChunk 的编码方式会尽可能一致,且开启自适应编码后,保证了写入数据的压缩比。
结语
为提升查询性能和存储性价比,Relyt AI-ready Data Cloud在数据裁剪、编解码及压缩上进行了大量的优化工作,为客户带来了实实在在的优质体验。未来 Relyt 将会在这方面持续发力,为客户提供极致的查询性价比和更好的用户体验。
版权声明: 本文为 InfoQ 作者【AI数据云Relyt】的原创文章。
原文链接:【http://xie.infoq.cn/article/af7a7552753163e4c7a9b6f0f】。文章转载请联系作者。
评论