Die ursprüngliche Antwort zum Code finden Sie unten.
Zunächst müssen Sie zwischen verschiedenen API-Typen mit jeweils eigenen Leistungsaspekten unterscheiden.
RDD-API
(reine Python-Strukturen mit JVM-basierter Orchestrierung)
Dies ist die Komponente, die am stärksten von der Leistung des Python-Codes und den Details der PySpark-Implementierung betroffen ist. Während es unwahrscheinlich ist, dass die Python-Leistung ein Problem darstellt, müssen Sie zumindest einige Faktoren berücksichtigen:
- Overhead der JVM-Kommunikation. Praktisch alle Daten, die zu und von Python Executor kommen, müssen über einen Socket und einen JVM-Worker übertragen werden. Obwohl dies eine relativ effiziente lokale Kommunikation ist, ist sie immer noch nicht kostenlos.
Prozessbasierte Executoren (Python) versus threadbasierte Executoren (einzelne JVM-Threads mit mehreren Threads) (Scala). Jeder Python-Executor wird in einem eigenen Prozess ausgeführt. Als Nebeneffekt bietet es eine stärkere Isolation als sein JVM-Gegenstück und eine gewisse Kontrolle über den Executor-Lebenszyklus, jedoch möglicherweise eine erheblich höhere Speichernutzung:
- Speicherbedarf des Interpreters
- Footprint der geladenen Bibliotheken
- weniger effizienter Rundfunk (jeder Prozess erfordert eine eigene Kopie eines Rundfunks)
Leistung des Python-Codes selbst. Im Allgemeinen ist Scala schneller als Python, variiert jedoch von Aufgabe zu Aufgabe. Darüber hinaus haben Sie mehrere Optionen, einschließlich JITs wie Numba , C-Erweiterungen ( Cython ) oder Spezialbibliotheken wie Theano . Schließlich , wenn Sie verwenden ML / MLlib (oder einfach NumPy Stack) nicht , prüfen , mit PyPy als Alternative Dolmetscher. Siehe SPARK-3094 .
- Die PySpark-Konfiguration bietet die
spark.python.worker.reuse
Option, mit der Sie zwischen dem Verzweigen des Python-Prozesses für jede Aufgabe und der Wiederverwendung eines vorhandenen Prozesses wählen können. Die letztere Option scheint nützlich zu sein, um eine teure Speicherbereinigung zu vermeiden (dies ist eher ein Eindruck als ein Ergebnis systematischer Tests), während die erstere (Standard) für teure Sendungen und Importe optimal ist.
- Die Referenzzählung, die in CPython als erste Garbage Collection-Methode verwendet wird, funktioniert recht gut mit typischen Spark-Workloads (Stream-ähnliche Verarbeitung, keine Referenzzyklen) und verringert das Risiko langer GC-Pausen.
MLlib
(gemischte Python- und JVM-Ausführung)
Grundlegende Überlegungen sind mit einigen zusätzlichen Problemen ziemlich identisch. Während mit MLlib verwendete Grundstrukturen einfache Python-RDD-Objekte sind, werden alle Algorithmen direkt mit Scala ausgeführt.
Dies bedeutet zusätzliche Kosten für die Konvertierung von Python-Objekten in Scala-Objekte und umgekehrt, eine erhöhte Speichernutzung und einige zusätzliche Einschränkungen, die wir später behandeln werden.
Ab sofort (Spark 2.x) befindet sich die RDD-basierte API in einem Wartungsmodus und soll in Spark 3.0 entfernt werden .
DataFrame API und Spark ML
(JVM-Ausführung mit auf den Treiber beschränktem Python-Code)
Dies ist wahrscheinlich die beste Wahl für Standarddatenverarbeitungsaufgaben. Da Python-Code hauptsächlich auf logische Operationen auf hoher Ebene des Treibers beschränkt ist, sollte es keinen Leistungsunterschied zwischen Python und Scala geben.
Eine einzige Ausnahme ist die Verwendung zeilenweiser Python-UDFs, die erheblich weniger effizient sind als ihre Scala-Entsprechungen. Zwar gibt es einige Verbesserungsmöglichkeiten (Spark 2.0.0 wurde erheblich weiterentwickelt), die größte Einschränkung besteht jedoch in der vollständigen Hin- und Rückfahrt zwischen der internen Darstellung (JVM) und dem Python-Interpreter. Wenn möglich, sollten Sie eine Komposition integrierter Ausdrücke bevorzugen ( Beispiel : Das Verhalten von Python UDF wurde in Spark 2.0.0 verbessert, ist jedoch im Vergleich zur nativen Ausführung immer noch nicht optimal.
Dies könnte sich in Zukunft verbessern, da die vektorisierten UDFs (SPARK-21190 und weitere Erweiterungen) eingeführt wurden , die Arrow Streaming für einen effizienten Datenaustausch mit Null-Kopien-Deserialisierung verwenden. Bei den meisten Anwendungen können die sekundären Gemeinkosten einfach ignoriert werden.
Vermeiden Sie außerdem unnötige Datenübertragungen zwischen DataFrames
und RDDs
. Dies erfordert eine teure Serialisierung und Deserialisierung, ganz zu schweigen von der Datenübertragung zum und vom Python-Interpreter.
Es ist erwähnenswert, dass Py4J-Anrufe eine ziemlich hohe Latenz haben. Dies beinhaltet einfache Anrufe wie:
from pyspark.sql.functions import col
col("foo")
Normalerweise sollte es keine Rolle spielen (der Overhead ist konstant und hängt nicht von der Datenmenge ab), aber bei weichen Echtzeitanwendungen können Sie das Zwischenspeichern / Wiederverwenden von Java-Wrappern in Betracht ziehen.
GraphX- und Spark-DataSets
Derzeit (Spark 1.6 2.1) bietet keiner die PySpark-API, sodass Sie sagen können, dass PySpark unendlich schlechter ist als Scala.
GraphX
In der Praxis wurde die GraphX-Entwicklung fast vollständig gestoppt und das Projekt befindet sich derzeit im Wartungsmodus. Die zugehörigen JIRA-Tickets wurden geschlossen, da dies nicht behoben werden kann . Die GraphFrames- Bibliothek bietet eine alternative Grafikverarbeitungsbibliothek mit Python-Bindungen.
Datensatz
Subjektiv gesehen gibt es Datasets
in Python nicht viel Platz für statische Eingaben, und selbst wenn es die aktuelle Scala-Implementierung gab, ist sie zu simpel und bietet nicht die gleichen Leistungsvorteile wie DataFrame
.
Streaming
Nach dem, was ich bisher gesehen habe, würde ich dringend empfehlen, Scala über Python zu verwenden. Es kann sich in Zukunft ändern, wenn PySpark Unterstützung für strukturierte Streams erhält, aber derzeit scheint die Scala-API viel robuster, umfassender und effizienter zu sein. Meine Erfahrung ist ziemlich begrenzt.
Strukturiertes Streaming in Spark 2.x scheint die Lücke zwischen den Sprachen zu verringern, befindet sich jedoch noch in den Anfängen. Trotzdem wird die RDD-basierte API bereits in der Databricks-Dokumentation (Datum des Zugriffs 2017-03-03) als "Legacy-Streaming" bezeichnet, sodass mit weiteren Vereinigungsbemühungen zu rechnen ist.
Überlegungen zur Nichterfüllung
Feature-Parität
Nicht alle Spark-Funktionen werden über die PySpark-API verfügbar gemacht. Überprüfen Sie unbedingt, ob die benötigten Teile bereits implementiert sind, und versuchen Sie, mögliche Einschränkungen zu verstehen.
Dies ist besonders wichtig, wenn Sie MLlib und ähnliche gemischte Kontexte verwenden (siehe Aufrufen der Java / Scala-Funktion von einer Aufgabe aus ). Um fair zu sein mllib.linalg
, bieten einige Teile der PySpark-API eine umfassendere Reihe von Methoden als Scala.
API-Design
Die PySpark-API spiegelt genau das Scala-Gegenstück wider und ist als solche nicht genau Pythonic. Dies bedeutet, dass es ziemlich einfach ist, zwischen Sprachen zuzuordnen, aber gleichzeitig kann es erheblich schwieriger sein, Python-Code zu verstehen.
Komplexe Architektur
Der PySpark-Datenfluss ist im Vergleich zur reinen JVM-Ausführung relativ komplex. Es ist viel schwieriger, über PySpark-Programme oder Debugging nachzudenken. Darüber hinaus ist zumindest ein grundlegendes Verständnis von Scala und JVM im Allgemeinen ein Muss.
Spark 2.x und darüber hinaus
Die fortlaufende Umstellung auf Dataset
API mit eingefrorener RDD-API bietet Python-Benutzern sowohl Chancen als auch Herausforderungen. Während übergeordnete Teile der API in Python viel einfacher verfügbar zu machen sind, können die erweiterten Funktionen kaum direkt verwendet werden .
Darüber hinaus sind native Python-Funktionen weiterhin Bürger zweiter Klasse in der SQL-Welt. Hoffentlich wird sich dies in Zukunft durch die Apache Arrow-Serialisierung verbessern ( aktuelle Bemühungen zielen auf Daten ab,collection
aber UDF-Serde ist ein langfristiges Ziel ).
Für Projekte, die stark von der Python-Codebasis abhängen, könnten reine Python-Alternativen (wie Dask oder Ray ) eine interessante Alternative sein.
Es muss nicht eins gegen das andere sein
Die Spark DataFrame-API (SQL, Dataset) bietet eine elegante Möglichkeit, Scala / Java-Code in die PySpark-Anwendung zu integrieren. Sie können DataFrames
Daten einem nativen JVM-Code aussetzen und die Ergebnisse zurücklesen. Ich habe einige Optionen an einer anderen Stelle erläutert. Ein funktionierendes Beispiel für eine Python-Scala-Rundreise finden Sie unter Verwenden einer Scala-Klasse in Pyspark .
Es kann durch die Einführung benutzerdefinierter Typen weiter erweitert werden (siehe Definieren des Schemas für benutzerdefinierte Typen in Spark SQL? ).
Was ist falsch an dem in der Frage angegebenen Code?
(Haftungsausschluss: Pythonista-Standpunkt. Höchstwahrscheinlich habe ich einige Scala-Tricks verpasst)
Zuallererst gibt es einen Teil in Ihrem Code, der überhaupt keinen Sinn ergibt. Wenn Sie bereits (key, value)
Paare mit erstellt haben zipWithIndex
oder enumerate
was bringt es, einen String zu erstellen, um ihn direkt danach zu teilen? flatMap
funktioniert nicht rekursiv, so dass Sie einfach Tupel ausgeben und das Folgen überspringen können map
.
Ein anderer Teil, den ich problematisch finde, ist reduceByKey
. Im Allgemeinen reduceByKey
ist dies nützlich, wenn durch Anwenden der Aggregatfunktion die Datenmenge reduziert werden muss, die gemischt werden muss. Da Sie Zeichenfolgen einfach verketten, gibt es hier nichts zu gewinnen. Wenn Sie Dinge auf niedriger Ebene wie die Anzahl der Referenzen ignorieren, ist die Datenmenge, die Sie übertragen müssen, genau die gleiche wie für groupByKey
.
Normalerweise würde ich nicht weiter darauf eingehen, aber soweit ich das beurteilen kann, handelt es sich um einen Engpass in Ihrem Scala-Code. Das Verbinden von Strings in JVM ist eine ziemlich teure Operation (siehe zum Beispiel: Ist die Verkettung von Strings in Scala genauso kostspielig wie in Java? ). Dies bedeutet, dass so etwas, _.reduceByKey((v1: String, v2: String) => v1 + ',' + v2)
das input4.reduceByKey(valsConcat)
Ihrem Code entspricht, keine gute Idee ist.
Wenn Sie vermeiden möchten, groupByKey
können Sie versuchen, aggregateByKey
mit zu verwenden StringBuilder
. Ähnliches sollte den Trick machen:
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)
}
)
aber ich bezweifle, dass es die ganze Aufregung wert ist.
Unter Berücksichtigung der obigen Punkte habe ich Ihren Code wie folgt umgeschrieben:
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")
Python :
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")
Ergebnisse
Im local[6]
Modus (Intel (R) Xeon (R) CPU E3-1245 V2 bei 3,40 GHz) mit 4 GB Speicher pro Executor (n = 3):
- Scala - Mittelwert: 250,00 s, stdev: 12,49
- Python - Mittelwert: 246,66 s, stdev: 1,15
Ich bin mir ziemlich sicher, dass die meiste Zeit für das Mischen, Serialisieren, Deserialisieren und andere sekundäre Aufgaben aufgewendet wird. Nur zum Spaß, hier ist naiver Single-Threaded-Code in Python, der dieselbe Aufgabe auf diesem Computer in weniger als einer Minute ausführt:
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:])