Vai al contenuto principale

Architettura di Apache Spark: una guida per i professionisti dei dati

Capisci come Apache Spark elabora i dati su larga scala—dai componenti fondamentali alle funzionalità avanzate che guidano i moderni workflow big data.
Aggiornato 16 apr 2026  · 15 min leggi

Ti è mai capitato di debuggare un job Spark che è improvvisamente fallito e renderti conto di essere completamente perso per quanto profonda sia la tana del Bianconiglio di Spark? 

Quando ho iniziato a lavorare con Apache Spark, pensavo di dover solo scrivere qualche trasformazione in PySpark e che Spark si sarebbe “magicamente” scalato sul cluster. Mi sbagliavo. Le prestazioni di Spark dipendono interamente dalla comprensione di ciò che succede dietro le quinte.

Questa guida è per chiunque non voglia trattare Spark come una scatola nera. Vedremo come è progettata l’architettura di Spark, dal modello master–worker e il flusso di esecuzione, fino alla gestione della memoria e ai meccanismi di tolleranza ai guasti. 

Se vuoi creare applicazioni big data veloci, tolleranti ai guasti ed efficienti, sei nel posto giusto!

Architettura di base di Apache Spark

Prima ancora di scrivere la tua prima riga di PySpark, Spark ha già preso alcune decisioni architetturali per te. Spark non è veloce solo per il computing in memoria, ma perché è costruito su un’architettura master–worker che scala e sopravvive al caos del mondo reale, come crash dei nodi, hes, Java Virtual Machine (JVM) e volumi di dati incostanti.

Vediamo nel dettaglio l’architettura core di Spark e perché è ancora così potente e presente nei moderni workflow big data.

Paradigma master–worker

Al cuore di Spark c’è il modello master–worker . Pensa a questa analogia:

  • Driver (master): è il cervello di Spark. Esegue la tua funzione main(), crea il contesto Spark, gestisce lo scheduling del DAG e dice al cluster cosa fare.
  • Executor (worker): sono i muscoli. Eseguono i task, mantengono i dati in memoria e riportano al driver.

Questa configurazione ti consente di concentrarti nel definire le trasformazioni, mentre Spark decide dove e come eseguirle in parallelo sugli executor. 

Quello che apprezzo di questo design è che è indipendente dal deployment. Lo stesso codice gira a prescindere che tu lo esegua in locale, su Kubernetes o Mesos. Questo rende semplice sviluppare e testare in locale, per poi scalare su cluster senza riscrivere il codice.

E c’è un altro vantaggio potente della separazione driver–worker: migliora l’isolamento dai guasti. Se un nodo worker muore mentre esegue un task, Spark può riassegnare quel task a un altro worker senza mandare in crash l’applicazione.

Componenti principali

Vediamo cosa succede all’interno del driver e dei nodi. 

Diagram showing the Spark architecture

Architettura di Spark. Immagine dell’autore.

Spark context

Quando chiami SparkContext() o usi SparkSession.builder.getOrCreate(), stai aprendo il portale verso tutta la magia interna di Spark.

Lo Spark context:

  • Si connette al tuo cluster manager
  • Alloca gli executor
  • Traccia lo stato dei job e i piani di esecuzione

Spark costruisce dietro le quinte un grafo aciclico diretto (DAG) di trasformazioni. Quel DAG viene suddiviso in stage e task, poi eseguiti in parallelo.

Il DAG scheduler capisce quali task possono essere eseguiti insieme, e il Task scheduler li assegna agli executor. Nel frattempo, il Block manager assicura che i dati vengano messi in cache, shufflati o ricaricati secondo necessità. 

Questo design a livelli rende Spark incredibilmente flessibile, perché puoi mettere a punto memoria, storage e compute in modo indipendente.

Se stai lavorando con trasformazioni Spark o feature engineering, dai un’occhiata al corso Feature Engineering with PySpark per vedere questa architettura in azione.

Runtime degli executor

Gli executor sono dove il lavoro viene svolto.

Ogni executor esegue:

  • Uno o più task (in thread)
  • Un blocco di memoria per mettere in cache i dati e scrivere l’output dello shuffle
  • Una propria istanza JVM, isolata dalle altre

Puoi configurare quanta memoria riceve ogni executor, quanti core usa e se deve scrivere su disco quando la memoria si esaurisce. 

