Qualcuno nel tuo team ti ha detto “cacha tutto” e il tuo job è diventato più veloce. Bene. Poi hai cachato tutto quanto e il job ha iniziato a crashare con errori OOM. Anche bene, nel senso che è stata un’esperienza formativa.
.cache() e .persist() sono strumenti potenti, ma non sono bottoni delle performance da premere a caso. Capire quando usarli (e quando no) è la differenza tra uno speedup di 10× e un mal di testa di 10×.
Cosa fa davvero il caching
Spark è lazy. Quando scrivi una catena di trasformazioni — filter, join, groupBy, select — non succede niente fino a quando non chiami un’azione (.count(), .show(), .write()). A quel punto Spark costruisce un piano di esecuzione e lancia l’intera catena.
Se chiami due azioni sullo stesso DataFrame, Spark esegue l’intera catena due volte. Ogni filter, ogni join, da capo. Non si ricorda i risultati intermedi.
.cache() dice a Spark: “Dopo aver calcolato questo DataFrame, tieni il risultato in memoria così la prossima azione può riusarlo invece di ricalcolare tutto da zero.”
expensive_df = (
raw_data
.join(lookup, "key")
.filter(col("status") == "active")
.groupBy("region").agg(sum("revenue").alias("total"))
)
expensive_df.cache()
# Prima azione: calcola e salva in memoria
expensive_df.count()
# Seconda azione: legge dalla cache, salta tutta la catena
expensive_df.show()
Quando il caching aiuta
Regola: cacha quando riutilizzi un DataFrame in più azioni downstream.
Scenari comuni:
- Pipeline ramificate. Un DataFrame alimenta due o più aggregazioni, scritture o report.
- Algoritmi iterativi. Loop di training ML, algoritmi su grafi, qualsiasi cosa che legge gli stessi dati ripetutamente.
- Esplorazione interattiva. Sei in un notebook e stai esplorando lo stesso dataset con query diverse.
Se un DataFrame viene usato una volta e buttato via, cacharlo spreca memoria a beneficio zero.
Quando il caching fa danni
1. Non hai abbastanza memoria.
I dati cachati vivono nella memoria degli executor. Se il tuo DataFrame è 50 GB e il cluster ha 40 GB di memoria executor, qualcosa viene sfrattato — o la tua cache (rendendola inutile) o la memoria di lavoro per altre operazioni (rendendo tutto più lento).
2. Cachi troppo presto.
# Male: cachare i dati grezzi prima di qualsiasi filtro
raw = spark.read.parquet("s3://huge-dataset/")
raw.cache() # 500 GB in memoria? Buona fortuna.
filtered = raw.filter(col("year") == 2025) # solo 5 GB dopo il filtro
Cacha dopo i filtri, non prima. Più piccolo è il DataFrame cachato, meglio è.
3. Ti dimentichi di fare unpersist.
I DataFrame cachati restano in memoria fino alla fine della SparkSession o finché non li rilasci esplicitamente:
expensive_df.unpersist()
Nei job di lunga durata con più fasi, dimenticarsi di fare unpersist significa che le cache precedenti mangiano la memoria di cui le fasi successive hanno bisogno. È l’errore di caching più comune nel codice di produzione.
4. Cachi qualcosa che è veloce da ricalcolare.
Leggere un piccolo file Parquet è già veloce di suo. Cacharlo risparmia millisecondi e spreca megabyte di memoria executor. Cacha solo le cose dove il costo di ricalcolo è genuinamente doloroso.
.cache() vs .persist()
.cache() è una scorciatoia per .persist(StorageLevel.MEMORY_AND_DISK). Puoi personalizzare:
from pyspark import StorageLevel
# Solo memoria (più veloce, ma viene sfrattato se la memoria è poca)
df.persist(StorageLevel.MEMORY_ONLY)
# Memoria + disco (riversa su disco se la memoria finisce)
df.persist(StorageLevel.MEMORY_AND_DISK)
# Solo disco (più lento ma risparmia memoria per altre operazioni)
df.persist(StorageLevel.DISK_ONLY)
# Serializzato (compresso, usa meno memoria, più lento da leggere)
df.persist(StorageLevel.MEMORY_ONLY_SER)
In pratica, il default (MEMORY_AND_DISK) va bene il 90% delle volte. Usa DISK_ONLY quando la memoria è poca e il ricalcolo è costoso. Usa MEMORY_ONLY quando la velocità conta e sei sicuro che entra tutto.
Come verificare se il caching funziona
Spark UI → tab Storage. Mostra ogni DataFrame cachato, quanto sta in memoria rispetto al disco, e quale percentuale è stata effettivamente cachata. Se vedi un DataFrame cachato al 20% e sfrattato all’80%, la cache non sta facendo quasi nulla — staresti meglio senza, liberando quella memoria.
Controlla anche: se uno stage è più veloce la seconda volta ma la prima è diventata più lenta (perché Spark stava materializzando la cache), assicurati che il totale sia comunque un guadagno netto. Il caching ha un costo iniziale.
La checklist decisionale
Prima di aggiungere .cache(), chiediti:
- Questo DataFrame viene usato più di una volta? No → non cachare.
- È costoso da ricalcolare? No → non cachare.
- Entra in memoria? No → usa
DISK_ONLYo non cachare. - Sto cachando dopo i filtri/select? No → sposta la cache più avanti.
- Mi ricorderò di fare
.unpersist()quando ho finito? No → creerai un memory leak.
Se hai risposto “sì” a tutte e cinque: cacha. Altrimenti, lascia che Spark ricalcoli. Il ricalcolo è il comportamento predefinito di Spark per un motivo — di solito è abbastanza efficiente, e non spreca memoria.