Cineva din echipă ți-a zis „pune-i cache” și job-ul a mers mai repede. Bine. Apoi ai pus cache la tot și job-ul a început să se prăbușească cu erori OOM (out of memory). Tot bine, într-un fel de „experiență de învățare”.
.cache() și .persist() sunt instrumente puternice, dar nu sunt butoane de performanță pe care le apeși la nimereală. Înțelegerea când să le folosești (și când nu) e diferența dintre o accelerare de 10× și o bătaie de cap de 10×.
Ce face caching-ul de fapt
Spark e leneș (lazy). Când scrii un lanț de transformări — filter, join, groupBy, select — nu se întâmplă nimic până nu apelezi o acțiune (.count(), .show(), .write()). În acel moment, Spark construiește un plan de execuție și rulează tot lanțul.
Dacă apelezi două acțiuni pe același DataFrame, Spark rulează tot lanțul de două ori. Fiecare filtru, fiecare join, de la zero. Nu reține rezultatele intermediare.
.cache() îi spune lui Spark: „După ce calculezi acest DataFrame, păstrează rezultatul în memorie ca următoarea acțiune să-l poată reutiliza în loc să recalculeze totul de la zero.”
expensive_df = (
raw_data
.join(lookup, "key")
.filter(col("status") == "active")
.groupBy("region").agg(sum("revenue").alias("total"))
)
expensive_df.cache()
# Prima acțiune: calculează și stochează în memorie
expensive_df.count()
# A doua acțiune: citește din cache, sare peste tot lanțul
expensive_df.show()
Când ajută caching-ul
Regulă: pune cache când refolosești un DataFrame în mai multe acțiuni downstream.
Scenarii frecvente:
- Pipeline-uri ramificate. Un DataFrame alimentează două sau mai multe agregări, scrieri sau rapoarte.
- Algoritmi iterativi. Bucle de antrenare ML, algoritmi pe grafuri, orice citește aceleași date în mod repetat.
- Explorare interactivă. Ești într-un notebook, sapi în același set de date cu query-uri diferite.
Dacă un DataFrame e folosit o singură dată și apoi aruncat, caching-ul risipește memorie pentru zero beneficiu.
Când dăunează caching-ul
1. Nu ai suficientă memorie.
Datele din cache trăiesc în memoria executorului. Dacă DataFrame-ul tău are 50 GB și clusterul tău are 40 GB de memorie pentru executoare, ceva se evacuează — fie cache-ul (făcându-l inutil) fie memoria de lucru pentru alte operații (făcând totul mai lent).
2. Pui cache prea devreme.
# Greșit: cache pe datele brute înainte de orice filtrare
raw = spark.read.parquet("s3://huge-dataset/")
raw.cache() # 500 GB în memorie? Mult succes.
filtered = raw.filter(col("year") == 2025) # doar 5 GB după filtru
Pune cache după filtrare, nu înainte. Cu cât DataFrame-ul din cache e mai mic, cu atât mai bine.
3. Uiți să faci unpersist.
DataFrame-urile din cache rămân în memorie până se termină SparkSession-ul sau le eliberezi explicit:
expensive_df.unpersist()
În job-uri de lungă durată cu mai multe etape, uitatul de unpersist înseamnă că cache-urile anterioare mănâncă memorie de care etapele ulterioare au nevoie. Aceasta e cea mai frecventă greșeală de caching în codul de producție.
4. Pui cache la ceva care e rapid de recalculat.
Citirea unui fișier Parquet mic e deja rapidă. Cache-ul salvează milisecunde și risipește megabytes de memorie a executorului. Pune cache doar la lucruri unde costul recalculării e cu adevărat dureros.
.cache() vs .persist()
.cache() e prescurtare pentru .persist(StorageLevel.MEMORY_AND_DISK). Poți personaliza:
from pyspark import StorageLevel
# Doar memorie (cel mai rapid, dar se evacuează dacă memoria e la limită)
df.persist(StorageLevel.MEMORY_ONLY)
# Memorie + disc (se varsă pe disc dacă memoria se umple)
df.persist(StorageLevel.MEMORY_AND_DISK)
# Doar disc (mai lent dar economisește memorie pentru alte operații)
df.persist(StorageLevel.DISK_ONLY)
# Serializat (comprimat, folosește mai puțină memorie, mai lent la citire)
df.persist(StorageLevel.MEMORY_ONLY_SER)
În practică, valoarea implicită (MEMORY_AND_DISK) e bună în 90% din cazuri. Folosește DISK_ONLY când memoria e la limită și recalcularea e costisitoare. Folosește MEMORY_ONLY când viteza contează și ești sigur că încape.
Cum verifici dacă caching-ul funcționează
Spark UI → tab-ul Storage. Arată fiecare DataFrame din cache, cât e în memorie vs disc și ce fracțiune a fost stocată cu succes. Dacă vezi un DataFrame care e 20% în cache și 80% evacuat, cache-ul nu face aproape nimic — ai fi mai bine fără cache, eliberând acea memorie.
Verifică și: dacă o etapă rulează mai repede a doua oară dar prima oară a fost mai lentă (pentru că Spark materializa cache-ul), asigură-te că totalul e totuși un câștig net. Caching-ul are un cost inițial.
Lista de verificare pentru decizie
Înainte să adaugi .cache(), întreabă:
- DataFrame-ul ăsta e folosit de mai multe ori? Nu → nu pune cache.
- E costisitor de recalculat? Nu → nu pune cache.
- Încape în memorie? Nu → folosește
DISK_ONLYsau nu pune cache. - Pun cache după filtre/selecturi? Nu → mută cache-ul mai târziu.
- O să-mi amintesc să fac
.unpersist()când termin? Nu → o să creezi o scurgere de memorie.
Dacă ai răspuns „da” la toate cinci: pune cache. Altfel, lasă Spark-ul să recalculeze. Recalcularea e comportamentul implicit al Spark-ului cu un motiv — e de obicei suficient de eficient, și nu risipește memorie.