web-dev-qa-db-fra.com

Comment contrôlez-vous la taille du fichier de sortie?

Dans spark, quel est le meilleur moyen de contrôler la taille du fichier de sortie? Par exemple, dans log4j, nous pouvons spécifier la taille de fichier maximale, après quoi le fichier pivote. 

Je cherche une solution similaire pour le dossier de parquet. Existe-t-il une option de taille de fichier maximale disponible lors de l'écriture d'un fichier?

J'ai peu de solutions de contournement, mais aucune n'est bonne. Si je veux limiter les fichiers à 64 Mo, une option consiste à repartitionner les données et à écrire dans un emplacement temporaire. Et puis fusionnez les fichiers ensemble en utilisant la taille du fichier dans l'emplacement temporaire. Mais obtenir la taille de fichier correcte est difficile.

7
user447359

Il est impossible pour Spark de contrôler la taille des fichiers Parquet, car le DataFrame en mémoire doit être encodé et compressé avant l'écriture sur les disques. Avant la fin de ce processus, il n'existe aucun moyen d'estimer la taille réelle du fichier sur le disque.

Donc ma solution est:

  • Écrire le DataFrame sur HDFS, df.write.parquet(path)
  • Obtenir la taille du répertoire et calculer le nombre de fichiers

    val fs = FileSystem.get(sc.hadoopConfiguration)
    val dirSize = fs.getContentSummary(path).getLength
    val fileNum = dirSize/(512 * 1024 * 1024)  // let's say 512 MB per file
    
  • Lire le répertoire et ré-écrire sur HDFS

    val df = sqlContext.read.parquet(path)
    df.coalesce(fileNum).write.parquet(another_path)
    

    Ne réutilisez PAS la df originale, sinon cela déclenchera votre travail deux fois. 

  • Supprimer l'ancien répertoire et renommer le nouveau répertoire

    fs.delete(new Path(path), true)
    fs.rename(new Path(newPath), new Path(path))
    

Cette solution présente l'inconvénient de devoir écrire les données deux fois, ce qui double les entrées/sorties disque, mais c'est la seule solution pour l'instant. 

23
soulmachine

Ok, voici ma méthode perfectionnée pour la prise en compte de la taille du fichier cible, de l'utilisation de la mémoire et du temps d'exécution. Ces fichiers incluent également la compression instantanée et le codage par dictionnaire.

Mon HDFS Blocksize est 128 Mo (128 * 1024 * 1024):

<property>
    <name>dfs.blocksize</name>
    <value>134217728</value>
</property>

Voici mes derniers fichiers de parquet qui sont tous très proches de la taille du bloc hdfs.

133916650 2018-07-06 07:05 /year=2018/month=01/HoldingDetail_201801_0001.parquet
133459404 2018-07-06 07:05 /year=2018/month=01/HoldingDetail_201801_0002.parquet
133668445 2018-07-06 07:05 /year=2018/month=01/HoldingDetail_201801_0003.parquet
134004329 2018-07-06 07:06 /year=2018/month=01/HoldingDetail_201801_0004.parquet
134015650 2018-07-06 07:06 /year=2018/month=01/HoldingDetail_201801_0005.parquet
132053162 2018-07-06 07:06 /year=2018/month=01/HoldingDetail_201801_0006.parquet
132917851 2018-07-06 07:06 /year=2018/month=01/HoldingDetail_201801_0007.parquet
122594040 2018-07-06 07:06 /year=2018/month=01/HoldingDetail_201801_0008.parquet

C'est comme ça que j'ai fait ça ..

R. Créez un nombre approximatif de rangées pour générer un ensemble de PETITS dossiers de parquet de l’ordre de 10 Mo environ. Dans mon cas, j'ai choisi 200 000 disques. De nombreux fichiers de parquet plus petits occupent moins d'espace qu'un fichier de parquet volumineux, car l'encodage par dictionnaire et d'autres techniques de compression est abandonné si les données d'un fichier sont plus variées. Écrire environ 10 Mo à la fois libère également de la mémoire.

Vos fichiers ressembleront à ceci:

