如何在Apache Spark中处理更改镶木地板模式

时间:2016-12-02 07:52:58

标签: apache-spark apache-spark-sql spark-dataframe emr parquet

我遇到了一个问题,我将Parquet数据作为S3中的每日块(以s3://bucketName/prefix/YYYY/MM/DD/的形式)但我无法从不同日期读取AWS EMR Spark中的数据,因为某些列类型不匹配我得到许多例外之一,例如:

java.lang.ClassCastException: optional binary element (UTF8) is not a group
在某些文件中出现

时,有一个数组类型有一个值,但同一列在其他文件中可能有null值,然后推断为String类型。

org.apache.spark.SparkException: Job aborted due to stage failure: Task 23 in stage 42.0 failed 4 times, most recent failure: Lost task 23.3 in stage 42.0 (TID 2189, ip-172-31-9-27.eu-west-1.compute.internal):
org.apache.spark.SparkException: Failed to merge incompatible data types ArrayType(StructType(StructField(Id,LongType,true), StructField(Name,StringType,true), StructField(Type,StringType,true)),true)

我在S3中以JSON格式存在原始数据,我最初的计划是创建一个自动作业,启动一个EMR集群,读取前一个日期的JSON数据,然后将其作为镶木地板写回S3。

JSON数据也分为日期,即密钥具有日期前缀。阅读JSON工作正常。无论当前正在读取多少数据,都可以从数据中推断出模式。

但是当编写镶木地板文件时问题就会出现。据我所知,当我使用元数据文件编写镶木地板时,这些文件包含镶木地板文件的所有零件/分区的模式。对我而言,似乎也可以使用不同的模式。当我禁用写入元数据时,据说Spark从给定Parquet路径中的第一个文件推断整个模式,并假设它通过其他文件保持不变。

当某些列(应该是double类型)只有给定日期的整数值时,从JSON读取它们(其中这些数字为整数,没有浮点数)使Spark认为它是一列类型long。即使我可以在编写Parquet文件之前将这些列转换为double,但这仍然不好,因为架构可能会更改,可以添加新列,并且无法跟踪此列。

我看到有些人有同样的问题,但我还没有找到一个足够好的解决方案。

对此最佳做法或解决方案是什么?

3 个答案:

答案 0 :(得分:9)

这些是我用于将镶木地板写入S3的选项;关闭模式合并会提高写回性能 - 也可能会解决您的问题

val PARQUET_OPTIONS = Map(
 "spark.sql.parquet.mergeSchema" -> "false",
 "spark.sql.parquet.filterPushdown" -> "true")

答案 1 :(得分:6)

当我从JSON的每日块中读取数据并在每日S3文件夹中写入Parquet时,在写入Jquet之前没有指定我自己的模式或在写入Parquet之前将容易出错的列转换为正确的类型时,Spark可能会推断出不同的模式不同天数的数据取决于数据实例中的值,并使用冲突的模式编写Parquet文件。

这可能不是一个完美的解决方案,但我发现通过不断发展的架构解决问题的唯一方法如下:

在我每天(更具体地说是每晚)批量处理前一天数据的cron工作之前,我正在创建一个大多数为空值的虚拟对象。

我确保ID是可识别的,例如,因为真实数据具有唯一ID-s,我添加" dummy" string作为伪数据对象的ID。

然后我将给出具有容易出错类型的属性的预期值,例如我将给出浮点数/双精度非零值,因此当编组到JSON时,它们肯定会有小数分隔符,例如" 0.2& #34;而不是" 0" (当编组到JSON时,带有0值的双精度/浮点数显示为" 0"不是" 0.0")。

字符串和布尔值和整数工作正常,但除了双精度/浮点数之外,我还需要将数组实例化为空数组和其他类/结构体的对象以及相应的空对象,这样它们就不会是" null" -s,因为Spark以字符串形式读取null-s。

然后如果我填写了所有必要的字段,我会将对象编组为JSON并将文件写入S3。

然后我会在我的Scala批处理脚本中使用这些文件来读取它们,将模式保存到变量中,并在读取真实的JSON数据时将此模式作为参数提供,以避免Spark执行自己的模式推断。

这样我知道所有字段都是相同的类型,只有在添加新字段时才需要加入模式。

当然,当添加了容易出错的新字段时,它会增加手动更新虚拟对象创建的缺点,但这是一个小缺点,因为它是我发现的唯一有效的解决方案。

答案 2 :(得分:0)

只需创建一个rdd [String],其中每个字符串都是一个json,当使用rdd作为数据帧时,使用primitiveAsString选项将所有数据类型设为String

 val binary_zip_RDD = sc.binaryFiles(batchHolder.get(i), minPartitions = 50000)
 // rdd[String]  each string is a json ,lowercased json
    val TransformedRDD = binary_zip_RDD.flatMap(kv => ZipDecompressor.Zip_open_hybrid(kv._1, kv._2, proccessingtimestamp))
 // now the schema of dataframe would be consolidate schema of all json strings
    val jsonDataframe_stream = sparkSession.read.option("primitivesAsString", true).json(TransformedRDD)

    println(jsonDataframe_stream.printSchema())


    jsonDataframe_stream.write.mode(SaveMode.Append).partitionBy(GetConstantValue.DEVICEDATE).parquet(ApplicationProperties.OUTPUT_DIRECTORY)