Ma attenzione: se non allochi abbastanza memoria, incapperai continuamente in errori di out-of-memory. D’altro canto, allocarne troppa spreca risorse. Monitoraggio e tuning qui sono essenziali.

Flusso di esecuzione: dal codice al cluster

Scrivere codice in PySpark sembra piuttosto semplice. Filtri un DataFrame, fai una join, aggreghi qualcosa e avvii. Ma dietro quella pulita API, Spark avvia silenziosamente un motore di esecuzione che può distribuire il lavoro su più nodi. 

Vediamo cosa succede dietro le quinte.

Conversione da piano logico a piano fisico

Ecco cosa la maggior parte degli utenti Spark non capisce subito: quando scrivi codice PySpark, non stai eseguendo nulla immediatamente. Stai costruendo un piano, e il Catalyst Optimizer di Spark prende quel piano e lo trasforma in una strategia di esecuzione efficiente.

Funziona in quattro fasi:

  1. Analisi: Spark risolve nomi di colonne, tipi di dato e riferimenti alle tabelle, assicurandosi che tutto sia valido.
  2. Ottimizzazione logica: Qui Spark applica regole come il predicate pushdown e il constant folding. Ottimizza i filtri e combina le proiezioni.
  3. Pianificazione fisica: Spark valuta più strategie di esecuzione e sceglie la più efficiente (in base a dimensione dei dati, partizionamento, ecc.).
  4. Generazione del codice: Infine usa la whole-stage code generation per produrre bytecode JVM. 

Image showing a diagram of Spark’s Catalyst Optimizer

Catalyst Optimizer di Spark. Immagine di Databricks.

Quindi quella catena di .select(), .join() e .groupBy() non viene eseguita riga per riga. Viene analizzata, ottimizzata e compilata in qualcosa che gira velocemente su un cluster.

Dai un’occhiata a questo PySpark Cheat Sheet se vuoi un promemoria dei comandi PySpark più utili.

DAG scheduler e creazione degli stage

Quando il piano è pronto, entra in gioco il DAG scheduler.

Scompone il job in stage in base ai confini degli shuffle, decidendo cosa avviene in sequenza e cosa può essere eseguito in parallelo.

Ci sono due tipi principali di stage:

  • ShuffleMapStage: coinvolge uno shuffle, di solito causato da trasformazioni ampie come groupBy() o join(). I dati vengono quindi partizionati e inviati attraverso la rete. Questo tipo di stage è richiesto per calcolare il ResultStage.
  • ResultStage: questi stage producono output, come la scrittura su disco o il ritorno dei risultati al driver.

Una cosa fondamentale che ho imparato è minimizzare gli shuffle. Uno shuffle deve avvenire prima che uno stage finisca ed è costoso. Devi capire dove si verificano nel tuo DAG e se puoi ottimizzare ulteriormente il codice per ridurne il numero. 

Ciclo di vita dell’esecuzione dei task

Una volta che il DAG scheduler ha creato tutti gli stage, possono essere eseguiti sui vari executor. 

Il ciclo di vita di esecuzione dei task è più o meno così:

  1. Serializzazione del task: il driver serializza le istruzioni del task e le invia agli executor.
  2. Fase di scrittura dello shuffle: Spark scrive in locale l’output partizionato.
  3. Fase di fetch: gli executor dello stage successivo recuperano i file di shuffle rilevanti dagli altri nel cluster.
  4. Deserializzazione ed esecuzione: gli executor deserializzano i dati, eseguono la tua logica e, potenzialmente, mettono in cache o scrivono i risultati.
  5. Garbage collection: la JVM recupera automaticamente la memoria che non è più utilizzata dalle applicazioni Spark. Questo passaggio è essenziale per prevenire memory leak e assicurare che le applicazioni Spark girino senza problemi.

Un piccolo suggerimento dalla mia esperienza: se il tuo job Spark rimane bloccato dopo aver funzionato bene in precedenza, spesso è a causa della garbage collection o di ritardi nel fetch dello shuffle. Controlla sempre il codice e assicurati di capire l’architettura di Spark, così da ottimizzare efficacemente questi aspetti.

Architettura della gestione della memoria

La gestione della memoria in Spark è un tema molto complesso e può costarti ore di debugging se non la comprendi. 

