摘要:使用Spark SQL进行ETL任务,在读取某张表的时候报错:“IOException: totalValueCount == 0”,但该表在写入时,并没有什么异常。

本文分享自华为云社区《你的Parquet该升级了:IOException: totalValueCount == 0问题定位之旅》,原文作者:wzhfy 。

1. 问题描述

使用Spark SQL进行ETL任务,在读取某张表的时候报错:“IOException: totalValueCount == 0”,但该表在写入时,并没有什么异常。

2. 初步分析

该表的结果是由两表join后生成。经分析,join的结果产生了数据倾斜,且倾斜key为null。Join后每个task写一个文件,所以partition key为null的那个task将大量的null值写入了一个文件,null值个数达到22亿。

22亿这个数字比较敏感,正好超过int最大值2147483647(21亿多)。因此,初步怀疑parquet在写入超过int.max个value时有问题。

【注】本文只关注大量null值写入同一个文件导致读取时报错的问题。至于该列数据产生如此大量的null是否合理,不在本文讨论范围之内。

3. Deep dive into Parquet (version 1.8.3,部分内容可能需要结合Parquet源码进行理解)

入口:Spark(Spark 2.3版本) -> Parquet

Parquet调用入口在Spark,所以从Spark开始挖掘调用栈。

InsertIntoHadoopFsRelationCommand.run()/SaveAsHiveFile.saveAsHiveFile() -> FileFormatWriter.write()

这里分几个步骤:

  1. 启动作业前,创建outputWriterFactory: ParquetFileFormat.prepareWrite()。这里会设置一系列与parquet写文件有关的配置信息。其中主要的一个,是设置WriteSupport类:ParquetOutputFormat.setWriteSupportClass(job, classOf[ParquetWriteSupport]),ParquetWriteSupport是Spark自己定义的类。
  2. 在executeTask() -> writeTask.execute()中,先通过outputWriterFactory创建OutputWriter (ParquetOutputWriter):outputWriterFactory.newInstance()。
  3. 对于每行记录,使用ParquetOutputWriter.write(InternalRow)方法依次写入parquet文件。
  4. Task结束前,调用ParquetOutputWriter.close()关闭资源。

3.1 Write过程

在ParquetOutputWriter中,通过ParquetOutputFormat.getRecordWriter构造一个RecordWriter(ParquetRecordWriter),其中包含了:

  1. prepareWrite()时设置的WriteSupport:负责转换Spark record并写入parquet结构
  2. ParquetFileWriter:负责写入文件

ParquetRecordWriter中,其实是把write操作委托给了一个internalWriter(InternalParquetRecordWriter,用WriteSupport和ParquetFileWriter构造)。

现在让我们梳理一下,目前为止的大致流程为:

SingleDirectoryWriteTask/DynamicPartitionWriteTask.execute
-> ParquetOutputWriter.write -> ParquetRecordWriter.write -> InternalParquetRecordWriter.write

接下来,InternalParquetRecordWriter.write里面,就是三件事:

(1)writeSupport.write,即ParquetWriteSupport.write,里面分三个步骤:

    1. MessageColumnIO.MessageColumnIORecordConsumer.startMessage;
    2. ParquetWriteSupport.writeFields:写入一行中各个列的值,null值除外;
    3. MessageColumnIO.MessageColumnIORecordConsumer.endMessage:针对第二步中的missing fields写入null值。
      ColumnWriterV1.writeNull -> accountForValueWritten:
      1) 增加计数器valueCount (int类型)
      2) 检查空间是否已满,需要writePage - 检查点1

(2)增加计数器recordCount(long类型)

(3)检查block size,是否需要flushRowGroupToStore - 检查点2

由于写入的值全是null,在1、2两个检查点的memSize都为0,所以不会刷新page和row group。导致的结果就是,一直在往同一个page里增加null值。而ColumnWriterV1的计数器valueCount是int类型,当超过int.max时,溢出,变为了一个负数。

因此,只有当调用close()方法时(task结束时),才会执行flushRowGroupToStore:
ParquetOutputWriter.close -> ParquetRecordWriter.close
-> InternalParquetRecordWriter.close -> flushRowGroupToStore
-> ColumnWriteStoreV1.flush -> for each column ColumnWriterV1.flush

由于valueCount溢出为负,此处也不会写page。

