提到镶木地板行组大小时实际意味着什么?

tee*_*jay 6 parquet apache-arrow pyarrow

我开始使用镶木地板文件格式。Apache 官方网站建议使用 512MB 到 1GB 的大行组(此处)。一些在线资源(例如这个)建议默认行组大小为 128MB。

我有大量 parquet 文件,稍后我将使用 AWS Glue 上的 PySpark 在下游处理这些文件。这些文件具有非常小的行组。我无法控制我开始使用的文件,但想要组合行组,以便在下游处理之前获得“更高效”的文件(为什么?这些文件将上传到 S3 并使用 Spark 进行处理;我的理解是Spark 一次会读取一个行组,因此更多较小的行组会导致 IO 操作增加,效率低下;如果此假设无效,请赐教)。

对于这个问题,我们只考虑其中一个文件。它经过压缩(经过snappy压缩),在磁盘上的大小为 85MB。当我使用该pqrs工具检查其架构时,它报告该文件在 1,115 个行组中有 55,733 条记录,每个行组似乎约为 500 kB - 具体来说,如下所示:

row group 7:
--------------------------------------------------------------------------------
total byte size: 424752
num of rows: 50
Run Code Online (Sandbox Code Playgroud)

如果我简单地采用(1115 个行组 * 500 kB/行组),则大约为 500MB;而磁盘上的文件为 85MB。诚然,有些行组小于 500kB,但我观察了大约 100 个行组(一半在顶部,一半在底部),它们都在大致范围内。

子问题 1: 差异是多少(计算值 500MB 与实际值 85MB),因为报告的行组大小实际上pqrs代表未压缩的大小,也许行组的内存大小是多少(大概会大于磁盘上的压缩序列化大小)?换句话说,我不能做一个简单的 1115 * 500 但必须应用某种压缩因子?

子问题2: 当我看到建议的批量大小是128MB时,这到底是什么意思?未压缩的内存大小?磁盘上的序列化压缩大小?还有别的事吗?它与 所报道的内容有什么关系pqrs

我用于压缩这些行组的(简化的)代码是:

row group 7:
--------------------------------------------------------------------------------
total byte size: 424752
num of rows: 50
Run Code Online (Sandbox Code Playgroud)

主要问题:应该batchsize是什么?

iter_batches视为batch_size记录数而不是字节大小。我可以根据总记录和所需的批次数来计算它,但我不清楚我应该在这里计算什么。

我试过这个:

  • required # 批次 = 磁盘上的文件大小(以 MB 为单位)/ 128 = 85/128 = 1(向上舍入)
  • 批次大小 = # 条记录 / 所需 # 批次 = 55,733 / 1 = 60000(四舍五入到下一个 10k)

当我以 60k 的批量大小运行代码时:

  • 我有两个记录组(太好了,1,115 减少到 2;但为什么不减少到 1?)
  • 第一个记录组的报告字节大小约为 250MB。因此,尽管它最终创建的行组数量是我预期的两倍,但它们实际上是我预期大小的两倍,而不是每个行组的大小是我预期的一半。
row group 0:
--------------------------------------------------------------------------------
total byte size: 262055359
num of rows: 32768
Run Code Online (Sandbox Code Playgroud)

我认为我的一些假设 - 或对 parquet 文件格式、pqrs工具或pyarrow库的理解 - 是错误的。有人可以帮我揭开神秘面纱吗?

Pac*_*ace 10

TL;DR - 1 Mi 行

你的理解大致是正确的。不同的工具有不同的建议,一些工具(例如 pyarrow)将使用行数来确定行组大小,而其他工具(例如 parquet-mr,spark 使用的 java parquet 实现)将使用字节数。

是差异(计算的 500MB 与实际的 85MB),因为 pqrs 报告的行组大小实际上代表未压缩的大小

是的。工具对此往往不是很清楚。我发现在处理 parquet 元数据字段时, parquet thrift 定义是一个很好的基本事实来源。

struct RowGroup {
  /** Metadata for each column chunk in this row group.
   * This list must have the same order as the SchemaElement list in FileMetaData.
   **/
  1: required list<ColumnChunk> columns