Vediamo quindi come Spark gestisce la memoria sotto il cofano, così da esserne consapevole ed evitare ore a fare debugging di codice lento o di errori di out-of-memory.

Modello di memoria unificato

Prima di Spark 1.6, la memoria era rigidamente divisa tra execution (per shuffle e join) e storage (per la cache). Con Spark 1.6 è stato introdotto il modello di memoria unificato. 

Nel modello unificato, la memoria è suddivisa in tre pool chiave:

  • Memoria riservata: una piccola quantità di memoria è usata per gli internals di Spark e il sistema. 
  • Memoria Spark: usata sia per i dati di esecuzione sia per la cache. È condivisa dinamicamente. Se il tuo job richiede più memoria per gli shuffle e meno per la cache (o viceversa), Spark si adatta.
  • Memoria utente: spazio per le strutture dati definite dall’utente necessarie per eseguire il codice utente all’interno delle applicazioni Spark.

Il pool di memoria Spark è ulteriormente diviso in due pool:

  1. Executor memory: memorizza i dati temporanei necessari durante le fasi di elaborazione dei task (ad es. shuffle, join, aggregazioni, …). 
  2. Storage memory pool: usato per la cache dei dati e per memorizzare strutture dati interne. 

Questa elasticità permette a Spark di essere più flessibile con volumi di dati imprevedibili. 

Tuttavia significa anche perdere un po’ di controllo quando non sai cosa sta succedendo. Ad esempio, se fai cache() di un DataFrame grande ma hai anche aggregazioni costose nello stesso stage, Spark potrebbe espellere i dati in cache per fare spazio allo shuffle.

Off-heap e storage colonnare

Nell’off-heap e nello storage colonnare di Spark entra in gioco il motore Tungsten. 

Tungsten ha introdotto diverse ottimizzazioni che hanno migliorato le prestazioni di Spark:

  • Gestione della memoria off-heap: Spark ora memorizza parte dei dati fuori dall’heap della JVM, riducendo l’overhead della garbage collection e rendendo la gestione della memoria più prevedibile.
  • Storage in formato binario: i dati sono memorizzati in una forma binaria compatta e adatta alla cache, che migliora l’uso della CPU e abilita l’esecuzione vettoriale.
  • Algoritmi cache-aware: Spark può ora usare più efficacemente le cache della CPU, evitando letture non necessarie da RAM o disco.

E se lavori con i DataFrame, stai già usando queste ottimizzazioni sotto il cofano. È uno dei motivi per cui invito a usare DataFrame e API SQL al posto degli RDD grezzi. Ottieni tutta la potenza di Catalyst e Tungsten senza tuning extra.

Se stai lavorando con pipeline di data cleaning, vedrai questo in azione in Cleaning Data with PySpark.

Meccanismi di tolleranza ai guasti

Se lavori con sistemi distribuiti, sai una cosa con certezza: falliscono. I nodi vanno in crash. Si verificano errori di rete. Gli executor esauriscono la memoria e si spengono.

Ma Spark è costruito per gestire questi problemi e far sì che i tuoi job vadano comunque a buon fine. 

Vediamo più a fondo come Spark garantisce che i job riescano anche in presenza di instabilità.

Tracciamento della lineage degli RDD

Gli RDD (Resilient Distributed Datasets) sono la struttura dati fondamentale in Spark. E si chiamano resilient per un motivo. 

Spark usa la lineage per assicurare che ciascun RDD possa essere ricalcolato in caso di guasto di un nodo e perdita di dati. 

Quindi, quando un nodo fallisce, Spark semplicemente ricalcola i dati persi usando il grafo di lineage. 

Ecco come funziona in pratica: 

  • Dipendenze strette (come map() o filter()): Spark ha bisogno solo della partizione persa per ricalcolare.
  • Dipendenze ampie (come groupBy() o join()): Spark potrebbe dover recuperare dati da più partizioni, poiché può richiedere l’output di diversi stage. 

La lineage evita di dover gestire i fallimenti manualmente. Tuttavia, se il tuo grafo di lineage diventa troppo lungo, contenendo centinaia di trasformazioni, ricalcolare i dati persi diventa costoso. Qui entra in gioco il checkpointing.

Checkpointing e write-ahead log

Quando affronti workflow complessi o job di streaming, Spark non può dipendere solo dalla lineage. Ecco dove entra in gioco il checkpointing.