因为未调用过writePage,所以此处的totalValueCount一直为0。
ColumnWriterV1.writePage -> ColumnChunkPageWriter.writePage -> 累计totalValueCount

在write结束时,InternalParquetRecordWriter.close -> flushRowGroupToStore -> ColumnChunkPageWriteStore.flushToFileWriter -> for each column ColumnChunkPageWriter.writeToFileWriter:

  1. ParquetFileWriter.startColumn:totalValueCount赋值给currentChunkValueCount
  2. ParquetFileWriter.writeDataPages
  3. ParquetFileWriter.endColumn:currentChunkValueCount(为0)和其他元数据信息构造出一个ColumnChunkMetaData,相关信息最终会被写入文件。

3.2 Read过程

同样以Spark为入口,进行查看。
初始化阶段:ParquetFileFormat.BuildReaderWithPartitionValues -> VectorizedParquetRecordReader.initialize -> ParquetFileReader.readFooter -> ParquetMetadataConverter.readParquetMetadata -> fromParquetMetadata -> ColumnChunkMetaData.get,其中包含valueCount(为0)。

读取时:VectorizedParquetRecordReader.nextBatch -> checkEndOfRowGroup:
1) ParquetFileReader.readNextRowGroup -> for each chunk, currentRowGroup.addColumn(chunk.descriptor.col, chunk.readAllPages())

由于getValueCount为0,所以pagesInChunk为空。

2)构造ColumnChunkPageReader:

由于page列表为空,所以totalValueCount为0,导致在构造VectorizedColumnReader时报了问题中的错误。

4. 解决方法:Parquet升级(version 1.11.1)

在新版本中,ParquetWriteSupport.write ->
MessageColumnIO.MessageColumnIORecordConsumer.endMessage ->
ColumnWriteStoreV1(ColumnWriteStoreBase).endRecord:

在endRecord中增加了每个page最大记录条数(默认2w条)的属性和检查逻辑,超出限制时会writePage,使得ColumnWriterV1的valueCount不会溢出(每次writePage后会清零)。

而对比老版本1.8.3中,ColumnWriteStoreV1.endRecord为空函数。

附:Parquet中的一个小trick

Parquet中为了节约空间,当一个long类型的值,在一定范围内时,会使用int来存储,其方法如下:

  • 判断是否可以用int存储:

  • 如果可以,用IntColumnChunkMetaData代替LongColumnChunkMetaData,构造时转换:

  • 使用时,再转回来,IntColumnChunkMetaData.getValueCount -> intToPositiveLong():

普通的int范围是 -2^31 ~ (2^31 - 1),由于元数据信息(如valueCount等)都是非负整数,那么实际只能存储0 ~ (2^31 - 1) 范围的数。而用这种方法,可以表示0 ~ (2^32 - 1) 范围的数,表达范围也大了一倍。

附件:可用于复现的测试用例代码(依赖Spark部分类,可置于Spark工程中运行)

测试用例代码.txt 1.88KB

点击关注,第一时间了解华为云新鲜技术~

