Spark performance per Scala vs Python


178

Preferisco Python rispetto alla Scala. Ma, poiché Spark è scritto in modo nativo in Scala, mi aspettavo che il mio codice funzionasse più velocemente in Scala rispetto alla versione Python per ovvie ragioni.

Con questo presupposto, ho pensato di imparare e scrivere la versione Scala di alcuni codici di preelaborazione molto comuni per circa 1 GB di dati. I dati vengono raccolti dalla competizione SpringLeaf su Kaggle . Giusto per dare una panoramica dei dati (contiene 1936 dimensioni e 145232 righe). I dati sono composti da vari tipi, ad esempio int, float, string, boolean. Sto usando 6 core su 8 per l'elaborazione Spark; ecco perché l'ho usato in minPartitions=6modo che ogni core abbia qualcosa da elaborare.

Codice Scala

val input = sc.textFile("train.csv", minPartitions=6)

val input2 = input.mapPartitionsWithIndex { (idx, iter) => 
  if (idx == 0) iter.drop(1) else iter }
val delim1 = "\001"

def separateCols(line: String): Array[String] = {
  val line2 = line.replaceAll("true", "1")
  val line3 = line2.replaceAll("false", "0")
  val vals: Array[String] = line3.split(",")

  for((x,i) <- vals.view.zipWithIndex) {
    vals(i) = "VAR_%04d".format(i) + delim1 + x
  }
  vals
}

val input3 = input2.flatMap(separateCols)

def toKeyVal(line: String): (String, String) = {
  val vals = line.split(delim1)
  (vals(0), vals(1))
}

val input4 = input3.map(toKeyVal)

def valsConcat(val1: String, val2: String): String = {
  val1 + "," + val2
}

val input5 = input4.reduceByKey(valsConcat)

input5.saveAsTextFile("output")

Codice Python

input = sc.textFile('train.csv', minPartitions=6)
DELIM_1 = '\001'


def drop_first_line(index, itr):
  if index == 0:
    return iter(list(itr)[1:])
  else:
    return itr

input2 = input.mapPartitionsWithIndex(drop_first_line)

def separate_cols(line):
  line = line.replace('true', '1').replace('false', '0')
  vals = line.split(',')
  vals2 = ['VAR_%04d%s%s' %(e, DELIM_1, val.strip('\"'))
           for e, val in enumerate(vals)]
  return vals2


input3 = input2.flatMap(separate_cols)

def to_key_val(kv):
  key, val = kv.split(DELIM_1)
  return (key, val)
input4 = input3.map(to_key_val)

def vals_concat(v1, v2):
  return v1 + ',' + v2

input5 = input4.reduceByKey(vals_concat)
input5.saveAsTextFile('output')

Scala Performance Stage 0 (38 minuti), Stage 1 (18 sec) inserisci qui la descrizione dell'immagine

Python Performance Stage 0 (11 minuti), Stage 1 (7 sec) inserisci qui la descrizione dell'immagine

Entrambi producono diversi grafici di visualizzazione DAG (a causa dei quali entrambe le immagini mostrano diverse funzioni dello stage 0 per Scala ( map) e Python ( reduceByKey))

Ma essenzialmente entrambi i codici tentano di trasformare i dati in (dimension_id, stringa dell'elenco di valori) RDD e salvarli su disco. L'output verrà utilizzato per calcolare varie statistiche per ogni dimensione.

Per quanto riguarda le prestazioni, il codice Scala per questi dati reali come questo sembra funzionare 4 volte più lentamente della versione Python. Una buona notizia per me è che mi ha dato una buona motivazione per stare con Python. Cattive notizie è che non ho capito bene perché?


8
Forse questo dipende dal codice e dall'applicazione mentre ottengo l'altro risultato, che apache spark python è più lento di scala, quando si sommano un miliardo di termini della formula di Leibniz per π
Paul,

3
Domanda interessante! A proposito, dai un'occhiata anche qui: emptypipes.org/2015/01/17/python-vs-scala-vs-spark Più core hai, meno puoi vedere le differenze tra le lingue.
Markon,

Hai considerato di accettare la risposta esistente?
10465355 dice Reinstate Monica il

Risposte:


358

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.reuseopzione 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 DataFramese 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 Datasetsin 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 Datasetall'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 DataFramesper 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 zipWithIndexo enumeratequal è il punto nella creazione della stringa solo per dividerlo subito dopo? flatMapnon 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 groupByKeyche si può provare a utilizzare aggregateByKeycon 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:])

23
Una delle risposte più chiare, complete e utili che ho incontrato per un po '. Grazie!
etov,

Che ragazzo eccezionale sei!
DennisLi

-4

Estensione alle risposte sopra -

Scala si dimostra più veloce in molti modi rispetto a Python ma ci sono alcuni motivi validi per cui Python sta diventando più popolare di Scala, vediamone alcuni -

Python per Apache Spark è abbastanza facile da imparare e usare. Tuttavia, questo non è l'unico motivo per cui Pyspark è una scelta migliore di Scala. C'è più.

L'API Python per Spark potrebbe essere più lenta sul cluster, ma alla fine i data scientist possono fare molto di più rispetto a Scala. La complessità di Scala è assente. L'interfaccia è semplice e completa.

Parlare della leggibilità del codice, della manutenzione e della familiarità con l'API Python per Apache Spark è molto meglio di Scala.

Python viene fornito con diverse librerie relative all'apprendimento automatico e all'elaborazione del linguaggio naturale. Questo aiuta nell'analisi dei dati e ha anche statistiche molto mature e testate nel tempo. Ad esempio, numpy, panda, scikit-learn, seaborn e matplotlib.

Nota: la maggior parte dei data scientist utilizza un approccio ibrido in cui utilizza il meglio di entrambe le API.

Infine, la comunità di Scala risulta spesso molto meno utile per i programmatori. Questo rende Python un apprendimento molto prezioso. Se hai abbastanza esperienza con qualsiasi linguaggio di programmazione tipicamente statico come Java, puoi smettere di preoccuparti di non usare del tutto Scala.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.