Puoi chiamare rdd.checkpoint() per persistere lo stato corrente dell’RDD in un’area di storage affidabile (come HDFS). 

Spark poi tronca la lineage. Se si verifica un errore, ricarica i dati direttamente invece di ricalcolarli.

Nello structured streaming, Spark usa anche i write-ahead log (WAL) per garantire che i dati non vadano persi in transito. 

Ecco cosa lo rende così stabile: 

  • Receiver affidabili: scrivono i dati in arrivo nei log prima dell’elaborazione.
  • Heartbeat degli executor: questi segnali regolari confermano che gli executor sono attivi e in salute.
  • Directory di checkpoint: per i job di streaming, contengono offset, metadati e stato dell’output, così puoi riprendere da dove eri rimasto.

Il checkpointing è opzionale per i job batch, ma richiesto per le pipeline di streaming. 

Immagina un job Spark fallito dopo 10 ore di esecuzione, ma che puoi riprendere da dove si è interrotto grazie a checkpointing e WAL. 

Funzionalità architetturali avanzate

A questo punto hai visto come Spark elabora i job e gestisce memoria e guasti.

In questa sezione, entriamo in alcune evoluzioni architetturali avanzate che rendono Spark più dinamico, più real-time e più adattabile.

Adaptive Query Execution (AQE)

AQE è stata introdotta in Spark 3.0 e migliora le prestazioni delle query regolando dinamicamente i piani di esecuzione a runtime in base alle statistiche raccolte durante l’esecuzione.

Le funzionalità di AQE includono:

  • Scelta dinamica delle strategie di join: se il tuo broadcast join non entra in memoria, AQE passa a un sort-merge join.
  • Coalescenza delle partizioni di shuffle: unisce piccole partizioni di shuffle in partizioni più grandi, riducendo l’overhead.
  • Gestione dei dati skewed: AQE può dividere le partizioni sbilanciate per uniformare i tempi di esecuzione.

Questa funzionalità cambia le carte in tavola, perché permette ai job che prima richiedevano tuning manuale e tentativi continui di adattarsi in tempo reale.

Assicurati solo di abilitarla esplicitamente via configurazione (spark.sql.adaptive.enabled = true). E se stai usando Spark 3.0+, non c’è motivo per non farlo.

Architettura dello Structured Streaming

Lo Structured Streaming estende il motore di Spark nel dominio real-time, senza costringerti a imparare una nuova API.

Dietro le quinte applica ancora il micro-batching. Ma gestisce:

  • Gestione degli offset: Spark traccia esattamente quali dati sono stati letti dalla tua sorgente (Kafka, socket, file, ecc.). Se configurato correttamente, fornisce forti garanzie di exactly-once.
  • Watermarking: con aggregazioni basate sul tempo, Spark usa watermark per decidere quando i dati in ritardo sono troppo in ritardo per essere inclusi. È cruciale per l’elaborazione in event time.
  • State store: quando fai aggregazioni finestrate o join in streaming, Spark mantiene lo stato tra i micro-batch. Questo stato è memorizzato su disco e messo in checkpoint per evitare perdite di dati.

La potenza qui è che lo streaming sembra batch. Scrivi un groupBy() o un filter() e Spark gestisce tutto il resto, rendendo l’analisi streaming accessibile senza una toolchain specializzata.

Architettura della sicurezza

Se esegui Spark in produzione, soprattutto in finanza, healthcare o settori simili, devi sapere come Spark gestisce autenticazione, crittografia e auditabilità.

Approfondiamo quindi questi temi e come Spark se ne occupa.

Autenticazione e crittografia

Spark ha molte funzionalità di sicurezza che devi prima abilitare. Ma una volta attivate, Spark offre una solida cassetta degli attrezzi per comunicazioni sicure e autenticazione: 

  • Autenticazione (SASL): Spark usa il Simple Authentication and Security Layer (SASL) per verificare che solo utenti e servizi autorizzati possano inviare job o connettersi al cluster.
  • Crittografia in transito (AES-GCM, SSL/TLS): Spark cripta la comunicazione tra i nodi usando AES-GCM (autenticata) o TLS. Questo protegge i dati dei job da intercettazioni, particolarmente importante in ambienti multi-tenant o cloud.
  • Integrazione con Kerberos: se esegui su Hadoop/YARN, Spark si integra con Kerberos per un’autenticazione utente sicura. Questo collega i tuoi job Spark direttamente ai sistemi aziendali di identity e access management.
  • Controllo di accesso alla UI: la Spark Web UI può esporre info sensibili (come log, path di input, query SQL), quindi imposta spark.acls.enable=true e spark.ui.view.acls e spark.ui.view.acls.groups per limitarne l’accesso.

