La risposta originale che discute il codice è disponibile di seguito.
Prima di tutto, devi distinguere tra diversi tipi di API, ognuno con le proprie considerazioni sulle prestazioni.
API RDD
(strutture Python pure con orchestrazione basata su JVM)
Questo è il componente che sarà maggiormente influenzato dalle prestazioni del codice Python e dai dettagli dell'implementazione di PySpark. Mentre è improbabile che le prestazioni di Python rappresentino un problema, ci sono almeno alcuni fattori da considerare:
- Spese generali di comunicazione JVM. Praticamente tutti i dati che arrivano e vengono dall'esecutore Python devono essere passati attraverso un socket e un worker JVM. Sebbene questa sia una comunicazione locale relativamente efficiente, non è ancora gratuita.
Esecutori basati su processi (Python) contro esecutori basati su thread (singoli thread multipli JVM) (Scala). Ogni esecutore di Python viene eseguito nel proprio processo. Come effetto collaterale, fornisce un isolamento più forte rispetto alla sua controparte JVM e un certo controllo sul ciclo di vita degli esecutori ma un utilizzo della memoria potenzialmente significativamente maggiore:
- impronta di memoria dell'interprete
- impronta delle librerie caricate
- trasmissione meno efficiente (ogni processo richiede la propria copia di una trasmissione)
Prestazioni del codice Python stesso. In generale Scala è più veloce di Python ma varia da un'attività all'altra. Inoltre hai diverse opzioni tra cui JIT come Numba , estensioni C ( Cython ) o librerie specializzate come Theano . Infine, se non usi ML / MLlib (o semplicemente lo stack NumPy) , considera l'utilizzo di PyPy come interprete alternativo. Vedi SPARK-3094 .
- La configurazione di PySpark fornisce l'
spark.python.worker.reuse
opzione che può essere utilizzata per scegliere tra il processo di fork di Python per ogni attività e il riutilizzo di processi esistenti. Quest'ultima opzione sembra essere utile per evitare la costosa raccolta dei rifiuti (è più un'impressione che un risultato di test sistematici), mentre la prima (impostazione predefinita) è ottimale per le trasmissioni e le importazioni costose.
- Il conteggio dei riferimenti, usato come metodo di garbage collection di prima linea in CPython, funziona abbastanza bene con i carichi di lavoro tipici di Spark (elaborazione simile a stream, nessun ciclo di riferimento) e riduce il rischio di lunghe pause GC.
MLlib
(esecuzione mista di Python e JVM)
Le considerazioni di base sono praticamente le stesse di prima con alcuni problemi aggiuntivi. Mentre le strutture di base utilizzate con MLlib sono semplici oggetti Python RDD, tutti gli algoritmi vengono eseguiti direttamente utilizzando Scala.
Significa un costo aggiuntivo per la conversione di oggetti Python in oggetti Scala e viceversa, un maggiore utilizzo della memoria e alcune limitazioni aggiuntive che verranno descritte in seguito.
A partire da ora (Spark 2.x), l'API basata su RDD è in modalità manutenzione ed è pianificata per essere rimossa in Spark 3.0 .
DataFrame API e Spark ML
(Esecuzione JVM con codice Python limitato al driver)
Queste sono probabilmente la scelta migliore per le attività standard di elaborazione dei dati. Poiché il codice Python è principalmente limitato alle operazioni logiche di alto livello sul driver, non dovrebbero esserci differenze di prestazioni tra Python e Scala.
Un'unica eccezione è l'uso di UDF Python per riga che sono significativamente meno efficienti dei loro equivalenti Scala. Sebbene ci siano alcune possibilità di miglioramenti (c'è stato uno sviluppo sostanziale in Spark 2.0.0), il limite più grande è il completo roundtrip tra la rappresentazione interna (JVM) e l'interprete Python. Se possibile, dovresti favorire una composizione di espressioni incorporate ( esempio . Il comportamento UDF di Python è stato migliorato in Spark 2.0.0, ma è ancora non ottimale rispetto all'esecuzione nativa.
Ciò potrebbe essere migliorato in futuro è migliorato in modo significativo con l'introduzione degli UDF vettorizzati (SPARK-21190 e altre estensioni) , che utilizza Arrow Streaming per uno scambio efficiente di dati con la deserializzazione a zero copie. Per la maggior parte delle applicazioni i loro costi secondari possono essere semplicemente ignorati.
Assicurati anche di evitare dati di passaggio non necessari tra DataFrames
e RDDs
. Ciò richiede costose serializzazioni e deserializzazioni, per non parlare del trasferimento dei dati da e verso l'interprete Python.
Vale la pena notare che le chiamate Py4J hanno una latenza piuttosto elevata. Ciò include chiamate semplici come:
from pyspark.sql.functions import col
col("foo")
Di solito, non dovrebbe importare (l'overhead è costante e non dipende dalla quantità di dati) ma nel caso di applicazioni soft in tempo reale, è possibile considerare la memorizzazione nella cache / il riutilizzo dei wrapper Java.
Set di dati GraphX e Spark
Per ora (Spark 1.6 2.1) nessuno dei due fornisce l'API PySpark, quindi puoi dire che PySpark è infinitamente peggio di Scala.
Graphx
In pratica, lo sviluppo di GraphX si è arrestato quasi completamente e il progetto è attualmente in modalità di manutenzione con i relativi ticket JIRA chiusi perché non risolti . La libreria GraphFrames offre una libreria di elaborazione grafica alternativa con collegamenti Python.
dataset
Soggettivamente parlando non c'è molto spazio per digitare staticamente Datasets
in Python e anche se ci fosse l'attuale implementazione di Scala è troppo semplicistica e non offre gli stessi vantaggi prestazionali di DataFrame
.
Streaming
Da quello che ho visto finora, consiglierei vivamente di utilizzare Scala su Python. Potrebbe cambiare in futuro se PySpark ottenga il supporto per flussi strutturati, ma in questo momento l'API Scala sembra essere molto più solida, completa ed efficiente. La mia esperienza è piuttosto limitata.
Lo streaming strutturato in Spark 2.x sembra ridurre il divario tra le lingue, ma per ora è ancora agli inizi. Tuttavia, l'API basata su RDD è già indicata come "streaming legacy" nella documentazione di Databricks (data di accesso 2017-03-03), quindi è ragionevole aspettarsi ulteriori sforzi di unificazione.
Considerazioni di non prestazione
Parità caratteristica
Non tutte le funzionalità di Spark sono esposte tramite l'API PySpark. Assicurati di verificare se le parti necessarie sono già implementate e cerca di capire le possibili limitazioni.
È particolarmente importante quando si utilizzano MLlib e contesti misti simili (vedere Chiamata della funzione Java / Scala da un'attività ). Ad essere onesti alcune parti dell'API PySpark, come mllib.linalg
, fornisce un set di metodi più completo rispetto a Scala.
Progettazione API
L'API PySpark riflette da vicino la sua controparte Scala e come tale non è esattamente Pythonic. Significa che è abbastanza facile mappare tra le lingue ma allo stesso tempo, il codice Python può essere significativamente più difficile da capire.
Architettura complessa
Il flusso di dati PySpark è relativamente complesso rispetto all'esecuzione JVM pura. È molto più difficile ragionare sui programmi o sul debug di PySpark. Inoltre, almeno una conoscenza di base di Scala e JVM in generale è praticamente un must.
Spark 2.xe oltre
Il passaggio continuo Dataset
all'API, con l'API RDD bloccata offre sia opportunità che sfide agli utenti di Python. Mentre parti di alto livello dell'API sono molto più facili da esporre in Python, le funzionalità più avanzate sono praticamente impossibili da usare direttamente .
Inoltre le funzioni native di Python continuano ad essere cittadini di seconda classe nel mondo SQL. Speriamo che questo possa migliorare in futuro con la serializzazione di Apache Arrow ( gli sforzi attuali mirano ai daticollection
ma UDF serde è un obiettivo a lungo termine ).
Per progetti fortemente dipendenti dalla base di codice Python, le alternative pure Python (come Dask o Ray ) potrebbero essere un'alternativa interessante.
Non deve essere l'uno contro l'altro
L'API Spark DataFrame (SQL, Dataset) fornisce un modo elegante per integrare il codice Scala / Java nell'applicazione PySpark. È possibile utilizzare DataFrames
per esporre i dati a un codice JVM nativo e rileggere i risultati. Ho spiegato alcune opzioni da qualche altra parte e puoi trovare un esempio funzionante di andata e ritorno in Python-Scala in Come usare una classe Scala all'interno di Pyspark .
Può essere ulteriormente migliorato introducendo Tipi definiti dall'utente (vedere Come definire lo schema per il tipo personalizzato in Spark SQL? ).
Cosa c'è di sbagliato nel codice fornito nella domanda
(Disclaimer: punto di vista di Pythonista. Molto probabilmente mi sono perso alcuni trucchi alla Scala)
Prima di tutto, c'è una parte nel tuo codice che non ha alcun senso. Se hai già (key, value)
creato coppie usando zipWithIndex
o enumerate
qual è il punto nella creazione della stringa solo per dividerlo subito dopo? flatMap
non funziona in modo ricorsivo, quindi puoi semplicemente produrre tuple e saltare di seguito map
.
Un'altra parte che trovo problematica è reduceByKey
. In generale, reduceByKey
è utile se l'applicazione della funzione aggregata può ridurre la quantità di dati che devono essere mescolati. Dato che concateni semplicemente le stringhe, qui non c'è nulla da guadagnare. Ignorando elementi di basso livello, come il numero di riferimenti, la quantità di dati che devi trasferire è esattamente la stessa di groupByKey
.
Normalmente non mi soffermerei su questo, ma per quanto posso dire è un collo di bottiglia nel tuo codice Scala. Unire stringhe su JVM è un'operazione piuttosto costosa (vedi ad esempio: la concatenazione di stringhe in scala è costosa come in Java? ). Significa che qualcosa del genere _.reduceByKey((v1: String, v2: String) => v1 + ',' + v2)
che è equivalente a input4.reduceByKey(valsConcat)
nel tuo codice non è una buona idea.
Se si vuole evitare groupByKey
che si può provare a utilizzare aggregateByKey
con StringBuilder
. Qualcosa di simile a questo dovrebbe fare il trucco:
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)
}
)
ma dubito che valga tutta la confusione.
Tenendo presente quanto sopra, ho riscritto il codice come segue:
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")
risultati
In local[6]
modalità (Intel (R) Xeon (R) CPU E3-1245 V2 a 3,40 GHz) con 4 GB di memoria per esecutore sono necessari (n = 3):
- Scala - media: 250,00, stdev: 12,49
- Python - media: 246.66s, stdev: 1.15
Sono abbastanza sicuro che la maggior parte del tempo sia dedicato a mischiare, serializzare, deserializzare e altri compiti secondari. Solo per divertimento, ecco l'ingenuo codice a thread singolo in Python che esegue la stessa attività su questa macchina in meno di un minuto:
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:])