web-dev-qa-db-fra.com

Spark: quelle est la meilleure stratégie pour joindre un RDD à 2 clés avec un RDD à clé unique?

J'ai deux RDD que je veux rejoindre et ils ressemblent à ceci:

val rdd1:RDD[(T,U)]
val rdd2:RDD[((T,W), V)]

Il se trouve que les valeurs clés de rdd1 sont uniques et que les valeurs de clé de tuple de rdd2 sont uniques. Je voudrais joindre les deux ensembles de données afin d'obtenir le rdd suivant:

val rdd_joined:RDD[((T,W), (U,V))]

Quelle est la manière la plus efficace d'y parvenir? Voici quelques idées auxquelles j'ai pensé.

Option 1:

val m = rdd1.collectAsMap
val rdd_joined = rdd2.map({case ((t,w), u) => ((t,w), u, m.get(t))})

Option 2:

val distinct_w = rdd2.map({case ((t,w), u) => w}).distinct
val rdd_joined = rdd1.cartesian(distinct_w).join(rdd2)

L'option 1 collectera toutes les données à maîtriser, non? Donc, cela ne semble pas être une bonne option si rdd1 est grand (il est relativement grand dans mon cas, bien qu'un ordre de grandeur plus petit que rdd2). L'option 2 fait un produit moche distinct et cartésien, qui semble également très inefficace. Une autre possibilité qui m’est venue à l’esprit (mais qui n’a pas encore essayé) est de faire l’option 1 et de diffuser la carte, bien qu’il serait préférable de diffuser de manière "intelligente" afin que les clés de la carte soient co-localisées avec le clés de rdd2.

Quelqu'un a-t-il déjà rencontré ce genre de situation auparavant? Je serais heureux d'avoir vos pensées.

Merci!

48
RyanH

Une option consiste à effectuer une jointure de diffusion en collectant rdd1 au pilote et le diffuser à tous les mappeurs; fait correctement, cela nous permettra d'éviter un remaniement coûteux de la grande rdd2 RDD:

val rdd1 = sc.parallelize(Seq((1, "A"), (2, "B"), (3, "C")))
val rdd2 = sc.parallelize(Seq(((1, "Z"), 111), ((1, "ZZ"), 111), ((2, "Y"), 222), ((3, "X"), 333)))

val rdd1Broadcast = sc.broadcast(rdd1.collectAsMap())
val joined = rdd2.mapPartitions({ iter =>
  val m = rdd1Broadcast.value
  for {
    ((t, w), u) <- iter
    if m.contains(t)
  } yield ((t, w), (u, m.get(t).get))
}, preservesPartitioning = true)

Le preservesPartitioning = true indique Spark que cette fonction de carte ne modifie pas les clés de rdd2; cela permettra Spark pour éviter de re-partitionner rdd2 pour toutes les opérations ultérieures qui se joignent sur la base du (t, w) clé.

Cette diffusion pourrait être inefficace car elle implique un goulot d'étranglement des communications au niveau du pilote. En principe, il est possible de diffuser un RDD à un autre sans impliquer le pilote; J'en ai un prototype que j'aimerais généraliser et ajouter à Spark.

Une autre option consiste à remapper les clés de rdd2 et utilisez la méthode Spark join; cela impliquera un mélange complet de rdd2 (et éventuellement rdd1):

rdd1.join(rdd2.map {
  case ((t, w), u) => (t, (w, u))
}).map {
  case (t, (v, (w, u))) => ((t, w), (u, v))
}.collect()

Sur mon échantillon d'entrée, ces deux méthodes produisent le même résultat:

res1: Array[((Int, Java.lang.String), (Int, Java.lang.String))] = Array(((1,Z),(111,A)), ((1,ZZ),(111,A)), ((2,Y),(222,B)), ((3,X),(333,C)))

Une troisième option serait de restructurer rdd2 pour que t soit sa clé, puis effectuez la jointure ci-dessus.

59
Josh Rosen

Une autre façon de le faire consiste à créer un partitionneur personnalisé, puis à utiliser zipPartitions pour joindre vos RDD.

import org.Apache.spark.HashPartitioner

class RDD2Partitioner(partitions: Int) extends HashPartitioner(partitions) {

  override def getPartition(key: Any): Int = key match {
    case k: Tuple2[Int, String] => super.getPartition(k._1)
    case _ => super.getPartition(key)
  }

}

val numSplits = 8
val rdd1 = sc.parallelize(Seq((1, "A"), (2, "B"), (3, "C"))).partitionBy(new HashPartitioner(numSplits))
val rdd2 = sc.parallelize(Seq(((1, "Z"), 111), ((1, "ZZ"), 111), ((1, "AA"), 123), ((2, "Y"), 222), ((3, "X"), 333))).partitionBy(new RDD2Partitioner(numSplits))

val result = rdd2.zipPartitions(rdd1)(
  (iter2, iter1) => {
    val m = iter1.toMap
    for {
        ((t: Int, w), u) <- iter2
        if m.contains(t)
      } yield ((t, w), (u, m.get(t).get))
  }
).partitionBy(new HashPartitioner(numSplits))

result.glom.collect
14
Roger Hoover