Puoi controllare tutte le funzionalità di sicurezza nella documentazione ufficiale di Spark. Dagli un’occhiata e assicurati di abilitare le funzionalità di cui hai bisogno per mettere in sicurezza le tue applicazioni Spark.

Audit e compliance

Registrare chi ha fatto cosa e quando è altrettanto fondamentale. 

Spark supporta: 

  • Event logging: quando abilitato (spark.eventLog.enabled=true), Spark registra su disco ogni evento di job, stage e task. Puoi usare questi log per rigiocare la storia dei job o soddisfare requisiti di audit.
  • Controllo accessi basato sui ruoli (RBAC): Spark non fornisce RBAC, ma se lo usi tramite piattaforme come Databricks, EMR o OpenShift, di solito ottieni RBAC a livello di infrastruttura. Spark invia i job usando un’identità definita, che controlla l’accesso sia ai dati sia alle risorse di calcolo.
  • Data masking e access control alla fonte: Spark legge da molte fonti (Parquet, Delta Lake, Hive, ecc.) e il controllo degli accessi dovrebbe essere applicato lì.

Pattern di ottimizzazione delle prestazioni

Spark è molto potente e veloce, e può essere ottimizzato per essere ancora più veloce se sai dove intervenire. 

Ci sono diverse aree in cui puoi provare a ottimizzare per ottenere il massimo da Spark. Vediamole più da vicino.

Ottimizzazione degli shuffle

Se Spark ha un punto debole, è lo shuffle. Gli shuffle avvengono quando i dati devono essere spostati tra partizioni, in genere dopo trasformazioni ampie come groupByKey(), distinct() o join().

E quando gli shuffle vanno male, puoi avere un enorme I/O su disco, lunghe pause di garbage collection o task sbilanciati che non finiscono mai. 

Ecco come migliorare gli shuffle:

  • Preferisci reduceByKey() a groupByKey(): reduceByKey() aggrega localmente prima dello shuffle. groupByKey() manda tutto sulla rete.
  • Ripartiziona con criterio: usa .repartition(n) per aumentare il parallelismo, o .coalesce(n) per ridurlo. Non lasciare tutto al partizionamento di default di Spark.
  • Usa le broadcast join (con giudizio): se un dataset è abbastanza piccolo, diffondilo a tutti i worker. Imposta spark.sql.autoBroadcastJoinThreshold per controllare il limite di dimensione.
  • Evita collect(): evitalo quando possibile, perché riportare i dati al driver uccide le prestazioni.

Linee guida per la configurazione della memoria

Ottimizzare la memoria di Spark può essere una scienza, ma puoi usare la checklist qui sotto per semplificare:

  • Alloca memoria sufficiente: parti da almeno 6 GB di memoria per il cluster Spark e adatta in base alle tue esigenze specifiche.
  • Considera la frazione di memoria Spark: di default, il 60% è la frazione di memoria in Spark. Aumentala se le tue applicazioni si basano molto su operazioni con DataFrame/Dataset o se ti serve più memoria utente. 
  • Usa il numero corretto di core per executor: in genere 3–5 è ottimale. Troppo pochi portano a sotto-utilizzo, troppi portano a contesa tra task.
  • Abilita l’allocazione dinamica (se supportata): Spark può aumentare/ridurre gli executor in base al carico. 
spark.dynamicAllocation.enabled=true
spark.dynamicAllocation.minExecutors=2
spark.dynamicAllocation.maxExecutors=20
  • Regola la storage fraction: se hai bisogno di più cache, aumenta il valore di spark.memory.storageFraction.
  • Monitora e profila l’uso della memoria: usa strumenti come la Spark UI o VisualVM per tracciare il consumo di memoria e individuare i colli di bottiglia.

Adattare la configurazione della memoria può aiutare molto. Una volta ho ridotto un job da 30 minuti a 8 minuti solo modificando la configurazione della memoria, senza cambiare una riga di codice.

