java.lang.IllegalArgumentException: Illegal Capacity: -177

java.lang.IllegalArgumentException: Illegal Capacity: -177

Background

公司内部一些来自其他部门的重要数据需要我们Load到数仓中建表管理以供下游数据分析师使用,我们将其他Team产生的Parqeut格式的数据从其他的三方分布式文件系统(如S3)中抽取到HDFS上,然后在这些数据上建表。通常是没有问题的,但之后我们对某个文件进行建表后查询,就得到该异常。

Debug

通常我们拿到数据之后,会通过如下方式创建一张view,然后通过该view对该Parquet格数的数据的Schema进行校验。这些Parquet是其他Team用脚本处理之后通过Python写成的parquet文件。

Create or replace temporary view ***_data_check_test as
select * from parquet.`/data_stored_path_*`;

desc formatted ***_data_check_test;

针对正常的文件,这样的方式是可行的,但是当我们对个别文件进行同样的操作就会得到java.lang.IllegalArgumentException: Illegal Capacity: -177这样的异常。由于创建view时是通过select * 的方式创建的,于是我决定检查数据的每一个字段是否有问题。

select coulmn_name from parquet.`/data_stored_path_*`;

通过对每个字段的检查,果然其中大多数字段都是正常的可以被查询出来,只有某一个字段有该异常。然后我检查了异常文件的大小,由于平常每个parqet文件的大小都是2GB,而异常文件的大小往往都大于10G,因此我怀疑是否时该字段存在产生时由于某些循环导致多个值拼接在一起导致该字段超长导致的。

随后我检查了异常字段的长度,发现最大的字符串长度也才8万多,远达不到整型的益处范围。

select max(length(column_name)) from parquet.`/data_stored_path_*`;

我们让上游Team将对应CSV格式的数据发给我们,以同样的方式去建view读取数据,插入表等。CSV格数的数据完全正常。
由于我们的Job开发中只针对Parquet文件进行处理,并没有针对CSV格式进行拓展。因此我们还要针对这个异常进行进一步的检查,直到找到Root Cause。

于是我们向上游Team要了他们产生数据的code,并检查了产生Parquet和CSV文件的生成逻辑。我们发现实际上Parquet文件和CSV文件源于同一个DataFrame,只不过在生成CSV之后,对个别字段进行了类型转换,然后toParquet.并且CSV数据是正常的,而且个别字段的类型转换也不会导致数据的异常。

异常原因

之后在Google上进行了查询,也发现了类似的情况,并且异常的原因并不是数据本身造成的,而是由Parquet本身结构而造成的。

Apache Parquet是一种柱状文件格式。我们常用的一些文件,如文本文件、CSV等,每次只存储一行(或者记录)信息。这很容易概念化,并且有一些优点,但是列式存储数据也有一些优点。

Parquet structure :

  • 文件以一个页脚(Footer)结束,其中包含索引数据,用于在文件中找到其他数据的位置。
  • 文件被分成行组,包含该行组的行。
  • 行组包含关于一组行的每个列的信息。
  • 每个列包含元数据和字典查找数据,以及特定单个列的连续存储的行数。
  • 列数据本身被分割成页面。页面是不可分割的,如果他们被访问,必须完全解码,并可以应用压缩。

Why do this?

  • 将给定列的数据存储在一起,由于可能有很多相似的值,这种结构有助于更好的数据压缩。
  • 读取文件时,只需要读取和解码我们感兴趣的列。
  • 列包含具有其内容统计信息的元数据,我们可以跳过我们知道不感兴趣的数据。(谓词下推)

所有这一切意味着更少的IO和更快的数据读取,以更复杂的数据写入为代价。

Vortexa 遇到的问题

在Vortexa的幕后进程中,有一个Python进程,它在一个Parquet文件中产生大约6GB的输出,以及一个Java进程用来消耗这些输出。当有一天停止消费,会有一个ArrayList构造函数抱怨一个大小为负的参数。作者怀疑是32位有符号整数溢出。但是由于文件包含了大约6700万份记录,远远少于2³¹~= 21亿。应该是其他地方出现了什么问题。 用brew获取“parquet-tools”然后对文件运行“meta”命令。

hadoop jar /tmp/parquet-tools-1.11.1.jar meta hdfs_file

其中98%的数据在第一个行组,可以看到这个行组的大小超过6GB,远远超过2³¹字节的限制。

解决方案:

在Python中使用Panda编写文件时,指定一个行组大小参数为100万条记录。

data_frame.to_parquet(“file.parquet”, **{“engine”: “pyarrow”, “row_group_size”: 1000000})

添加这个参数可能会给文件大小增加5%的开销,但是文件的每个内部结构现在都可以轻松地适应2GB的大小,溢出问题也就解决了。

注:

默认情况下,Panda似乎根本没有拆分行组,这在使用Hive、Presto和其他技术时可能会损害性能。行组,特别是与智能排序相结合时,可以有效地对数据进行分片。

根据Apache库源代码,并根据本地旧的“损坏的”6GB生产文件重新创建问题,在调试器中摸索,追踪失败的地方后发现:

  • 行组大小不是直接导致溢出问题的原因。
  • 行组中的列大小是导致问题的原因。
  • 字符串列通常压缩得很好,并应用字典查找(Parquet做到了这一点),但我们有一个列包含SHA哈希,熵很高,不能压缩。在一个行组中,这个列的大小超过4GB。
  • Parquet规范没有限制这些数据结构的大小为2GB(2³¹字节)甚至4GB(2³²字节)。当与某些工具一起使用时,Python/Pandas输出可能效率不高,但它并没有错。

因此,库的错误是无法读取这个有效的文件,尽管来自Python/Pandas的文件结构不是很好。

经过更多的试验,发现拥有一个较大的(超过2GB)列并不会破坏Java库本身,但是如果在该数据之后有一个后续的行组,读取该数据就会破坏Java库。在调试过程中,发现从Parquet文件中读取了一个64位的有符号值作为文件偏移量,但随后它被转换为一个32位的有符号值,截断的值的大小超过2³¹。

有效地将32位(或更多)的无符号正数放入32位有符号类型中,这使我们的数字为负——它溢出了。这导致ArrayList的构造函数被调用为一个负大小值。

引用: https://www.vortexa.com/insight/when-parquet-columns-get-too-big

https://issues.apache.org/jira/browse/PARQUET-1633
posted @ 2022-04-13 18:13  yuexiuping  阅读(507)  评论(0编辑  收藏  举报