42.parquet – 大数据时代的 Zip 炸弹

Author Avatar
Hannes Mühleisen
2024-03-26 · 5 分钟

概要:一个 42 KB 的 Parquet 文件可以包含超过 4 PB 的数据。

Apache Parquet 已经成为表格数据交换的事实标准。它使用二进制、列式和压缩数据表示,大大优于可怕的 CSV。此外,Parquet 文件附带足够的元数据,因此无需额外信息即可正确解释文件。大多数现代数据工具和服务都支持读取和写入 Parquet 文件。

但是,Parquet 文件并非没有危险:例如,损坏的文件可能会导致不小心解释内部偏移量等的读取器崩溃。但即使是完全有效的文件也可能存在问题,并导致崩溃和服务中断,我们将在下面展示。

一种针对幼稚防火墙和病毒扫描程序的非常著名的攻击是 Zip 炸弹,一个著名的例子是 42.zip,之所以这样命名,当然是因为 42 是一个完美的数字,而且文件只有 42 KB 大小。这个完全有效的 zip 文件中包含一堆其他的 zip 文件,这些 zip 文件又包含其他的 zip 文件等等。最终,如果有人试图解压缩所有这些文件,您最终会得到 4 PB 的数据。确实是大数据。

Parquet 文件支持多种数据压缩方法。在 zip 炸弹的精神下,可以使用仅 42 KB 大小的 Parquet 文件创建多大的表? 让我们来了解一下!出于可移植性的原因,我们为 DuckDB 实现了我们自己的 Parquet 读取器和写入器。在实现它时,不可避免地要了解大量关于 Parquet 格式的知识。

一个 Parquet 文件由一个或多个行组组成,这些行组包含列,而列又包含所谓的页面,这些页面包含编码格式的实际数据。在其他编码中,Parquet 支持 字典编码,我们首先有一个包含字典的页面,然后是引用字典而不是包含纯数据值的数据页面。这对于经常重复长值(如分类字符串)的列更有效,因为字典引用可以小得多。

让我们利用这一点。我们编写一个包含单个值的字典,并一遍又一遍地引用它。在我们的示例中,我们使用一个 64 位整数,这是可能的最大值,因为为什么不呢。然后,我们使用 Parquet 中指定的 RLE_DICTIONARY 游程编码 来引用此字典条目。 指定的编码有点奇怪,因为出于某种原因,它结合了位打包和游程编码,但本质上我们可以使用尽可能大的游程长度,即 2^31-1,略高于 20 亿。由于字典很小(一个条目),因此我们重复的值为 0,指的是唯一的条目。包括其所需的元数据标头和页脚(与 Parquet 中的所有元数据一样,这是使用 Thrift 编码的),此文件仅为 133 字节。用 133 字节表示 20 亿个 8 字节整数还不错,即使它们都相同。

但是我们可以从那里开始上升。列可以包含多个引用相同字典的页面,因此我们可以一遍又一遍地重复我们的数据页面,每次仅向文件中添加 31 个字节,但向文件表示的表中添加 20 亿个值。我们还可以使用另一个技巧来扩大数据大小:如前所述,Parquet 文件包含一个或多个行组,这些行组存储在文件末尾的 Thrift 页脚中。此行组中的每一列都包含指向文件中存储列页面的字节偏移量(data_page_offset 和朋友)。没有任何东西可以阻止我们添加多个都指向同一个字节偏移量的行组,即我们存储略微恶作剧的字典和数据页面的那个字节偏移量。我们添加的每个行组都会在逻辑上重复所有页面。当然,添加行组也需要元数据存储,因此在添加页面(20 亿个值)和行组(2x 重复的其他行组)之间存在某种权衡。

经过一些摆弄,我们发现,如果我们重复数据页面 1000 次并重复行组 290 次,我们最终会得到 一个 Parquet 文件,该文件大小为 42 KB,但包含622 万亿个值(确切地说是 622,770,257,630,000 个)。如果有人在内存中实现此表,则需要超过4 PB 的内存,最终成为 大数据 的真实示例,巧合的是,大小与上述原始 42.zip 大致相同。

我们还提供了 我们用于生成此文件的脚本,我们希望它可以用来更好地测试 Parquet 读取器。我们希望已经表明 Parquet 文件可能被认为是有害的,绝对不应在不格外小心的情况下将其推入某些管道中。虽然 DuckDB 可以从我们的文件中读取数据(例如,使用 LIMIT),但如果您让它全部读取,最好喝杯咖啡。