你的Parquet该升级了:IOException: totalValueCount == 0问题定位之旅的更多相关文章

  1. Hadoop-1.2.1 升级到Hadoop-2.6.0 HA

      Hadoop-1.2.1到Hadoop-2.6.0升级指南   作者 陈雪冰 修改日期 2015-04-24 版本 1.0     本文以hadoop-1.2.1升级到hadoop-2.6.0 Z ...

  2. DPA 9.1.85 升级到DPA 10.0.352流程

    SolarWinds DPA的升级其实是一件非常简单的事情,这里介绍一下从DPA 9.1.95升级到 DPA 10.0.352版本的流程.为什么要升级呢? DPA给用户发的邮件已经写的非常清楚了(如下 ...

  3. jackson2.5.0升级到2.7.0

    开发环境:spring-mvc4.1.7.jackson2.7.0 问题描述:项目中将原来的jackson2.5.0升级到2.7.0,导致服务调用出错. mvc相关的配置文件如下: <?xml ...

  4. ORACLE 10升级到10.2.0.5 Patch Set遇到的内核参数检测失败问题

    在测试ORACLE 10.2.0.4升级到10.2.0.5 Patch Set的过程中,遇到一个内核参数检查失败的问题,具体错误信息如下所示   实验环境: 操作系统:Oracle Linux Ser ...

  5. 探索Oracle数据库升级6 11.2.0.4.3 Upgrade12c(12.1.0.1)

    探索Oracle数据库升级6 11.2.0.4.3 Upgrade12c(12.1.0.1) 一.前言:       Oracle 12c公布距今已经一年有余了,其最大亮点是一个能够插拔的数据库(PD ...

  6. linux内核升级(ubuntu12.04从3.13.0升级到3.4.0 )

    花了一天的时间,终于把ubuntu12.04 的linux内核版本从3.13.0升级到3.4.0 升级后,系统更加稳定.具体步骤:# wget http://www.kernel.org/pub/li ...

  7. mac版chrome升级到Version 65.0.3325.18后无法打开百度bing搜狗

    mac版本chrome升级到Version 65.0.3325.18后发现突然无法访问百度,搜狗,bing,神马等一系列的国内搜索引擎网站.连百度的儿子们比如知道,百度百科都无法访问. 1.首先想到的 ...

  8. 【Android Studio安装部署系列】三十一、从Android studio3.0.0升级到Android studio3.0.1

    版权声明:本文为HaiyuKing原创文章,转载请注明出处! 概述 突然想要升级到较高版本.要跟随潮流嘛,不然就落后了. 下载IDE http://www.wanandroid.com/tools/i ...

  9. CoreProfiler升级到.NetStandard 2.0

    致所有感兴趣的朋友: CoreProfiler和相应的Sample项目cross-app-profiling-demo都已经升级到.NetStandrard 2.0和.NetCore 2.0. 有任何 ...

  10. maven私库nexus2.11.4迁移升级到nexus3.12.0

    nexus简介 nexus是一个强大的maven仓库管理器,它极大的简化了本地内部仓库的维护和外部仓库的访问. nexus是一套开箱即用的系统不需要数据库,它使用文件系统加Lucene来组织数据 .n ...

随机推荐

  1. Chromium Command Buffer原理解析

    Command Buffer 是支撑 Chromium 多进程硬件加速渲染的核心技术之一.它基于 OpenGLES2.0 定义了一套序列化协议,这套协议规定了所有 OpenGLES2.0 命令的序列化 ...

  2. k8s Rabbitmq安装部署

    安装方式 kubectl apply -f rabbitmq.yaml -n yunda-dev-cache rabbitmq.yaml ##创建PV # 注意更换存储方式 --- apiVersio ...

  3. 八个SVN提交的好习惯(转)

    一.提交之前先更新 1.         SVN更新的原则是要随时更新,随时提交.当完成了一个小功能,能够通过编译并且自己测试之后,谨慎地提交. 2.         如果在修改的期间别人也更改了sv ...

  4. MySQL函数解读

    一.字符串函数 1.1.instr() INSTR(str,substr)一共有两个参数str被查询字符,substr查询字符,查询时下标从1开始记,只查询第一次出现的地方,为查询到显示为0 一般用法 ...

  5. 万字解析XML配置映射为BeanDefinition的源码

    本文分享自华为云社区<Spring高手之路16--解析XML配置映射为BeanDefinition的源码>,作者:砖业洋__. 1. BeanDefinition阶段的分析 Spring框 ...

  6. 混合应用与Hybrid App开发上架流程透析

    Hybrid App(混合 App)已经成为大家接触最为广泛的 App 形式,不管是我们用到的微信.支付宝还是淘宝.京东等大大小小的应用都非常热衷于Hybrid App 带来的研发效率提升和灵活性. ...

  7. 【算法】状态之美,TCP/IP状态转换探索

    最近城市里甲流肆虐,口罩已经成为了出门必备的物品.小悦也不得不开始采取防护措施,上下班过程中,将口罩戴起来以保护自己不受病毒的侵害. 每天下班后,小悦总是喜欢投入到自己的兴趣爱好中,她热衷于翻阅与IT ...

  8. python列表之索引及len()函数

    我们在刚开始使用列表的时候,经常会遇到这种错误 list_1 = ['one', 'two', 'three', 'four', 'five'] print(list_1[5]) 这段代码看上去是没有 ...

  9. python之字符串format()方法

    format()方法就是格式化指定的值,然后再将其插入字符串的占位符内 占位符用大括号{}来定义哈,可以使用命名索引{sws}.编号索引{0}.甚至空的占位符{}来标识占位符. 语法就是 string ...

  10. python判断素数

    def slowsnail(num): count = num // 2 while count > 1: if num % count == 0: print('%d最大的约数是%d' % ( ...