Je voudrais savoir si le foreachPartitions
entraînera de meilleures performances, en raison d'un niveau de parallélisme plus élevé, par rapport à la méthode foreach
compte tenu du cas dans lequel je traverse un RDD
afin d'effectuer quelques sommes dans une variable d'accumulateur.
foreach
et foreachPartitions
sont des actions.
Une fonction générique pour appeler des opérations avec des effets secondaires. Pour chaque élément du RDD, il appelle la fonction passée. Ceci est généralement utilisé pour manipuler des accumulateurs ou écrire dans des magasins externes.
Remarque: la modification de variables autres que les accumulateurs en dehors de la foreach()
peut entraîner un comportement non défini. Voir Comprendre les fermetures pour plus de détails.
exemple :
scala> val accum = sc.longAccumulator("My Accumulator")
accum: org.Apache.spark.util.LongAccumulator = LongAccumulator(id: 0, name: Some(My Accumulator), value: 0)
scala> sc.parallelize(Array(1, 2, 3, 4)).foreach(x => accum.add(x))
...
10/09/29 18:41:08 INFO SparkContext: Tasks finished in 0.317106 s
scala> accum.value
res2: Long = 10
Similaire à
foreach()
, mais au lieu d'appeler une fonction pour chaque élément, il l'appelle pour chaque partition. La fonction doit pouvoir accepter un itérateur. C'est plus efficace queforeach()
car cela réduit le nombre d'appels de fonction (tout commemapPartitions
()).
Utilisation d'exemples foreachPartition
:
/** * Insérer dans la base de données à l'aide de chaque partition. * * @Param sqlDatabaseConnectionString * @Param sqlTableName */ def insertToTable (sqlDatabaseConnectionString: String, sqlTableName: String): Unit = { // numPartitions = nombre de connexions DB simultanées que vous pouvez prévoir de donner datframe.repartition (numofpartitionsyouwant) val tableHeader: String = dataFrame.columns.mkString (",") dataFrame.foreachPartition {partition => // Remarque: pour chaque partition, une connexion (le meilleur moyen consiste à utiliser des pools de connexions) Val sqlExecutorConnection: Connection = DriverManager.getConnection (sqlDatabaseConnectionString) // Taille de lot de 1000 est utilisé car certaines bases de données ne peuvent pas utiliser une taille de lot supérieure à 1000 pour ex: Azure sql partition.grouped (1000) .foreach { group => val insertString: sca la.collection.mutable.StringBuilder = new scala.collection.mutable.StringBuilder () group.foreach { record => insertString.append ("('" + record.mkString (", ") +" '), ") } sqlExecutorConnection.createStatement () .executeUpdate (f" INSERT INTO [$ sqlTableName] ($ tableHeader) VALEURS " + InsertString.stripSuffix (", ")) } SqlExecutorConnection.close () // ferme la connexion afin que les connexions ne s'épuisent pas. } }
Utilisation de foreachPartition
avec sparkstreaming (dstreams) et kafka producteur
dstream.foreachRDD { rdd =>
rdd.foreachPartition { partitionOfRecords =>
// only once per partition You can safely share a thread-safe Kafka //producer instance.
val producer = createKafkaProducer()
partitionOfRecords.foreach { message =>
producer.send(message)
}
producer.close()
}
}
Remarque: Si vous voulez éviter cette façon de créer un producteur une fois par partition, mieux vaut diffuser le producteur en utilisant
sparkContext.broadcast
Puisque Kafka producteur est asynchrone et met en mémoire tampon les données avant l'envoi.
Accumulator samples snippet to play around with it ... through which you can test the performance
test ("Foreach - Spark") { import spark.implicits ._ var accum = sc.longAccumulator sc.parallelize (Seq (1,2 , 3)). Foreach (x => accum.add (x)) Assert (accum.value == 6L) } Test (" Foreach partition - Spark ") { Import spark.implicits ._ Var accum = sc.longAccumulator Sc.parallelize (Seq (1,2,3)). ForeachPartition ( x => x.foreach (accum.add (_))) assert (accum.value == 6L) }
foreachPartition
opérations sur les partitions donc évidemment ce serait mieux Edge queforeach
foreachPartition
doit être utilisé lorsque vous accédez à des ressources coûteuses telles que les connexions à la base de données ou kafka producteur, etc. qui initialiserait une par partition plutôt qu'une par élément (foreach
). En ce qui concerne les accumulateurs, vous pouvez mesurer les performances par les méthodes de test ci-dessus, qui devraient également fonctionner plus rapidement dans le cas des accumulateurs.
Voir aussi map vs mappartitions qui a un concept similaire mais ce sont des transformations.
foreach
exécute automatiquement la boucle sur de nombreux nœuds.
Cependant, vous souhaitez parfois effectuer certaines opérations sur chaque nœud. Par exemple, établissez une connexion à la base de données. Vous ne pouvez pas simplement établir une connexion et la passer dans la fonction foreach
: la connexion n'est établie que sur un seul nœud.
Ainsi, avec foreachPartition
, vous pouvez établir une connexion à la base de données sur chaque nœud avant d'exécuter la boucle.
Il n'y a vraiment pas beaucoup de différence entre foreach
et foreachPartitions
. Sous les couvertures, tout ce que foreach
fait est d'appeler le foreach
de l'itérateur en utilisant la fonction fournie. foreachPartition
vous donne juste la possibilité de faire quelque chose en dehors du bouclage de l'itérateur, généralement quelque chose de cher comme faire tourner une connexion à une base de données ou quelque chose dans ce sens. Donc, si vous n'avez rien qui puisse être fait une fois pour l'itérateur de chaque nœud et réutilisé tout au long, je suggère d'utiliser foreach
pour une clarté améliorée et une complexité réduite.
foreachPartition
ne signifie pas qu'il s'agit d'une activité par nœud, mais plutôt qu'il est exécuté pour chaque partition et il est possible que vous ayez un grand nombre de partitions par rapport au nombre de nœuds dans ce cas, vos performances peuvent être dégradées. Si vous avez l'intention de faire une activité au niveau du nœud, la solution expliquée ici peut être utile bien qu'elle ne soit pas testée par moi
foreachPartition
n'est utile que lorsque vous parcourez des données que vous agrégez par partition.
Un bon exemple est le traitement des flux de clics par utilisateur. Vous souhaitez vider votre cache de calcul chaque fois que vous avez terminé le flux d'événements d'un utilisateur, mais le conserver entre les enregistrements du même utilisateur afin de calculer certaines informations sur le comportement de l'utilisateur.