介绍
在 Hudi 0.10 中,我们引入了对高级数据布局优化技术的支持,例如 Z-order和希尔伯特空间填充曲线[1](作为新的聚类算法),即使在经常使用过滤器查询大表的复杂场景中,也可以在多个列而非单个列上进行数据跳过。
但实际上什么是Data Skipping数据跳过?
随着存储在数据湖中的数据规模越来越大,数据跳过作为一种技术越来越受欢迎。数据跳过本质上是各种类型索引[2]的通用术语,使查询引擎能够有效地跳过数据,这与它当前执行的查询无关,以减少扫描和处理的数据量,节省扫描的数据量以及( 潜在地)显着提高执行时间。让我们以一个简单的非分区parquet表“sales”为例,它存储具有如下模式的记录:
此表的每个 parquet 文件自然会在每个相应列中存储一系列值,这些值与存储在此特定文件中的记录集相对应,并且对于每个列 parquet 将遵循自然顺序(例如,字符串、日期、整数等) 或推导一个(例如,复合数据类型 parquet 按字典顺序对它们进行排序,这也匹配其二进制表示的排序)。但是如果有一个排序和一个范围......还有最小值和最大值!现在意味着每个 Parquet 文件的每一列都有明确定义的最小值和最大值(也可以为 null)。最小值/最大值是所谓的列统计信息的示例 - 表征存储在列文件格式(如 Parquet)的单个列中的值范围的指标,比如
• 值的总数
• 空值的数量(连同总数,可以产生列的非空值的数量)
• 列中所有值的总大小(以字节为单位)(取决于使用的编码、压缩等)
配备了表征存储在每个文件的每个单独列中的一系列值的列统计信息,现在让我们整理下表:每一行将对应于一对文件名和列,并且对于每个这样的对,我们将写出相应的统计数据:最小值,最大值,计数,空计数:
这本质上是一个列统计索引!为方便起见我们对上表进行转置,使每一行对应一个文件,而每个统计列将分叉为每个数据列的自己的副本:
这种转置表示为数据跳过提供了一个非常明确的案例:对于由列统计索引索引的列 C1、C2、... 上的谓词 P1、P2、... 的查询 Q,我们可以根据存储在索引中的列统计信息评估这些谓词 P1、P2 等对于表的每个对应文件,以了解特定文件“file01”、“file02”等是否可能包含与谓词匹配的值。这种方法正是 Spark/Hive 和其他引擎所做的,例如,当他们从 Parquet 文件中读取数据时——每个单独的 Parquet 文件都存储自己的列统计信息(对于每一列),并且谓词过滤器被推送到 Parquet Reader 它能够评估所讨论的查询是否符合存储在列中(在文件中)的数据条件,从而避免在文件不包含任何与查询谓词匹配的数据的情况下对数据进行不必要的提取、解压缩和解码。但是如果 Parquet 已经存储了列统计信息,那么创建附加索引有什么意义呢?每个 Parquet 文件仅单独存储我们上面组合的索引中的一行。这种方法的明显缺点是,要了解哪些文件可能包含查询正在寻找的数据,查询引擎必须读取表中影响查询性能的每个 Parquet 文件的 Parquet 页脚(甚至可能导致来自云的限制[3])存储)与以更紧凑格式表示的专用索引相比。
Hudi 0.11 中的列统计索引和数据跳过
在 Hudi 0.10 中,我们引入了非常简单的列统计索引(存储为简单的 Parquet 表)的权宜之计实现,以支持 Hudi 中数据跳过实现的第一个版本,以展示 Z-order 和 Hilbert 的强大功能空间填充曲线作为高级布局优化技术。在 Hudi 0.11 中,我们在元数据表中引入了多模索引[4],例如布隆过滤器索引和列统计索引,这两者都实现为元数据表中的专用分区(分别为“column_stats”和“bloom_filters”)。虽然这些新索引仍处于试验阶段,但将列统计索引移动到元数据表中意味着更多:
• 强大的支持:列统计索引 (CSI) 现在还享有元数据表的一致性保证
• 高效实现:元数据表使用 HFile[5] 作为基础文件和日志文件格式,促进基于键的快速查找(排序键值存储)。实际上意味着对于具有大量列的大型表,我们不需要读取整个列统计索引,并且可以通过查找查询中引用的列来简单地投影其部分。
设计
在这里,我们将介绍新列统计索引设计的一些关键方面。如果您对更多详细信息感兴趣,请查看 RFC-27[6] 了解更多详细信息。列统计索引作为独立分区保留在元数据表中(指定为“column_stats”)。为了能够在保持灵活性的同时跟上最大表的规模,可以将索引配置为分片到多个文件组中,并根据其键值将单个记录散列到其中的任何一个中。要配置文件组的数量,请使用以下配置(默认值为 2):
如前所述,元数据表使用 HFile 作为其存储文件格式(这是一种非常有效的排序二进制键值格式),以便能够
• 有效地查找基于它们的键的记录以及
• 根据键的前缀有效地扫描记录范围
为了解释如何在列统计索引中使用它,让我们看一下它的记录键的组成:
用列前缀索引记录的键不是随机的,而是由以下观察引起的
• 通过 HFile 存储所有排序的键值对,这样的键组合提供了与特定列 C 相关的所有记录的局部性的良好属性
• 对原始表的任何给定查询通常只过滤少数列,这意味着我们可以通过避免读取完整索引来寻求效率,而是简单地将其连续切片投影到列 C1、C2 等查询过滤上
为了更好地举例说明,让我们看一下 C2 列上的查询 Q 过滤:
我们可以简单地读取一个连续的记录块,而无需 a) 读取整个索引(可能很大),也不需要 b) 随机寻找我们感兴趣的记录。这使我们能够在非常大的表上获得可观的性能改进。
基准测试
为了全面演示列统计索引和数据跳过功能,我们将使用众所周知的 Amazon 评论数据集(仅占用 50Gb 存储空间),以便任何人都可以轻松复制我们的结果,但是使用稍微不常见的摄取配置来展示列统计索引和数据跳过带来的效率如何随着数据集中的文件数量而变化。
摄取
为了将 Amazon 评论数据集提取到 Hudi 表中,我们使用了这个gist[7]。请注意,您必须指定以下配置属性以确保在摄取期间同步构建列统计索引:
但是,如果您想在当前没有列统计索引的现有表上运行实验,您可以利用异步索引器功能回填现有表的索引。
查询
请注意要查看数据跳过操作,需要执行以下操作:
• 确保在读取路径上启用了元数据表
• 数据跳过功能已启用
为此必须将以下 2 个属性指定为 Spark 或 Hudi 选项:
默认情况下元数据表仅在写入端启用,如果读者愿意在读取路径上利用元数据表,他们仍然必须明确指定相应的配置 请查看此gist[8]以了解如何查询先前摄取的数据集。
EMR 配置
所有测试都在具有以下配置的小型 EMR 集群上执行,如果您选择这样做可以轻松地重现相同的结果。节点:m5.xlarge(1 个 master / 3 个 executor) Spark:OSS 3.2.1(Hadoop 3.2)
运行非分区 COW 表
请注意我们故意压缩文件大小以生成大量有意义的文件,因为数据集只有 50Gb。
• 数据集:亚马逊评论(约 50Gb 未压缩)
• 记录:161M(~160 字节)
• 表类型:COW(非分区)
• 文件大小:1Mb
• 文件数:~39k(总大小~47Gb,压缩,zstd)
• 列统计:21 列(~847k 记录,~63 Mb)
• 预热:否(冷缓存,每次都重新启动 shell 以刷新任何缓存)
从上表中可以很容易地看出,由 Hudi 0.11 中的新列统计索引提供支持的数据跳过显着提高了查询的执行性能(与其修剪潜力成正比),减少了执行运行时间并节省了关键的计算资源 直接转化为基于 Hudi 的基于云的 Lakes 和 Lakehouses 的成本节约。尽管现在 Hudi 用户已经可以使用列统计索引和数据跳过的功能,但目前还有更多工作要做:
• 支持 Merge-On-Read 表中的数据跳过
• 为列统计索引查询添加缓存
• 进一步分析和优化列统计索引性能
如果您想关注当前正在进行的工作,请查看 HUDI-1822[9] 并留下您的评论。