Formule per il dimensionamento del cluster

Questa è la parte in cui la maggior parte dei team sbaglia, perché indovina la dimensione del cluster invece di stimarla correttamente. 

Ma puoi fare meglio usando le formule seguenti: 

  1. Determina il numero di partizioni: 
    • Calcola il numero di partizioni necessario in base alla dimensione dei dati e alla dimensione desiderata delle partizioni. 
    • Una linea guida standard è avere una partizione ogni 128 MB–256 MB di dati non compressi.
    • Formula: Numero di partizioni = Arrotonda per eccesso (Dimensione totale dati ÷ Dimensione partizione).
  2. Calcola il numero totale di core: 
    • Il numero di core necessario dovrebbe essere sufficiente a processare tutte le partizioni in parallelo.
    • Formula: Core totali = Arrotonda per eccesso (Numero di partizioni ÷ Partizioni per core).
  3. Determina la memoria per executor: 
    • Calcola la memoria necessaria a ciascun executor in base ai suoi core, alla dimensione della partizione e all’overhead.
    • Formula: Memoria per executor = Memoria base × (1 + Percentuale di overhead).
  4. Calcola il numero di executor: 
    • Determina il numero di executor in base al numero totale di core e ai core per executor.
    • Formula: Numero di executor = Arrotonda per eccesso (Core totali ÷ Core per executor).
  5. Calcola la memoria totale: 
    • Calcola la memoria totale necessaria per il cluster in base al numero di executor e alla memoria per executor.
    • Formula: Memoria totale = Numero di executor × Memoria per executor.

Per esempio: 

  • Input: 500 GB di dati e una dimensione partizione di ~128 MB
  • Partizioni: ~4.000 partizioni
  • Core: 4.000 partizioni / 4 partizioni per core = 1.000
  • Memoria per executor: supponi 8 GB per executor e 20% di overhead. 8 GB * 1,20 = 9,6 GB
  • Executor: 1.000 core / 4 core per executor = 250 executor
  • Memoria totale: 250 executor * 9,6 GB = 2.400 GB

Ma ricorda: è solo una stima. Usala come punto di partenza e poi ottimizza ulteriormente tramite profiling.

Trend architetturali emergenti

Spark esiste da un decennio, ma è ancora molto attuale. Sta evolvendo più che mai, grazie a piattaforme cloud-native, accelerazione GPU e integrazione più stretta con l’ML.

Se oggi usi Spark nello stesso modo di tre anni fa, probabilmente stai lasciando prestazioni sul tavolo e ti perdi ottime nuove funzionalità.

Vediamo alcune delle più recenti.

Photon engine (Databricks)

Se lavori con Databricks, probabilmente hai già usato e sentito parlare di Photon.

Se vuoi saperne di più su Databricks, ti consiglio il corso Introduction to Databricks.

Photon è il motore di nuova generazione sulla piattaforma Lakehouse di Databricks che offre prestazioni di query elevate a basso costo. È compatibile con le API Spark, quindi non devi adattare il tuo codice Spark per sfruttarlo. 

Aiuta a potenziare in modo significativo il tuo codice SQL e PySpark.

Photon include le seguenti funzionalità: 

  • Esecuzione vettoriale: Photon elabora i dati in batch colonnari, sfruttando le istruzioni CPU SIMD (Single Instruction, Multiple Data) per operare su più valori simultaneamente. Spark tradizionale usa un’esecuzione riga per riga e si affida molto alla JVM per allocazione e garbage collection della memoria.
  • Runtime in C++ (niente overhead JVM): niente garbage collection Java, che può essere un collo di bottiglia nei job Spark grandi. La memoria è gestita con precisione in C++.
  • Migliori ottimizzazioni di query: Photon si integra profondamente con il Catalyst Optimizer di Spark, ma include anche proprie ottimizzazioni a runtime (come filtri a runtime, percorsi di codice adattivi, ottimizzazioni per join e aggregazioni). 
  • Accelerazione hardware: supporto per hardware moderno (come GPU NVIDIA, set di istruzioni AVX-512 per CPU Intel, processori Graviton (ARM) su AWS). 

Serverless Spark

