J'ai lu sur la documentation de HashPartitioner
. Malheureusement, rien n'a été expliqué, sauf pour les appels d'API. Je suppose que HashPartitioner
partitionne le jeu distribué en fonction du hachage des clés. Par exemple si mes données sont comme
(1,1), (1,2), (1,3), (2,1), (2,2), (2,3)
Donc, le partitionneur mettrait cela dans différentes partitions avec les mêmes clés tombant dans la même partition. Cependant, je ne comprends pas la signification de l'argument du constructeur
new HashPartitoner(numPartitions) //What does numPartitions do?
Pour le jeu de données ci-dessus, en quoi les résultats seraient-ils différents si je le faisais?
new HashPartitoner(1)
new HashPartitoner(2)
new HashPartitoner(10)
Alors, comment fonctionne HashPartitioner
en réalité?
Eh bien, rendons votre jeu de données légèrement plus intéressant:
val rdd = sc.parallelize(for {
x <- 1 to 3
y <- 1 to 2
} yield (x, None), 8)
Nous avons six éléments:
rdd.count
Long = 6
pas de partitionneur:
rdd.partitioner
Option[org.Apache.spark.Partitioner] = None
et huit partitions:
rdd.partitions.length
Int = 8
Définissons maintenant un petit assistant pour compter le nombre d'éléments par partition:
import org.Apache.spark.rdd.RDD
def countByPartition(rdd: RDD[(Int, None.type)]) = {
rdd.mapPartitions(iter => Iterator(iter.length))
}
Puisque nous n'avons pas de partitionneur, notre jeu de données est distribué uniformément entre les partitions ( Schéma de partitionnement par défaut dans Spark ):
countByPartition(rdd).collect()
Array[Int] = Array(0, 1, 1, 1, 0, 1, 1, 1)
Partons maintenant notre jeu de données:
import org.Apache.spark.HashPartitioner
val rddOneP = rdd.partitionBy(new HashPartitioner(1))
Puisque le paramètre passé à HashPartitioner
définit le nombre de partitions que nous avons prévu, une partition:
rddOneP.partitions.length
Int = 1
Comme nous n'avons qu'une seule partition, elle contient tous les éléments:
countByPartition(rddOneP).collect
Array[Int] = Array(6)
Notez que l'ordre des valeurs après le remaniement n'est pas déterministe.
De la même manière si nous utilisons HashPartitioner(2)
val rddTwoP = rdd.partitionBy(new HashPartitioner(2))
nous aurons 2 partitions:
rddTwoP.partitions.length
Int = 2
Puisque rdd
est partitionné par clé, les données ne seront plus distribuées uniformément:
countByPartition(rddTwoP).collect()
Array[Int] = Array(2, 4)
Parce qu'avec avec trois clés et seulement deux valeurs différentes de hashCode
mod numPartitions
il n'y a rien d'inattendu ici:
(1 to 3).map((k: Int) => (k, k.hashCode, k.hashCode % 2))
scala.collection.immutable.IndexedSeq[(Int, Int, Int)] = Vector((1,1,1), (2,2,0), (3,3,1))
Juste pour confirmer ce qui précède:
rddTwoP.mapPartitions(iter => Iterator(iter.map(_._1).toSet)).collect()
Array[scala.collection.immutable.Set[Int]] = Array(Set(2), Set(1, 3))
Enfin, avec HashPartitioner(7)
, nous obtenons sept partitions, trois non vides avec 2 éléments chacune:
val rddSevenP = rdd.partitionBy(new HashPartitioner(7))
rddSevenP.partitions.length
Int = 7
countByPartition(rddTenP).collect()
Array[Int] = Array(0, 2, 2, 2, 0, 0, 0)
HashPartitioner
prend un seul argument qui définit le nombre de partitionsles valeurs sont assignées aux partitions en utilisant hash
of keys. La fonction hash
peut varier en fonction de la langue (Scala RDD peut utiliser hashCode
, DataSets
utiliser MurmurHash 3, PySpark, portable_hash
).
Dans un cas simple comme celui-ci, où key est un petit entier, vous pouvez supposer que hash
est une identité (i = hash(i)
).
L’API Scala utilise nonNegativeMod
pour déterminer la partition en fonction du hachage calculé,
si la distribution des clés n’est pas uniforme, vous pouvez vous retrouver dans des situations où une partie de votre cluster est inactive
les clés doivent être hashable. Vous pouvez consulter ma réponse pour ne liste en tant que clé de la clé reduction de PySpark pour en savoir plus sur les problèmes spécifiques à PySpark. Un autre problème possible est mis en évidence par documentation HashPartitioner :
Les tableaux Java ont des hashCodes basés sur les identités des tableaux plutôt que sur leur contenu. Tenter de partitionner un RDD [Array []] ou RDD [(Array [], _)] à l'aide d'un HashPartitioner produira un résultat inattendu ou incorrect.
Dans Python 3, vous devez vous assurer que le hachage est cohérent. Voir Qu'est-ce que Exception: le caractère aléatoire d'une chaîne de hachage doit être désactivé via PYTHONHASHSEED dans pyspark?
Le partitionneur de hachage n'est ni injectif ni surjectif. Plusieurs clés peuvent être attribuées à une seule partition et certaines partitions peuvent rester vides.
Veuillez noter que les méthodes actuellement basées sur le hachage ne fonctionnent pas dans Scala lorsqu'elles sont combinées avec REPL classes de cas définies ( égalité de classe de cas dans Apache Spark) ).
HashPartitioner
(ou tout autre Partitioner
) mélange les données. À moins que le partitionnement ne soit réutilisé entre plusieurs opérations, cela ne réduit pas la quantité de données à mélanger.
RDD
est distribué, cela signifie qu'il est divisé en plusieurs parties. Chacune de ces partitions est potentiellement sur une machine différente. Le partitionneur de hachage avec l'argument numPartitions
choisit sur quelle partition placer la paire (key, value)
De la manière suivante:
numPartitions
partitions.(key, value)
Dans la partition portant le numéro Hash(key) % numPartitions
La méthode HashPartitioner.getPartition
Prend comme argument une clé et renvoie l'index de la partition à laquelle appartient la clé. Le partitionneur doit savoir quels sont les index valides. Il renvoie donc les nombres dans la plage correcte. Le nombre de partitions est spécifié via l'argument de constructeur numPartitions
.
L'implémentation retourne approximativement key.hashCode() % numPartitions
. Voir Partitioner.scala pour plus de détails.