web-dev-qa-db-fra.com

Calculer la taille de Spark dataframe - SizeEstimator donne des résultats inattendus

J'essaie de trouver un moyen fiable de calculer la taille (en octets) d'un Spark dataframe par programme.

La raison en est que j'aimerais avoir une méthode pour calculer un nombre "optimal" de partitions ("optimal" pourrait signifier différentes choses ici: cela pourrait signifier avoir une taille de partition optimale , ou résultant en une taille de fichier optimale lors de l'écriture sur des tables Parquet - mais les deux peuvent être supposés être une fonction linéaire de la taille de la trame de données). En d'autres termes, je voudrais appeler coalesce(n) ou repartition(n) sur la trame de données, où n n'est pas un nombre fixe mais plutôt une fonction de la taille de la trame de données.

D'autres sujets sur SO suggère d'utiliser SizeEstimator.estimate de org.Apache.spark.util pour obtenir la taille en octets de la trame de données, mais les résultats que j'obtiens sont incohérents.

Tout d'abord, je conserve ma trame de données en mémoire:

df.cache().count 

L'interface utilisateur Spark affiche une taille de 4,8 Go dans l'onglet Stockage. Ensuite, j'exécute la commande suivante pour obtenir la taille de SizeEstimator:

import org.Apache.spark.util.SizeEstimator
SizeEstimator.estimate(df)

Cela donne un résultat de 115'715'808 octets = ~ 116 Mo. Cependant, appliquer SizeEstimator à différents objets conduit à des résultats très différents. Par exemple, j'essaie de calculer la taille séparément pour chaque ligne de la trame de données et je les additionne:

df.map(row => SizeEstimator.estimate(row.asInstanceOf[ AnyRef ])).reduce(_+_)

Il en résulte une taille de 12'084'698'256 octets = ~ 12 Go. Ou, je peux essayer d'appliquer SizeEstimator à chaque partition:

df.mapPartitions(
    iterator => Seq(SizeEstimator.estimate(
        iterator.toList.map(row => row.asInstanceOf[ AnyRef ]))).toIterator
).reduce(_+_)

ce qui se traduit à nouveau par une taille différente de 10'792'965'376 octets = ~ 10,8 Go.

Je comprends qu'il y a des optimisations de mémoire/surcharge de mémoire impliquées, mais après avoir effectué ces tests, je ne vois pas comment SizeEstimator peut être utilisé pour obtenir une estimation suffisamment bonne de la taille de la trame de données (et par conséquent de la taille de la partition, ou résultant des tailles de fichier Parquet).

Quelle est la manière appropriée (le cas échéant) d'appliquer SizeEstimator afin d'obtenir une bonne estimation de la taille d'une trame de données ou de ses partitions? S'il n'y en a pas, quelle est l'approche suggérée ici?

10
hiryu

Malheureusement, je n'ai pas pu obtenir d'estimations fiables de SizeEstimator, mais j'ai pu trouver une autre stratégie - si la trame de données est mise en cache, nous pouvons extraire sa taille de queryExecution comme suit:

df.cache.foreach(_=>_)
val catalyst_plan = df.queryExecution.logical
val df_size_in_bytes = spark.sessionState.executePlan(
    catalyst_plan).optimizedPlan.stats.sizeInBytes

Pour l'exemple de trame de données, cela donne exactement 4,8 Go (ce qui correspond également à la taille du fichier lors de l'écriture sur une table Parquet non compressée).

Cela a l'inconvénient que la trame de données doit être mise en cache, mais ce n'est pas un problème dans mon cas.

9
hiryu

Mis à part l'estimateur de taille, que vous avez déjà essayé (bon aperçu).

ci-dessous est une autre option

RDDInfo[] getRDDStorageInfo()

Renvoyer des informations sur les RDD mis en cache , s'ils sont en mem ou sur les deux, combien d'espace ils prennent, etc.

en fait spark utilise ceci . Spark docs

Ci-dessous est le implémentation de spark

 /**
   * :: DeveloperApi ::
   * Return information about what RDDs are cached, if they are in mem or on disk, how much space
   * they take, etc.
   */
  @DeveloperApi
  def getRDDStorageInfo: Array[RDDInfo] = {
    getRDDStorageInfo(_ => true)
  }

  private[spark] def getRDDStorageInfo(filter: RDD[_] => Boolean): Array[RDDInfo] = {
    assertNotStopped()
    val rddInfos = persistentRdds.values.filter(filter).map(RDDInfo.fromRdd).toArray
    rddInfos.foreach { rddInfo =>
      val rddId = rddInfo.id
      val rddStorageInfo = statusStore.asOption(statusStore.rdd(rddId))
      rddInfo.numCachedPartitions = rddStorageInfo.map(_.numCachedPartitions).getOrElse(0)
      rddInfo.memSize = rddStorageInfo.map(_.memoryUsed).getOrElse(0L)
      rddInfo.diskSize = rddStorageInfo.map(_.diskUsed).getOrElse(0L)
    }
    rddInfos.filter(_.isCached)
  }

yourRDD.toDebugString de RDD l'utilise également. code ici


Note générale :

À mon avis, pour obtenir un nombre optimal d'enregistrements dans chaque partition et vérifier que votre répartition est correcte et qu'ils sont uniformément distribués, je suggère d'essayer comme ci-dessous ... et d'ajuster votre numéro de re-partition. puis mesurer la taille de la partition ... serait plus raisonnable. pour résoudre ce genre de problèmes

yourdf.rdd.mapPartitionsWithIndex{case (index,rows) => Iterator((index,rows.size))}
  .toDF("PartitionNumber","NumberOfRecordsPerPartition")
  .show

ou des fonctions spark (basées sur spark))

import org.Apache.spark.sql.functions._ 

df.withColumn("partitionId", sparkPartitionId()).groupBy("partitionId").count.show
4
Ram Ghadiyaram

SizeEstimator renvoie le nombre d'octets qu'un objet occupe sur le tas JVM. Cela inclut les objets référencés par l'objet, la taille réelle de l'objet sera presque toujours beaucoup plus petite.

Les différences de tailles que vous avez observées sont dues au fait que lorsque vous créez de nouveaux objets sur la JVM, les références prennent également de la mémoire, et cela est compté.

Découvrez les documents ici ????
https://spark.Apache.org/docs/2.2.0/api/scala/index.html#org.Apache.spark.util.SizeEstimator $

4
Steven Black