Оригінальну відповідь, що обговорює код, можна знайти нижче.
Перш за все, ви повинні розрізняти різні типи API, кожен з яких має власні міркування щодо продуктивності.
RDD API
(чисті структури Python з оркестрованістю на базі JVM)
Це компонент, на який найбільше вплине ефективність коду Python та деталі реалізації PySpark. Хоча ефективність Python навряд чи буде проблемою, є хоча б мало факторів, які слід врахувати:
- Накладні витрати JVM-зв'язку. Практично всі дані, які надходять до виконавця Python і повинні передаватися через сокет і працівника JVM. Хоча це відносно ефективне місцеве спілкування, воно все ще не є безкоштовним.
Виконавці на основі процесів (Python) проти потоків (одиночні множинні JVM) виконавці (Scala). Кожен виконавець Python працює у своєму процесі. Як побічний ефект, він забезпечує більш сильну ізоляцію, ніж його аналог JVM, і деякий контроль над життєвим циклом виконавця, але потенційно значно більший об'єм пам'яті:
- слід пам'яті перекладача
- слід завантажених бібліотек
- менш ефективне мовлення (кожен процес вимагає власної копії трансляції)
Виконання самого коду Python. Взагалі Scala швидше, ніж Python, але вона буде змінюватися залежно від завдання. Крім того, у вас є кілька варіантів, включаючи JIT, такі як Numba , розширення C ( Cython ) або спеціалізовані бібліотеки на зразок Theano . Нарешті, якщо ви не використовуєте ML / MLlib (або просто NumPy стек) , подумайте про використання PyPy в якості альтернативного інтерпретатора. Див. SPARK-3094 .
- Конфігурація PySpark забезпечує
spark.python.worker.reuse
опцію, яку можна використовувати для вибору між розгортанням процесу Python для кожної задачі та повторним використанням існуючого процесу. Останній варіант здається корисним, щоб уникнути дорогого вивезення сміття (це більше враження, ніж результату систематичних тестів), тоді як перший варіант (за замовчуванням) є оптимальним у випадку дорогих трансляцій та імпорту.
- Підрахунок посилань, який використовується як метод збору сміття першого рядка в CPython, добре працює з типовими робочими навантаженнями Spark (потокова обробка, відсутність еталонних циклів) і зменшує ризик тривалих пауз GC.
MLlib
(змішане виконання Python та JVM)
Основні міркування майже такі ж, як і раніше, з кількома додатковими питаннями. Хоча основні структури, що використовуються з MLlib, є простими об'єктами RDD Python, всі алгоритми виконуються безпосередньо за допомогою Scala.
Це означає додаткові витрати на перетворення об’єктів Python в об’єкти Scala та навпаки, збільшення використання пам'яті та деякі додаткові обмеження, які ми покриємо пізніше.
На даний момент (Spark 2.x) API на основі RDD знаходиться в режимі обслуговування та планується видалити в Spark 3.0 .
API DataFrame та Spark ML
(Виконання JVM з кодом Python обмежено драйвером)
Це, мабуть, найкращий вибір для стандартних завдань з обробки даних. Оскільки код Python здебільшого обмежений логічними операціями високого рівня на драйвері, між Python та Scala не повинно бути різниці в продуктивності.
Єдиним винятком є використання строкових UDF-файлів Python, які значно менш ефективні, ніж їх еквіваленти Scala. Незважаючи на те, що є певні шанси на вдосконалення (істотний розвиток у Spark 2.0.0), найбільше обмеження - це повний перехід між внутрішнім представництвом (JVM) та інтерпретатором Python. Якщо можливо, вам слід віддати перевагу композиції вбудованих виразів ( наприклад . Поведінка Python UDF була покращена в Spark 2.0.0, але вона все ще є неоптимальною порівняно з нативним виконанням.
Це може покращитись у майбутньому, значно покращившись із впровадженням векторизованих UDF (SPARK-21190 та подальші розширення) , який використовує Arrow Streaming для ефективного обміну даними з десеріалізацією нульової копії. Для більшості програм їх вторинні накладні витрати можуть бути просто проігноровані.
Також не забудьте уникнути зайвих передач даних між DataFrames
та RDDs
. Для цього потрібна дорога серіалізація та десеріалізація, не кажучи вже про передачу даних до і з інтерпретатора Python.
Варто зазначити, що виклики Py4J мають досить високу затримку. Сюди входять прості дзвінки, такі як:
from pyspark.sql.functions import col
col("foo")
Зазвичай це не має значення (накладні витрати постійні і не залежать від кількості даних), але у випадку з м'якими програмами в реальному часі ви можете розглянути можливість кешування / повторного використання обгортки Java.
Набори даних GraphX та Spark
Як і зараз (Spark 1.6 2.1) жоден не надає API PySpark, тому можна сказати, що PySpark нескінченно гірший, ніж Scala.
GraphX
На практиці розробка GraphX майже повністю припинилася, і проект наразі перебуває в режимі технічного обслуговування, а відповідні квитки JIRA закриті, оскільки не виправлять . GraphFramesБібліотека надає альтернативну бібліотеку обробки графіків із прив'язкою Python.
Набір даних
Суб'єктивно кажучи, не існує багато місця для статичного набору Datasets
в Python, і навіть якщо поточна реалізація Scala надто спрощена і не дає тих же переваг продуктивності, що й DataFrame
.
Потокове
З того, що я бачив до цього часу, я настійно рекомендую використовувати Scala над Python. Це може змінитися в майбутньому, якщо PySpark отримає підтримку структурованих потоків, але зараз API Scala здається набагато більш надійним, всеосяжним та ефективним. Мій досвід досить обмежений.
Структурована трансляція в Spark 2.x, здається, зменшує розрив між мовами, але поки що це ще в перші дні. Тим не менш, API на основі RDD вже посилається на "застаріле потокове" в Документації Databricks (дата доступу 2017-03-03), тому розумно очікувати подальших зусиль щодо об'єднання.
Невиконання міркувань
Паритет функції
Не всі функції іскри відкриваються через PySpark API. Не забудьте перевірити, чи потрібні вам частини вже реалізовані, і спробуйте зрозуміти можливі обмеження.
Це особливо важливо, коли ви використовуєте MLlib та подібні змішані контексти (див. Виклик функції Java / Scala із завдання ). Для справедливості деякі частини API PySpark, наприклад mllib.linalg
, надають більш вичерпний набір методів, ніж Scala.
Дизайн API
API PySpark уважно відображає його аналог Scala і як такий не є точно Pythonic. Це означає, що карту між мовами досить легко, але в той же час зрозуміти код Python може бути значно важче.
Складна архітектура
Потік даних PySpark порівняно складний у порівнянні з чистим виконанням JVM. Набагато складніше міркувати про програми PySpark або налагодження. Більше того, принаймні базове розуміння Scala та JVM взагалі, як правило, необхідно мати.
Іскра 2.x і більше
Постійний перехід до Dataset
API, заморожений API RDD приносить і можливості, і проблеми для користувачів Python. Хоча частини API високого рівня набагато простіше експонувати в Python, проте більш досконалі функції майже неможливо використовувати безпосередньо .
Більше того, нативні функції Python продовжують залишатися громадянами другого класу у світі SQL. Сподіваємось, це в майбутньому покращиться з серіалізацією Apache Arrow ( цільові дані про поточні зусилля,collection
але serde UDF - це довгострокова мета ).
Для проектів, сильно залежних від кодової бази Python, чисті альтернативи Python (як Dask або Ray ) можуть бути цікавою альтернативою.
Це не повинно бути один проти іншого
API Spark DataFrame (SQL, набір даних) забезпечує елегантний спосіб інтеграції коду Scala / Java в додаток PySpark. Ви можете використовувати DataFrames
для викриття даних до нативного коду JVM та для читання результатів. Я пояснив деякі варіанти десь в іншому місці, і ви можете знайти робочий приклад зворотного переходу Python-Scala в розділі Як використовувати клас Scala всередині Pyspark .
Його можна додатково доповнити, ввівши визначені користувачем типи (див. Як визначити схему для користувацького типу в Spark SQL? ).
Що не так з кодом, наведеним у питанні
(Відмова: Точка зору Pythonista. Швидше за все, я пропустив деякі хитрощі Scala)
Перш за все, у вашому коді є одна частина, яка взагалі не має сенсу. Якщо у вас вже є (key, value)
пари, створені за допомогою, zipWithIndex
або enumerate
який сенс у створенні рядка, щоб розділити його відразу після цього? flatMap
не працює рекурсивно, так що ви можете просто отримати кортежі та пропустити наступні дії map
.
Інша частина, яку я вважаю проблематичною, - це reduceByKey
. Взагалі кажучи, reduceByKey
корисно, якщо застосування сукупної функції може зменшити кількість даних, які підлягають переміщенню. Оскільки ви просто з'єднуєте рядки, тут нічого не виграти. Ігноруючи матеріали низького рівня, як кількість посилань, обсяг даних, який ви повинні передати, точно такий же, як і для groupByKey
.
Зазвичай я б на цьому не зупинявся, але, наскільки я можу сказати, це вузьке місце у вашому коді Scala. Об'єднання рядків у JVM - досить дорога операція (див. Наприклад: Чи конкатенація рядків у шкалі така ж затратна, як і у Java? ). Це означає, що щось подібне, _.reduceByKey((v1: String, v2: String) => v1 + ',' + v2)
що еквівалентно input4.reduceByKey(valsConcat)
вашому коду, не є хорошою ідеєю.
Якщо ви хочете уникнути , groupByKey
ви можете спробувати використовувати aggregateByKey
з StringBuilder
. Щось подібне до цього має зробити трюк:
rdd.aggregateByKey(new StringBuilder)(
(acc, e) => {
if(!acc.isEmpty) acc.append(",").append(e)
else acc.append(e)
},
(acc1, acc2) => {
if(acc1.isEmpty | acc2.isEmpty) acc1.addString(acc2)
else acc1.append(",").addString(acc2)
}
)
але я сумніваюсь, що це вартує суєти.
Маючи на увазі вищесказане, я переписав ваш код наступним чином:
Scala :
val input = sc.textFile("train.csv", 6).mapPartitionsWithIndex{
(idx, iter) => if (idx == 0) iter.drop(1) else iter
}
val pairs = input.flatMap(line => line.split(",").zipWithIndex.map{
case ("true", i) => (i, "1")
case ("false", i) => (i, "0")
case p => p.swap
})
val result = pairs.groupByKey.map{
case (k, vals) => {
val valsString = vals.mkString(",")
s"$k,$valsString"
}
}
result.saveAsTextFile("scalaout")
Пітон :
def drop_first_line(index, itr):
if index == 0:
return iter(list(itr)[1:])
else:
return itr
def separate_cols(line):
line = line.replace('true', '1').replace('false', '0')
vals = line.split(',')
for (i, x) in enumerate(vals):
yield (i, x)
input = (sc
.textFile('train.csv', minPartitions=6)
.mapPartitionsWithIndex(drop_first_line))
pairs = input.flatMap(separate_cols)
result = (pairs
.groupByKey()
.map(lambda kv: "{0},{1}".format(kv[0], ",".join(kv[1]))))
result.saveAsTextFile("pythonout")
Результати
У local[6]
режимі (Intel (R) Xeon (R) процесор E3-1245 V2 @ 3,40 ГГц) з 4 Гб пам'яті на одного виконавця потрібно (n = 3):
- Scala - означає: 250,00с, стдев: 12,49
- Python - означає: 246,66s, stdev: 1,15
Я майже впевнений, що більшість цього часу витрачається на перемішування, серіалізацію, десеріалізацію та інші вторинні завдання. Для задоволення, ось наївний однопотоковий код в Python, який виконує те саме завдання на цій машині менше ніж за хвилину:
def go():
with open("train.csv") as fr:
lines = [
line.replace('true', '1').replace('false', '0').split(",")
for line in fr]
return zip(*lines[1:])