  /** Total byte size of all the uncompressed column data in this row group **/
  2: required i64 total_byte_size
Run Code Online (Sandbox Code Playgroud)

当我看到建议的批量大小是 128MB 时,这到底是什么意思?未压缩的内存大小?磁盘上的序列化压缩大小?还有别的事吗?它与 pqrs 报告的内容有何关系?

主要问题:批量大小应该是多少?

答案通常归结为确保您发出的 I/O 请求适合您的存储系统。但是,如果您的行组非常小(例如 100、1k、10k 行),那么您的存储是什么可能并不重要(既因为行组引入了额外的计算,又因为行组影响元数据/数据比率)。这些非常小的尺寸几乎总是对性能不利。

如果您使用 HDFS,我相信规则可能会略有不同。我对 HDFS 没有太多经验。在所有其他情况下,您通常希望行组足够大,以便您的 I/O 请求足够大以满足您的存储系统。

例如,当从 HDD 读取时,如果执行一堆随机 64 字节读取,则会比一堆连续 64 字节读取获得更差的性能。但是,如果您执行一堆随机 4MiB 读取,那么您应该获得与一堆连续 4MiB 读取大致相同的性能。当然,这取决于硬盘驱动器,但我发现 4MiB 对于 HDD 来说是一个不错的数字。另一方面,如果您从 S3 读取数据,那么他们的指南建议为 8-16MiB。

将其转换为行组大小有点棘手,并且取决于您的查询习惯。如果您通常从文件中读取所有列,那么您将希望行组为 8-16MiB。另一方面,如果您通常只从文件中读取“某些”列,那么您希望每列为 8-16MiB。

现在事情变得棘手,因为我们必须考虑压缩和编码。例如,布尔列几乎不会是 8MiB。由于压缩,您至少需要 64Mi 行,并且可能需要更多行。float32 列更容易理解。您应该获得 2Mi 行的 8MiB 读取,并且在许多情况下,您不会从中获得太多压缩。

以上都是理论。在实践中,我在本地磁盘和 S3 上进行了大量的基准测试,我发现 1Mi 行通常对于行组来说是一个不错的大小。在某些情况下,较大的行组可能是个好主意,而使用较小的行组仍然可以获得相当好的性能。您最终需要针对您自己的个人用例进行基准测试。然而,1Mi 是一个很好记的整数。如果您需要以未压缩字节数表示行组大小,那么这取决于您有多少列。同样,根据经验,我们可以假设列是 4 个字节,因此您可以使用以下计算:

# of bytes = 1Mi * # of columns * 4
Run Code Online (Sandbox Code Playgroud)

换句话说,如果您有 10 列,那么行组的大小至少为 40MiB。

如果我使行组太大怎么办?

鉴于上述情况,使行组变得庞大似乎很简单。这将确保您拥有理想的 I/O 请求。在完美的世界中,如果所有 parquet 读取器都创建为平等的,那么我会说这是正确的(每个文件 1 行组是理想的)。

然而,许多镶木地板阅读器将使用行组作为:

  • 并行度单位 - 在这种情况下,如果文件中只有一个行组,您将无法获得足够的并行度。
  • 读取的最小大小 - 在这种情况下,即使您正在进行流处理,读取器的 RAM 消耗也会非常高。

由于这些原因,您通常希望避免行组大小过大。

下推过滤/统计/等怎么样?

这通常是保持行组较小的另一个原因。行组统计是最容易使用的下推过滤工具,一些读者完全依赖于此。这意味着下推只能根据过滤器过滤掉整个行组。因此,较小的行组意味着您更有可能完全消除 I/O。

幸运的是,parquet 读取工具已经慢慢转向使用页面级统计数据(或页面级布隆过滤器)来执行此过滤。页面非常小(~1MiB),并且可以提供非常好的过滤分辨率(尽管在极少数情况下,分辨率太精细,因为它需要更多的元数据处理)。如果您的镶木地板阅读器能够利用页面级统计信息进行下推,那么行组大小应该不会影响下推。

任何类型的单行跳过或加载都与行组大小无关。Parquet 读取器应该能够以页面级分辨率应用跳过。

免责声明:我从事 arrow-c++/pyarrow 工作。由于我上面描述的一些原因,pyarrow 数据集读取器的性能非常依赖于行组大小(我正在慢慢尝试解决此问题)。