Il serverless è fantastico, perché significa che non devi gestire cluster, pre-provisionare risorse e paghi solo per il tempo in cui Spark è in esecuzione. 

E il serverless per Spark è già disponibile in servizi come Databricks Serverless, AWS Glue e GCP Dataproc Serverless.

Ecco perché è incredibile:

  • Scalabilità automatica: la piattaforma scala il compute in base alle esigenze reali del job, quindi non devi indovinare quanti nodi ti servono.
  • Convenienza economica: paghi solo ciò che usi. Niente più costi per server inattivi. 
  • Semplicità: niente setup del cluster, configurazione o manutenzione: ci pensa la piattaforma.
  • Prestazioni: sono possibili tempi di esecuzione più rapidi, poiché configurazione e setup sono ottimizzati per te.

Serverless Spark è ideale per analytics interattive, job ad hoc o workload imprevedibili.

Ma fai attenzione: pipeline lunghe e costanti possono essere ancora più economiche su cluster fissi. Misura sempre sia costo sia latenza.

Integrazione con MLflow

Man mano che l’industria evolve, il confine tra data engineering e AI si fa sempre più sfumato. Come ha esplorato Deepak Goyal, CEO & Founder di Azurelib Academy, nel podcast DataFramed

Il data engineering giocherà un ruolo vitale e fondamentale nel prossimo passaggio all’AI.

Deepak GoyalCEO & Founder at Azurelib Academy

Se fai machine learning su larga scala e vuoi portare i modelli in produzione, Spark da solo non basta. Ti servono i principi MLOps, come tracciamento degli esperimenti, versioning dei modelli e riproducibilità. È qui che si inserisce MLflow

MLflow ora si integra con Spark e porta uno stack MLOps completo nelle tue pipeline.

Puoi: 

  • Tracciare gli esperimenti: registra parametri, metriche e artifact dai job Spark ML usando mlflow.log_param() e mlflow.log_metric().
  • Versionare i modelli: salva modelli da pyspark.ml o sklearn direttamente nel model registry di MLflow.
  • Servire i modelli: distribuisci i modelli addestrati su endpoint REST usando il model serving di MLflow.

Non devi cambiare strumenti. Continui a usare Spark per training, feature engineering e scoring, sfruttando MLflow per i compiti MLOps.

Conclusione

Se non conosci bene Spark, sembra una gigantesca scatola nera. Scrivi un po’ di codice PySpark, avvii e speri che funzioni. 

A volte per me ha funzionato bene, altre volte ha portato a lunghe sessioni di debugging per capire cosa fosse andato storto. 

Solo quando ho iniziato a guardare dietro le quinte tutto ha iniziato ad avere senso. E mi ci è voluto un bel po’ per capire cosa stesse succedendo.

Ecco su cosa mi concentrerei se dovessi ricominciare da zero: 

  • Impara come Spark suddivide il tuo codice in job, stage e task.
  • Comprendi la memoria.
  • Fai attenzione agli shuffle.
  • Parti in piccolo ed esegui in local mode. Sporcati le mani.

È esattamente ciò che abbiamo imparato in questo articolo.

Se vuoi continuare a imparare, ecco alcune risorse per principianti che consiglio:

FAQ

Come scelgo il cluster manager giusto per il mio deployment Spark?

Spark supporta diversi cluster manager (YARN, Mesos, Kubernetes e standalone). La tua scelta dipende dall’infrastruttura esistente, dalle esigenze di condivisione delle risorse e dall’expertise operativa: YARN si integra bene nei cluster Hadoop, Kubernetes offre portabilità containerizzata e Mesos eccelle nell’isolamento multi-tenant.

Cos’è l’external shuffle service e come migliora le prestazioni?

L’external shuffle service disaccoppia il servizio dei file di shuffle dal ciclo di vita degli executor, abilitando l’allocazione dinamica e riducendo la perdita di dati durante l’evizione degli executor. Mantiene disponibili i file di shuffle anche dopo l’arresto degli executor, accelerando i retry degli stage e riducendo l’I/O su disco sotto carico elevato.

Come funzionano internamente le broadcast join e quando dovrei usarle?

Per le broadcast join, Spark invia una piccola tabella di lookup a ogni executor per evitare shuffle completi dei dati. Usale quando un lato della join è al di sotto di spark.sql.autoBroadcastJoinThreshold (default 10 MB), perché riducono drasticamente l’I/O di rete e velocizzano le join con distribuzioni di chiavi sbilanciate.

