Як працює HashPartitioner?


82

Я прочитав документацію HashPartitioner. На жаль, нічого особливо не було пояснено, окрім викликів API. Я припускаю, що HashPartitionerрозділяє розподілений набір на основі хешу ключів. Наприклад, якщо мої дані схожі

(1,1), (1,2), (1,3), (2,1), (2,2), (2,3)

Отже, секціонер розміщував би це в різних розділах з однаковими клавішами, що потрапляли в один і той же розділ. Однак я не розумію значення аргументу конструктора

new HashPartitoner(numPartitions) //What does numPartitions do?

Для вищевказаного набору даних, як би відрізнялися результати, якби я це зробив

new HashPartitoner(1)
new HashPartitoner(2)
new HashPartitoner(10)

То як насправді HashPartitionerпрацює?

Відповіді:


162

Що ж, давайте зробимо ваш набір даних набагато цікавішим:

val rdd = sc.parallelize(for {
    x <- 1 to 3
    y <- 1 to 2
} yield (x, None), 8)

У нас є шість елементів:

rdd.count
Long = 6

немає розділу:

rdd.partitioner
Option[org.apache.spark.Partitioner] = None

і вісім розділів:

rdd.partitions.length
Int = 8

Тепер давайте визначимо маленький помічник для підрахунку кількості елементів на розділ:

import org.apache.spark.rdd.RDD

def countByPartition(rdd: RDD[(Int, None.type)]) = {
    rdd.mapPartitions(iter => Iterator(iter.length))
}

Оскільки у нас немає розділювача, наш набір даних розподіляється рівномірно між розділами ( Схема розділення за замовчуванням у Spark ):

countByPartition(rdd).collect()
Array[Int] = Array(0, 1, 1, 1, 0, 1, 1, 1)

початково-розподіл

Тепер можна перерозподілити наш набір даних:

import org.apache.spark.HashPartitioner
val rddOneP = rdd.partitionBy(new HashPartitioner(1))

Оскільки параметр, переданий для HashPartitionerвизначення кількості розділів, ми очікуємо один розділ:

rddOneP.partitions.length
Int = 1

Оскільки у нас є лише один розділ, він містить усі елементи:

countByPartition(rddOneP).collect
Array[Int] = Array(6)

хеш-секціонер-1

Зверніть увагу, що порядок значень після перетасовки є недетермінованим.

Той самий спосіб, якщо ми використовуємо HashPartitioner(2)

val rddTwoP = rdd.partitionBy(new HashPartitioner(2))

ми отримаємо 2 розділи:

rddTwoP.partitions.length
Int = 2

Оскільки rddрозділений ключовими даними більше не буде розподілятися рівномірно:

countByPartition(rddTwoP).collect()
Array[Int] = Array(2, 4)

Оскільки при наявності трьох ключів і лише двох різних значень hashCodeмода, numPartitionsтут немає нічого несподіваного:

(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))

Тільки для підтвердження вищезазначеного:

rddTwoP.mapPartitions(iter => Iterator(iter.map(_._1).toSet)).collect()
Array[scala.collection.immutable.Set[Int]] = Array(Set(2), Set(1, 3))

хеш-секціонер-2

Нарешті, HashPartitioner(7)ми отримуємо сім розділів, три непусті з 2 елементами в кожному:

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)

хеш-перегородка-7

Короткий зміст та примітки

  • HashPartitioner приймає один аргумент, який визначає кількість розділів
  • значення присвоюються розділам за допомогою hashклавіш. hashФункція може відрізнятися залежно від мови (може використовувати Scala RDD hashCode, DataSetsвикористовувати MurmurHash 3, PySpark, portable_hash).

    У такому простому випадку, коли ключ є малим цілим числом, можна припустити, що hashце ідентичність ( i = hash(i)).

    API Scala використовує nonNegativeModдля визначення розділу на основі обчисленого хешу,

  • якщо розподіл ключів неоднаковий, ви можете потрапити в ситуації, коли частина кластера не працює

  • ключі повинні бути розмитими. Ви можете перевірити мою відповідь на список як ключ для PySpark's reduceByKey, щоб прочитати про конкретні проблеми PySpark. Інша можлива проблема висвітлена в документації HashPartitioner :

    Масиви Java мають хеш-коди, які базуються на ідентифікаціях масивів, а не на їх вмісті, тому спроба розділити RDD [Array [ ]] або RDD [(Array [ ], _)] за допомогою HashPartitioner дасть несподіваний або неправильний результат.

  • У Python 3 ви повинні переконатися, що хешування є послідовним. Дивіться, що означає виняток: випадковість хешу рядків слід вимкнути за допомогою PYTHONHASHSEED у pyspark?

  • Розділювач хешів не є ні ін’єктивним, ні сюр’єктивним. Одному розділу можна призначити кілька ключів, а деякі розділи можуть залишатися порожніми.

  • Зверніть увагу, що в даний час методи, засновані на хеші, не працюють у Scala в поєднанні з визначеними REPL класами випадків ( рівність класу Case в Apache Spark ).

  • HashPartitioner(або будь-який інший Partitioner) перетасовує дані. Якщо розділення не використовується повторно між кількома операціями, це не зменшує обсяг даних, що підлягають перетасовці.


Чудово пишу, дякую. Тим НЕ менше, я помітив в зображеннях , які ви маєте (1, None)з hash(2) % Pде P є розділом. Чи не повинно бути hash(1) % P?
javamonkey79

Я використовую іскру 2.2, і partitionByв rdd немає api. є розділBy під dataframe.write, але він не приймає Partitioner як аргумент.
хакунамі

Awsome reply ... Перетасовка заснована на парціонері, хороший парціонер може зменшити кількість перемішаних даних.
Леон

1
Чудова 👌 відповідь. У мене є запитання, на яке я не отримую твердої відповіді в Інтернеті. Коли ми використовуємо df.repartition (n), тобто коли ми не вказуємо жодного стовпця як ключ, то як хеш функціонує внутрішньо, оскільки нема чого хешувати?
dsk

@dsk, якщо ви не вказали ключ, я вважаю, що переділ використовує RoundRobinPartitioning. Деякі дискусії тут .
Mike Souder

6

RDDрозподіляється, це означає, що він розділений на деяку кількість частин. Кожен із цих розділів потенційно знаходиться на іншій машині. Розділювач хешу з аргументом numPartitionsвибирає, в якому розділі розмістити пару (key, value)наступним чином:

  1. Створює саме numPartitionsрозділи.
  2. Місця (key, value)в розділі з номеромHash(key) % numPartitions

3

HashPartitioner.getPartitionМетод приймає ключ в якості аргументу і повертає індекс розділу , який ключ належить. Розділювач повинен знати, які дійсні індекси, тому він повертає числа в потрібному діапазоні. Кількість розділів визначається через numPartitionsаргумент конструктора.

Реалізація повертається приблизно key.hashCode() % numPartitions. Докладніше див. У розділі Partitioner.scala .

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.