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!
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.
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