Quali sono le best practice per ottimizzare la garbage collection della JVM in Spark?

Monitora le pause di GC tramite la Spark UI o strumenti come VisualVM e preferisci il collector G1GC per i suoi tempi di pausa ridotti. Alloca memoria agli executor con margine per l’overhead (spark.executor.memoryOverhead) e regola -XX:InitiatingHeapOccupancyPercent per avviare la GC prima, prevenendo lunghe pause stop-the-world.

Come posso sfruttare l’accelerazione GPU per velocizzare i job Spark?

Usa NVIDIA RAPIDS Accelerator for Apache Spark per scaricare in modo trasparente le operazioni SQL e DataFrame sulle GPU. Si integra nel motore di esecuzione di Spark, sostituendo gli operatori basati su CPU con equivalenti accelerati da GPU e offrendo fino a 10× di velocità in più per i workload adatti.

Qual è la differenza tra allocazione statica e dinamica delle risorse in Spark?

L’allocazione statica fissa il numero di executor per tutta la durata del job, offrendo prevedibilità al costo di possibili risorse inattive. L’allocazione dinamica permette a Spark di richiedere o rilasciare executor in base ai task in coda e al carico, migliorando l’utilizzo del cluster per job fluttuanti—ideale in ambienti condivisi.

Come dovrei configurare Spark per prestazioni ottimali su sistemi di storage cloud come S3?

Abilita S3 Transfer Acceleration, regola spark.hadoop.fs.s3a.connection.maximum e usa la consistent view (S3A v2) per gestire l’eventual consistency. Coalesci i file piccoli prima di scrivere e considera gli S3A committer per ridurre l’overhead delle operazioni di listing e migliorare il throughput in scrittura.

Come posso mettere in sicurezza le comunicazioni Spark con Kerberos e TLS?

Abilita TLS per l’RPC (spark.ssl.enabled) e configura SASL/Kerberos (spark.authenticate and spark.kerberos.keytab) per imporre l’autenticazione reciproca. Conserva le credenziali in un keytab sicuro e accessibile da HDFS e limita l’accesso alla Spark UI tramite ACL per prevenire esposizioni non autorizzate.

Cosa sono le Pandas UDF e quando sono più efficienti delle UDF normali?

Le Pandas UDF (UDF vettoriali) usano Apache Arrow per scambiare dati in batch tra JVM e Python, riducendo drasticamente l’overhead di serializzazione. Superano le UDF tradizionali riga per riga per operazioni numeriche complesse, specialmente quando si elaborano grandi batch colonnari in PySpark.

Quali vantaggi offre l’API DataSource V2 rispetto alla V1 per sorgenti dati personalizzate?

DataSource V2 offre un’interfaccia più pulita e modulare che supporta nativamente push-down dei filtri, pruning delle partizioni e sorgenti di streaming. Abilita un controllo più granulare di lettura/scrittura e una migliore integrazione con il Catalyst optimizer di Spark, risultando in prestazioni superiori e maggiore manutenibilità per connettori su misura.


Patrick Brus's photo
Author
Patrick Brus
LinkedIn

Sono un Cloud Engineer con una solida base in Ingegneria Elettrica, machine learning e programmazione. Ho iniziato la mia carriera nella visione artificiale, concentrandomi sulla classificazione delle immagini, per poi passare a MLOps e DataOps. Mi occupo di creare piattaforme MLOps, supportare i data scientist e fornire soluzioni basate su Kubernetes per semplificare i flussi di lavoro di machine learning.

Argomenti

Approfondisci Spark con questi corsi!

Corso

Machine Learning con PySpark

4 h
29.3K
Impara a fare previsioni dai dati con Apache Spark, usando alberi decisionali, regressione logistica, regressione lineare, insiemi e pipeline.
Vedi dettagliRight Arrow
Inizia il corso
Mostra altroRight Arrow
Correlato

blog

Tokenizzazione nel NLP: come funziona, sfide e casi d'uso

Guida al preprocessing NLP nel machine learning. Copriamo spaCy, i transformer di Hugging Face e come funziona la tokenizzazione in casi d'uso reali.
Abid Ali Awan's photo

Abid Ali Awan

10 min

Mostra altroMostra altro