07916650 2018-07-06 07:05 /year=2018/month=01/HoldingDetail_201801_0001.parquet
12259404 2018-07-06 07:05 /year=2018/month=01/HoldingDetail_201801_0002.parquet
11368445 2018-07-06 07:05 /year=2018/month=01/HoldingDetail_201801_0003.parquet
07044329 2018-07-06 07:06 /year=2018/month=01/HoldingDetail_201801_0004.parquet
13145650 2018-07-06 07:06 /year=2018/month=01/HoldingDetail_201801_0005.parquet
08534162 2018-07-06 07:06 /year=2018/month=01/HoldingDetail_201801_0006.parquet
12178451 2018-07-06 07:06 /year=2018/month=01/HoldingDetail_201801_0007.parquet
11940440 2018-07-06 07:06 /year=2018/month=01/HoldingDetail_201801_0008.parquet
09166540 2018-07-06 07:05 /year=2018/month=01/HoldingDetail_201801_0009.parquet
12594044 2018-07-06 07:05 /year=2018/month=01/HoldingDetail_201801_0010.parquet
11684245 2018-07-06 07:05 /year=2018/month=01/HoldingDetail_201801_0011.parquet
07043129 2018-07-06 07:06 /year=2018/month=01/HoldingDetail_201801_0012.parquet
13153650 2018-07-06 07:06 /year=2018/month=01/HoldingDetail_201801_0013.parquet
08533162 2018-07-06 07:06 /year=2018/month=01/HoldingDetail_201801_0014.parquet
12137851 2018-07-06 07:06 /year=2018/month=01/HoldingDetail_201801_0015.parquet
11943040 2018-07-06 07:06 /year=2018/month=01/HoldingDetail_201801_0016.parquet

B. Créez une liste de tous vos plus petits fichiers de parquet avec des tailles de fichier ajoutées ne dépassant pas la taille de bloc HDFS. Dans l'exemple ci-dessus:

/year=2018/month=01/HoldingDetail_201801_0001.parquet
to
/year=2018/month=01/HoldingDetail_201801_0012.parquet
plus
/year=2018/month=01/HoldingDetail_201801_0014.parquet

Prenez 133,408,651 octets.

C. Ouvrez un nouveau fichier appelé HoldingDetail_201801_temp.parquet

Lisez tous les fichiers les plus petits de votre liste, l'un après l'autre, et écrivez-les dans le fichier temporaire en tant que parquet ROW GROUP. Il est très important d'écrire chaque fichier en tant que groupe de lignes, ce qui préserve le codage de la compression et garantit que le nombre d'octets (moins les métadonnées du schéma) écrits sera identique à la taille du fichier d'origine.

Supprimez tous les fichiers plus petits de la liste . Renommez le fichier temporaire en HoldingDetail_201801_0001.parquet.

Répétez les étapes B et C pour les fichiers plus petits restants afin de créer * _0002.parquet, * _0003.parquet, * _0004.parquet, etc., qui seront des fichiers cible dont la taille est inférieure à la taille de bloc hdfs.

(J'ajoute également une vérification que si la somme des tailles de fichiers> 0,95 * dfs.blocksize, allez simplement de l'avant et fusionnez les fichiers trouvés)

1
David Lee

Comme d'autres l'ont mentionné, vous ne pouvez pas atteindre explicitement une taille cible par fichier. Cependant, vous pouvez obtenir que tous vos fichiers de sortie aient à peu près le même nombre de lignes. Si vous savez en moyenne à quoi ressemble votre taux de compression, une répartition uniforme des lignes entre les fichiers de sortie, jusqu'à un max_rows, vous permettra d'obtenir des tailles cohérentes d'environ votre cible.

C'est plus facile à dire qu'à faire si vous faites une partition avant d'écrire. Voici quelques pseudocodes pour savoir comment procéder:

-- #3 distribute partitionC's rows based on partitions plus random integer that pertains to file number
select * from dataframe_table as t4
inner join

    -- #2 calculate the number of output files per partition
    ((select t1.partitionA, t1.partitionB, cast(t2.partition_num_rows / max_rows as int) + 1 as partition_num_files from dataframe_table) as t1
        inner join 

        -- #1 determine number of rows in output partition
        (select partitionA, partitionB, count(*) as partition_num_rows from dataframe_table group by (partitionA, partitionB)) as t2
        on t1.partitionA = t2.partitionA and t1.partitionB = t2.partitionB) as t3

on t3.partitionA = t4.partitionA and t3.partitionB=t4.partitionB
distribute by (t4.partitionA, t4.partitionC, floor(Rand() * t3.partition_num_files)) sort by (partitionC, sortfield)

J'ai inclus une sorte sur la partition ici car, dans notre cas d'utilisation, cela améliore considérablement la compression tout en n'impactant que très peu les performances. 

Et si vos résultats des étapes 1 et 2 sont suffisamment petits, Spark pourra peut-être diffuser les rejoindre pour les accélérer.

1
MrChrisRodriguez

Voici ma solution et cela fonctionne bien pour moi.

val repartition_num = 20  
val hqc = new org.Apache.spark.sql.Hive.HiveContext(sc)
val t1 = hqc.sql("select * from customer")

// 20 parquet files will be generated in hdfs dir
// JUST control your file with partition number
t1.repartition(repartition_num ).saveAsParquetFile(parquet_dir)

Et voici le résultat:

> hadoop fs -ls /tpch-parquet/customer/*.parquet  | wc -l
20
0
Jia