alitrack

小心,大数据炸弹来了

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

原文:[42.parquet – A Zip Bomb for the Big Data Age - DuckDB](https://duckdb.org/2024/03/26/42-parquet-a-zip-bomb-for-the-big-data-age)

汉内斯·穆勒塞恩 2024-03-26

太长不看:一个42 kB的Parquet文件可以包含超过4 PB的数据。

Apache Parquet[1] 已经成为表格数据交换的事实标准。它通过使用二进制、列式和压缩数据表示,远远优于它的可怕表亲CSV。此外,Parquet文件带有足够多的元数据,使得文件可以在没有额外信息的情况下被正确解释。大多数现代数据工具和服务都支持读取和写入Parquet文件。

然而,Parquet文件并非没有危险:例如,损坏的文件可以使不仔细解释内部偏移量的读取器崩溃。但即使是完全有效的文件也可能存在问题,并导致崩溃和服务停机,如下所示。

对天真的防火墙和病毒扫描器的一个众所周知的攻击是Zip炸弹[2],一个著名的例子是42.zip[3],当然是因为42是完美的数字[4],而且该文件只有42千字节大。这个完全有效的zip文件包含许多其他zip文件,这些文件又包含其他zip文件,如此等等。最终,如果有人试图解压所有这些,你将得到4 PB的数据。确实是大数据。

Parquet文件支持多种数据压缩方法。一个只有42千字节大的Parquet文件可以创建多大的表格,以zip炸弹的精神?让我们找出答案!出于可移植性的考虑,我们为DuckDB实现了自己的Parquet读取器和写入器[5]。在实现它时,不可避免地要对Parquet格式有深入的了解。

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

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

但我们可以在此基础上增加。列可以包含多个引用相同字典的页面,所以我们可以一遍又一遍地重复我们的数据页面,每次只向文件添加31字节,但向文件所代表的表格添加20亿个值。我们还可以使用另一种技巧来扩大数据大小:如前所述,Parquet文件包含一个或多个行组,这些行组存储在文件末尾的Thrift页脚中。这个行组中的每个列包含字节偏移量(data_page_offset等)到文件中存储列页面的位置。没有什么可以阻止我们添加多个行组,这些行组都引用相同的字节偏移量,即我们存储我们稍微淘气的字典和数据页面的位置。我们添加的每个行组在逻辑上重复所有页面。当然,添加行组也需要元数据存储,所以在添加页面(20亿个值)和行组(2倍于其他行组的内容)之间存在某种权衡。

经过一些调整,我们发现,如果我们重复数据页面1000次,重复行组290次,我们会得到一个Parquet文件[10],它只有42千字节大,但包含622万亿个值(确切地说是622,770,257,630,000)。如果有人在内存中实现这个表格,它将需要超过4 PB的内存,最终是一个真正的大数据[11]的例子,巧合的是,大约与上面提到的原始42.zip大小相同。

我们已经公开了我们用来生成这个文件的脚本[12],我们希望它可以被用来更好地测试Parquet读取器。我们希望已经展示了Parquet文件可能被认为是有害的,并且肯定不应该在没有额外小心的情况下被塞进某个流程中。虽然DuckDB可以从我们的文件中读取数据(例如,使用LIMIT),但如果你让它全部读取,你最好准备一些咖啡。

引用链接

[1] Apache Parquet: https://parquet.apache.org
[2] Zip炸弹: https://en.wikipedia.org/wiki/Zip_bomb
[3] 42.zip: https://www.unforgettable.dk
[4] 42是完美的数字: https://en.wikipedia.org/wiki/42_(number)#The_Hitchhiker's_Guide_to_the_Galaxy
[5] Parquet读取器和写入器: https://duckdb.org/docs/data/parquet/overview
[6] 字典编码: https://en.wikipedia.org/wiki/Dictionary_coder
[7] 行程长度编码: https://en.wikipedia.org/wiki/Run-length_encoding
[8] 指定的编码: https://parquet.apache.org/docs/file-format/data-pages/encodings/#run-length-encoding--bit-packing-hybrid-rle--3
[9] Thrift: https://thrift.apache.org
[10] Parquet文件: https://github.com/hannes/fortytwodotparquet/raw/main/42.parquet
[11] 大数据: https://motherduck.com/blog/big-data-is-dead/
[12] 公开了我们用来生成这个文件的脚本: https://github.com/hannes/fortytwodotparquet/blob/main/create-parquet-file.py