Puoi usare Spark per anni e non guardare mai un diagramma di architettura. La maggior parte delle persone fa proprio così. Scrivono df.groupBy(...).agg(...), premono run, e se ne vanno. Funziona finché qualcosa non si rompe: un job si pianta al 99%, un executor muore, la Spark UI mostra barre rosse e shuffle read nell’ordine dei gigabyte, e a quel punto “non so bene cosa stia girando dove” diventa un problema serio.
Quindi prima di toccare una sola riga di PySpark, costruiamo il modello mentale di cosa succede quando sottometti un job. Alla fine di questa lezione saprai leggere la Spark UI come un romanzo: quale JVM sta facendo cosa, dove sta il parallelismo, e perché quella cosa chiamata “shuffle” è il cattivo di ogni storia di performance.
I personaggi
Un’applicazione Spark in esecuzione ha, come minimo, tre cose:
- Un driver: un processo JVM che esegue il tuo codice e coordina tutto.
- Un cluster manager: la cosa che assegna le macchine (o meglio, gli slot sulle macchine) alla tua applicazione.
- Uno o più executor: processi JVM che fanno effettivamente il lavoro.
Tutto qui. Niente magia. Nessun demone in background che ti sei dimenticato di installare. Tre componenti, tre responsabilità. Una volta interiorizzato questo, Spark smette di essere una scatola nera.
Vediamoli uno per uno.
Il driver
Il driver è la JVM che contiene la tua SparkSession. Quando esegui spark-submit my_job.py, o quando avvii un notebook e la prima cella crea una sessione, il driver è il processo che parte. Esegue il tuo codice Python (o Scala) dall’alto in basso, esattamente come farebbe un programma normale.
Il driver è single-threaded per il flusso lineare del tuo codice. I tuoi cicli for sono sequenziali. I tuoi if sono sequenziali. Le tue print escono in ordine. Il driver è semplicemente un programma normale che si dà il caso sappia parlare con un cluster.
Cosa fa davvero il driver:
- Tiene la
SparkSessione loSparkContext: i tuoi punti di ingresso al motore. - Costruisce il DAG (Directed Acyclic Graph) delle trasformazioni mentre concateni
.select(),.filter(),.join(),.groupBy(). Niente viene ancora eseguito: il driver sta solo raccogliendo una ricetta. - Quando chiami una action (
.count(),.show(),.write.parquet(...),.collect()), il driver passa il DAG all’ottimizzatore Catalyst, che lo riscrive in un piano fisico efficiente. - Il driver poi suddivide quel piano in stage e task e spedisce quei task agli executor.
- Riceve i risultati, monitora il progresso, riavvia i task falliti, e infine restituisce la risposta finale al tuo codice.
Due cose da ricordare sul driver. Primo: quando chiami .collect(), ogni riga del risultato torna nella heap della JVM del driver. Se fai .collect() su una tabella da un miliardo di righe, il tuo driver va in OOM e il job muore. Sappi sempre se la tua action è “summary” (.count(), .show(20)) o “tutto quanto” (.collect(), .toPandas()). Secondo: il driver è un single point of failure. Se muore lui, muore l’applicazione. I cluster manager possono riavviare gli executor in modo trasparente; non possono riavviare un driver in volo.
Gli executor
Gli executor sono le JVM che macinano i numeri. Girano sui worker node (macchine nel tuo cluster) e il driver gli dà lavoro. Un cluster Spark tipico ha da 2 a 2.000 executor, a seconda del job e del budget.
Ogni executor ha due risorse che ti interessano:
- Memoria. Usata per cachare i DataFrame su cui hai chiamato
.cache(), per tenere i dati di shuffle, per eseguire i task. La configuri conspark.executor.memory=4go simile. - Core. Il numero di task che un executor può eseguire in parallelo. Un executor con 4 core esegue 4 task contemporaneamente. La configuri con
spark.executor.cores=4.
Moltiplica gli executor per i core e ottieni il tuo parallelismo totale. Un cluster con 10 executor e 4 core ciascuno può eseguire 40 task simultaneamente. Se il tuo DataFrame ha 200 partizioni, quei 200 task verranno processati in circa 5 ondate da 40.
Gli executor vivono per la durata dell’applicazione. Partono quando parte la tua SparkSession, muoiono quando finisce la tua applicazione. (La dynamic allocation può aggiungere e rimuovere executor durante il job, ma è un’ottimizzazione che vedremo molto più avanti.) Un executor che muore durante un job, per OOM, guasto della macchina, problema di rete, viene rimpiazzato dal cluster manager, e Spark riesegue qualunque task ci stesse girando sopra. Questa è la famosa fault tolerance: il lineage permette a Spark di ricalcolare sempre quello che ha perso.
Un punto sottile ma importante: in PySpark, ogni JVM executor può anche avviare uno o più processi Python worker se il tuo job usa UDF Python o operazioni RDD. La JVM e i Python worker comunicano su un socket, e i dati devono essere serializzati tra di loro. È qui che PySpark si guadagna la fama di essere “più lento di Scala”, ma solo se usi davvero UDF Python. Lo approfondiamo nella lezione 6.
Il cluster manager
Il cluster manager è il livello che possiede le macchine e decide chi può usarle. Spark di per sé non gestisce le macchine: chiede a qualcun altro di dargliele.
I quattro cluster manager che Spark supporta:
- Spark Standalone. Il manager incluso con Spark stesso. Arriva con la distribuzione Spark, è facile da configurare, va bene per piccoli cluster dedicati. Lo vedrai nei tutorial e negli ambienti di laboratorio.
- YARN. Il resource manager di Hadoop. Se la tua azienda ha un cluster Hadoop, e un numero sorprendente lo ha ancora nel 2026, anche se cala ogni anno, Spark on YARN è il default. YARN è stato il deployment di produzione dominante per anni.
- Kubernetes. L’opzione moderna cloud-native. Spark su K8s è maturato moltissimo dal 2020 ed è ora lo standard per i nuovi deployment greenfield su AWS EKS, GCP GKE, Azure AKS. Databricks, EMR, e la maggior parte delle piattaforme Spark gestite girano su K8s sotto il cofano. Se stai partendo con un nuovo deployment Spark nel 2026, è quasi sicuramente quello che vuoi.
- Mesos. Un tempo era un contendente, ora praticamente morto. Apache ha ritirato Mesos nel 2021. Lo vedrai in installazioni legacy e da nessun’altra parte.
Il lavoro del cluster manager è ristretto: quando il tuo driver chiede “dammi 10 executor con 4 core e 8 GB di RAM ciascuno”, il cluster manager trova le macchine che hanno quelle risorse libere e avvia lì le JVM degli executor. Una volta che sono su, il driver parla direttamente con gli executor. Il cluster manager è praticamente fuori dai giochi durante il lavoro vero e proprio.
Di solito non scegli tu il cluster manager: lo sceglie la tua piattaforma. Databricks sceglie Kubernetes (la loro variante). EMR ti lascia scegliere tra YARN e Kubernetes. Glue sceglie qualcosa di proprietario. La maggior parte delle volte scrivi lo stesso codice PySpark a prescindere e il manager è invisibile.
Come gira davvero un job
Ora tracciamo cosa succede quando chiami una action. Diciamo che esegui:
result = (
spark.read.parquet("s3://bucket/orders/")
.filter("order_status = 'shipped'")
.groupBy("country")
.agg({"amount": "sum"})
.collect()
)
Ecco la sequenza:
- Le prime tre righe (
read,filter,groupBy,agg) costruiscono un DAG nel driver. Nessun dato viene ancora letto. .collect()è una action. Il driver invia il DAG a Catalyst, l’ottimizzatore di query. Catalyst lo riscrive: spinge il filtro dentro la lettura Parquet così non leggiamo righe non spedite; sceglie una strategia di aggregazione; produce un piano fisico.- Il piano fisico viene suddiviso in stage. Uno stage è un blocco contiguo di lavoro dove ogni operazione può essere fatta indipendentemente per ogni partizione senza spostamento di dati. Il filtro e l’aggregazione parziale possono accadere nello stage 1. Il groupBy su tutti i dati richiede spostamento di dati (uno shuffle), quindi l’aggregazione finale è nello stage 2.
- Il driver chiede al cluster manager gli executor (se non li ha già).
- Lo stage 1 parte come N task, dove N è il numero di partizioni di input. Se il Parquet ha 200 file, N è probabilmente 200. Ogni task è una partizione di lavoro, inviata a un core di un executor.
- Quando i task dello stage 1 finiscono, scrivono i loro risultati parziali sul disco locale dell’executor: questo è lo shuffle write.
- Lo stage 2 parte. Ogni task nello stage 2 legge il suo input dall’output di shuffle dello stage 1 attraverso la rete, lo shuffle read, e calcola le somme finali per gruppo.
- I risultati finali vengono inviati al driver, che li assembla in una lista Python e li restituisce al tuo codice.
Tre parole di vocabolario da quella sequenza sono quelle che vedrai nella Spark UI tutto il giorno:
- Job. Quello che innesca una action. Una chiamata a
.collect()= un job. - Stage. Una fetta contigua del DAG senza shuffle in mezzo. La maggior parte dei job ha 2-10 stage.
- Task. Una partizione di uno stage in esecuzione su un core di un executor. L’unità di parallelismo. Un job tipico ha migliaia di task.
Il momento “aha” nella Spark UI
La Spark UI (porta di default 4040 sul driver) è lo strumento diagnostico singolarmente più utile in tutto l’ecosistema. Una volta che capisci l’architettura, la UI ti dice tutto.
Quando apri il tab Executors, stai guardando le JVM che fanno il lavoro. Memoria usata, core attivi, task completati, tempo GC, byte di shuffle letti e scritti. Se un executor ha fatto 10 volte il lavoro degli altri, hai skew e il tuo job ne sta soffrendo.
Quando apri il tab Stages, stai guardando le fette del DAG. La durata di ogni stage, il numero di task, la dimensione dell’input, lo shuffle read/write. Uno stage che impiega 10 minuti quando gli altri ne hanno impiegati 30 secondi è il collo di bottiglia: vai a guardarlo.
Quando apri la vista Tasks dentro uno stage, stai guardando le unità individuali a livello di partizione. La durata minima, mediana e massima dei task ti dicono se il lavoro è bilanciato. Se la massima è 50 volte la mediana, hai una strategia di partizionamento sbagliata e poche righe stanno facendo il lavoro di milioni.
Questo è l’intero loop del performance tuning di Spark: leggere la UI, trovare lo stage lento, trovare il task con skew, sistemare il layout dei dati. Quasi tutto il resto è una nota a piè di pagina.
Cosa non abbiamo ancora coperto
Alcune cose deliberatamente saltate, perché sono lezioni intere a sé:
- Layout della memoria di un executor (storage memory, execution memory, l’unified memory manager). Lezione 14.
- Internals dello shuffle: sort-shuffle, push-based shuffle, il ruolo dell’external shuffle service. Lezione 22.
- Adaptive Query Execution (AQE), che riscrive dinamicamente il piano a metà del job sulla base delle dimensioni reali delle partizioni. Lezione 24.
- Cluster autoscaling e dynamic allocation: Spark che aggiunge e rimuove executor man mano che scopre di averne bisogno. Lezione 41.
Per ora, tieni in testa questa immagine: un driver, molti executor, un cluster manager che assegna le macchine, e un DAG di lavoro che fluisce dalla tua SparkSession giù fino alle partizioni sul disco. Tutto il resto in PySpark è dettaglio.
Prossima lezione: le tre API che Spark ti dà, RDD, DataFrame, Dataset, e perché per il ~99% del lavoro ne tocchi sempre e solo una.
References
- Apache Spark — Cluster Mode Overview: https://spark.apache.org/docs/latest/cluster-overview.html
- Apache Spark — Submitting Applications: https://spark.apache.org/docs/latest/submitting-applications.html
- Apache Spark — Monitoring and Instrumentation (Spark UI): https://spark.apache.org/docs/latest/monitoring.html
Retrieved 2026-05-01.