Uber应用分享 | 使用 Parquet Page Index 加速 Presto 查询

Source
当前,数据量呈快速增长态势,给诸如 Presto 等查询引擎带来了挑战。
Presto 作为一种流行的交互式查询引擎,具有可扩展、高性能并可与 Hadoop 进行平滑集成的特性。随着数据量的增长,Presto 需要读取更大的数据块并将其加载到内存中,继而导致IO、内存占用增大以及 GC 时间变长等。
Apache Parquet 是一种可用于高效存储和检索数据的开源列式文件格式,提供高效的数据压缩和编码方案,性能更优,能批量处理复杂的数据。

我们先前已经采取了一些措施来加快 Presto 对 Parquet 数据的读取速度,但需要读取的数据量依旧很大。从 Java 版本的 Parquet(parquet-mr 1.11.0) 开始,Parquet 添加了一个名为 Page Index 的特性,通过在列块(column chunk)中过滤不必要的 Parquet 页(page)来加快查询速度。

本文就该特性、移植到 Presto 的状态以及基准测试结果进行了介绍。

统计信息

Parquet 文件元数据包含有关文件中数据的最小/最大值的统计信息。对于一个给定 filter 的查询而言,统计信息的最小/最大值可以用作 filter 要查找的值的范围。如果要查找的值不在范围内,就可以跳过读取该数据块。这样一来提高了 IO、内存和 CPU等资源的利用率,从而加快了查询速度。

下述示例展示了如何将 filter 应用于包含统计信息的表,‘x > 100’的 filter 会查找(100, ∞) 范围内的值。表中的统计信息显示只有第二列(最小值为 1 和最大值为 209) 的数值范围 [1, 209],与 filter 的过滤范围重叠。因此,我们可以跳过读取剩余 3 列,从而将数据读取的时间减少3/4 。

Parquet Page Index

Parquet 包含列块和页的统计信息。列块的统计信息显示出该列块中数据的取值范围,而页统计信息显示的是页数据,粒度更细、更有效。下述示例展示了页统计信息与列统计信息相比,如何能更好地降低数据读取量。

在第一个示例中,有一个查询中包含了“x = 2000”的filter条件。为便于演示,我们仅在一个列块中显示三个页。图表中的三个页的统计信息构成了三个范围:[-100, 60]、[50, 234]和[800, 1000] 。列块的统计信息构成 [-100, 1000] 的范围。由于我们正在查找的值为2000 ,因此页统计信息和列块统计信息都显示无需读取该列中的数据。在这种情况下,页统计信息和列块统计信息一样,都有效地减少了数据读取量。

在第二个示例中,列块和页中的数据和统计信息与第一个示例相同。唯一不同的是filter “x = 55”。在这种情况下,由于该值在列块统计范围 [-100, 1000] 内,因此对于读取的判断为“yes”。同样地,第一个页统计范围 [50, 234] 和第二个页统计范围 [-100, 60] 对于读取的判断都为“yes”,但对于最后一个页而言,由于超出了 [800, 1000 ]的范围,因此读取判断为“no”。

在第三个示例中,列块和页中的数据和统计信息依旧与第一个示例相同,但filter更改为'x = 700'。该值在列块统计范围 [-100, 1000]内,因此对于读取的判断为“yes”,但对于所有页统计信息而言,由于所有的范围都不包含要查找的值“700”,因此读取的判断结果为“No”, 跳过整列数据,不予读取。

在所有上述示例中,我们在一个列块中只显示3个页,但实际上一个列块中通常有几十甚至几百个页。因此,在现实操作中能节省更多的成本。如果列数据是经过排序的,那么误报的可能性就会降低,从而使得页统计效果更好。

Presto的页统计

在1.11.0 版本之前,页统计信息都放在页头(page header)中。问题是要获取页统计信息必须读取每一个页。即便之后判定无需读取该页,但该页已经被读取。为了解决该问题,parquet-mr 1.11.0 将列块的所有页统计信息放在同一位置,方便reader可以一次性读取并判定应该读取哪个页。由于 Presto 部分重写了 parquet-mr 代码,我们需要将 Parquet 1.11.0 中的修改移植到 Presto 代码库中。代码修改(PR-17284) 已于 2022 年 2 月合并到 Presto master 分支中,并将在 0.273 版本中发布。

基准测试结果

我们基于需要频繁访问的生产数据表对 Parquet 列索引进行了基准测试。我们用原始生产环境的数据表的快照创建了一个测试表,它包含了若干数据分区,并且数据是经过排序的。之后,我们在测试环境中对表运行 Presto 查询。可以针对每个查询通过 Presto 会话属性设置打开和关闭 Page Index 功能,从而实现横向比较。

我们观察到,输入数据扫描存在巨大的优化空间。下图显示了 Parquet Page Index 启用(左)与禁用(右)时运行示例查询的统计信息。可以看出,他们在这一阶段生成了相同的数据,但是在未启用 Parquet 列索引(column index)时需要扫描 149.31MB/239K 行原始输入数据,而在启用 Parquet 列索引的情况下只需要扫描 63.31MB/75.4K 行原始输入数据。后者的输入读取量降低了57%。这是因为在使用 Parquet Page Index 时,operator 可以识别并跳过不符合过滤条件的页。

除了在原始输入读取量方面的提升外,我们还从测试中观察到内存使用量也有所降低。从下面的截图可以看出,在启用 Parquet Page Index(右)时,只需要使用 91.73MB的峰值内存,而在未启用 Parquet Page Index 时,则需要使用141.60 MB的峰值内存。实现此项提升在预期范围内:由于查询需要读取的原始输入数据更少,因此保存数据所需的内存也更小。

值得注意的是,我们测试用的Presto查询在经过排序的列上使用filter,例如:WHERE foo = bar,其中 foo 列是有序的,这也是 Parquet Page Index 降低读取量效果最显著的地方,如果不对 filter 依赖的列数据进行排序,则收益可能降低。

分享嘉宾:
Uber: Xinli Shang
Uber: Chen Liang

想要了解更多关于Alluxio的干货文章、热门活动、专家分享,可点击进入【Alluxio智库】