web-dev-qa-db-fra.com

Comment gérer le changement de schéma de parquet dans Apache Spark

J'ai rencontré un problème où j'ai des données de parquet sous forme de morceaux quotidiens dans S3 (sous la forme de s3://bucketName/prefix/YYYY/MM/DD/) mais je ne peux pas lire les données dans AWS EMR Spark à partir de dates différentes car certains types de colonne ne correspondent pas et j'ai l'une des nombreuses exceptions. , par exemple:

Java.lang.ClassCastException: optional binary element (UTF8) is not a group

apparaît lorsque, dans certains fichiers, un type de tableau a une valeur, mais la même colonne peut avoir une valeur null dans d'autres fichiers, qui sont ensuite déduits sous forme de types String.

ou

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)

J'ai des données brutes au format JSON au format S3 et mon objectif initial était de créer un travail automatique qui démarre un cluster EMR, lit les données JSON de la date précédente et les écrit simplement en tant que parquets dans S3.

Les données JSON sont également divisées en dates, c’est-à-dire que les clés ont des préfixes de date. Lire JSON fonctionne bien. Le schéma est déduit des données, quelle que soit la quantité de données en cours de lecture.

Mais le problème se pose lorsque les fichiers de parquet sont écrits. Si j'ai bien compris, lorsque j'écris du parquet avec des fichiers de métadonnées, ces fichiers contiennent le schéma de toutes les parties/partitions des fichiers de parquet. Ce qui, à mon avis, peut aussi être associé à différents schémas. Lorsque je désactivais l'écriture des métadonnées, il était dit que Spark inférait le schéma entier du premier fichier dans le chemin Parquet donné et présumait qu'il restait le même à travers d'autres fichiers.

Lorsque certaines colonnes, qui doivent être de type double, n'ont que des valeurs entières pour un jour donné, leur lecture à partir de JSON (ces nombres étant des entiers, sans virgule flottante) fait penser à Spark qu'il s'agit d'une colonne de type long. Même si je peux doubler ces colonnes avant d'écrire les fichiers Parquet, cela n'est toujours pas correct car le schéma peut changer, de nouvelles colonnes peuvent être ajoutées et le suivi est impossible. 

J'ai vu certaines personnes avoir les mêmes problèmes, mais je n'ai pas encore trouvé de solution satisfaisante. 

Quelles sont les meilleures pratiques ou solutions pour cela? 

15
V. Samma

Au fur et à mesure que je lis les données quotidiennes dans JSON et écris dans Parquet dans des dossiers S3 quotidiens, sans spécifier mon propre schéma lors de la lecture de JSON ou de la conversion de colonnes sujettes à erreur en types corrects avant l'écriture dans Parquet, Spark peut déduire différents schémas pour différents jours des données en fonction des valeurs des instances de données et écrire des fichiers Parquet avec des schémas en conflit.

Ce n'est peut-être pas la solution parfaite, mais le seul moyen que j'ai trouvé pour résoudre mon problème avec un schéma en évolution est le suivant:

Avant mon travail quotidien (plus particulièrement la nuit) cron de traitement par lots des données du jour précédent, je crée un objet factice avec des valeurs généralement vides. 

Je m'assure que l'ID est reconnaissable, par exemple, étant donné que les données réelles ont un ID unique, j'ajoute une chaîne "fictive" en tant qu'ID à l'objet de données fictif. 

Ensuite, je donnerai les valeurs attendues pour les propriétés avec des types sujets aux erreurs, par exemple, je donnerai des valeurs flottantes/doubles non nulles. Ainsi, lors du marshalling en JSON, ils auront certainement un séparateur décimal, par exemple "0,2" au lieu de "0" ( Lors du marshalling en JSON, les doubles/floats avec des valeurs 0 sont indiqués par "0" et non par "0.0").

Les chaînes, les booléens et les entiers fonctionnent bien, mais en plus des doubles/floats, je devais aussi instancier des tableaux en tant que tableaux vides et des objets d'autres classes/structures avec des objets vides correspondants pour qu'ils ne soient pas "nuls", comme le lit Spark. null-s en tant que chaînes. 


Ensuite, si tous les champs nécessaires sont remplis, je vais transférer l'objet en JSON et écrire les fichiers sur S3.

Ensuite, j'utilisais ces fichiers dans mon script de traitement par lots Scala pour les lire, enregistrer le schéma dans une variable et donner ce schéma en tant que paramètre lorsque je lisais les données JSON réelles afin d'éviter que Spark ne fasse ses propres inférations de schéma. 

De cette façon, je sais que tous les champs sont toujours du même type et la fusion de schéma n’est nécessaire que pour joindre des schémas lorsque de nouveaux champs sont ajoutés.

Bien sûr, cela ajoute un inconvénient à la mise à jour manuelle de la création d'objet factice lorsque de nouveaux champs de types sujets à erreurs sont ajoutés, mais c'est actuellement un petit inconvénient car c'est la seule solution que j'ai trouvée qui fonctionne.

6
V. Samma

Ce sont les options que j'utilise pour écrire du parquet sur S3; désactiver la fusion de schéma augmente les performances d'écriture différée - cela peut également résoudre votre problème

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

Créez juste un rdd [String] où chaque chaîne est un json. Lorsque vous créez le rdd en tant que dataframe, utilisez l’option primitiveAsString pour transformer tous les types de données en chaîne

 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)
0
siva krishna