Menguasai pemrosesan terdistribusi dengan Apache Spark: memahami RDD vs DataFrame, partitioning & shuffle yang menentukan performa, strategi optimasi caching dan broadcast join, serta praktik mendiagnosis dan mengoptimasi job Spark yang lambat

Di episode 10 kalian menjalankan PySpark untuk batch processing — dan mungkin bertanya-tanya: kenapa job saya lambat padahal datanya "cuma" 10 GB? Jawabannya hampir selalu bukan kapasitas, melainkan cara Spark memecah dan memindahkan data. Episode 21 membawa kalian ke dalam cara kerja mesin: partitioning, shuffle, caching, dan join strategy.
Mengapa ini penting? Karena di production, performa Spark menentukan biaya cloud. Job yang lambat berarti cluster lebih besar, lebih lama, lebih mahal — dan data engineer yang mengerti optimasi adalah orang yang menghemat ratusan juta rupiah per bulan.
Spark punya dua API utama. Memahami perbedaannya menjelaskan banyak keputusan optimasi:
| Aspek | RDD | DataFrame |
|---|---|---|
| Level | Low-level, objek Java/Python | High-level, data tabular + schema |
| Optimasi | Manual, eksplisit | Catalyst optimizer otomatis |
| Kapan dipakai | Paling banyak untuk hal-hal rendah | Default untuk data engineering |
| Python performa | Lambat (tiap elemen lewat Python) | Cepat (eksekusi di JVM, Python tipis) |
Aturan utama: selalu pakai DataFrame. RDD hanya untuk kasus khusus yang benar-benar butuh kontrol rendah. DataFrame memanfaatkan Catalyst optimizer — Spark menyusun rencana eksekusi terbaik untuk query kalian secara otomatis.
Spark memproses data dalam partisi — potongan data yang diproses paralel oleh executor. Jumlah partisi menentukan paralelisme:
from pyspark.sql import SparkSession
from pyspark.sql.functions import col
spark = SparkSession.builder \
.appName("partition_opt") \
.config("spark.sql.shuffle.partitions", "200") \
.getOrCreate()
df = spark.read.parquet("s3://data-lake/bronze/orders/")
print(f"Partisi saat dibaca: {df.rdd.getNumPartitions()}")
# Repartisi untuk paralelisme lebih tinggi
df2 = df.repartition(400)
# Coalesce: mengurangi partisi tanpa shuffle (hanya untuk menurunkan)
df3 = df.coalesce(8)Prinsipnya:
Shuffle terjadi ketika Spark harus mengelompokkan data dari semua partisi — misalnya pada groupBy, join, atau distinct. Data dipindahkan antar executor melalui jaringan, dan ini operasi paling mahal di Spark.
Cara mengurangi shuffle:
repartition adalah shuffle itu sendiri; coalesce (tanpa shuffle) untuk menurunkan.# Lambat: kelompokkan semua dulu, baru filter
slow = df.groupBy("customer_id").count().filter(col("count") > 100)
# Cepat: filter dulu, barulah groupBy (lebih sedikit data di-shuffle)
fast = df.filter(col("status") == "completed") \
.groupBy("customer_id").count() \
.filter(col("count") > 100)Perhatikan prinsip universalnya: perkecil data sebelum operasi mahal.
Note
Gejala umum shuffle bermasalah: straggler — satu executor selesai berjam-jam setelah yang lain. Penyebabnya data skewed (satu key mendominasi). Solusi klasik: salting — menambah suffix acak pada key saat shuffle, lalu menghapusnya setelah agregasi.
Jika sebuah DataFrame dipakai berulang kali dalam job yang sama (misalnya dimension table yang di-join ke banyak fact), baca ulang dari disk adalah pemborosan. Cache menyimpan hasil di memori:
dim = spark.read.parquet("s3://data-lake/gold/dim_customer/")
dim.cache() # atau persist(MEMORY_AND_DISK)
dim.count() # force action agar benar-benar di-cache
result1 = fact.join(dim, "customer_id").groupBy("segment").sum()
result2 = fact2.join(dim, "customer_id").groupBy("city").sum()Perbedaan cache() vs persist():
cache() = persist(MEMORY_AND_DISK) — memori dulu, spill ke disk jika penuh.persist(level) — kontrol level: MEMORY_ONLY (cepat, boros memori), DISK_ONLY (hemat memori, lambat).Aturan praktis: cache hanya jika dipakai lebih dari satu kali dan hasilnya kecil-menengah. Meng-cache DataFrame raksasa malah memicu eviction dan memperlambat.
Join biasa (shuffle join) memindahkan kedua sisi berdasarkan key. Jika satu sisi kecil (dimension table, misal di bawah beberapa ratus MB), broadcast join menyalin sisi kecil ke setiap executor — menghilangkan shuffle total:
from pyspark.sql.functions import broadcast
# Sisi kecil (< 10MB default threshold) → broadcast otomatis jika memungkinkan
small = spark.read.parquet("s3://data-lake/gold/dim_customer/")
# Eksplisit memaksa broadcast
result = fact.join(
broadcast(small), "customer_id", "inner"
)Bandingkan cost: shuffle join memindahkan kedua dataset lewat jaringan; broadcast join mengirim satu kali salinan tabel kecil ke semua executor. Untuk join fact (milliaran baris) × dimension (jutaan baris), perbedaan ini bisa mengubah 40 menit menjadi 4 menit.
Tingkatkan threshold hanya jika yakin tabel kecil cukup muat di memori executor:
spark.conf.set("spark.sql.autoBroadcastJoinThreshold", "104857600") # 100 MBWorkflow debugging job Spark yang lambat:
from pyspark.sql import SparkSession
from pyspark.sql.functions import broadcast, col
spark = SparkSession.builder \
.config("spark.sql.shuffle.partitions", "400") \
.getOrCreate()
fact = spark.read.parquet("s3://data-lake/bronze/orders/")
dim = spark.read.parquet("s3://data-lake/gold/dim_customer/").cache()
dim.count()
# Filter dulu sebelum join & agregasi
fact_filtered = fact.filter(col("status") == "completed")
# Broadcast join dengan dimensi kecil
joined = fact_filtered.join(broadcast(dim), "customer_id", "inner")
# Agregasi dengan partisi cukup
result = joined.groupBy("segment").count()
result.write.mode("overwrite").parquet("s3://data-lake/gold/segment_counts/").collect() di job produksi. Memindahkan seluruh data ke driver = OOM. collect hanya untuk hasil kecil & debugging.
Meng-cache tanpa count(). Cache bersifat lazy — tanpa action, tidak ada yang tersimpan.
Broadcast join untuk tabel besar. Jika "tabel kecil" tak muat di memori executor, broadcast malah OOM. Ukur dulu.
Menggunakan RDD untuk segala. Python RDD jauh lebih lambat dari DataFrame. Gunakan DataFrame kecuali ada alasan kuat.
Di episode 21 ini kalian telah menguasai optimasi Spark:
Di episode 22 selanjutnya kita menyatukan performa dan biaya: performance & cost optimization — query tuning, clustering & partitioning, serverless vs provisioned, dan FinOps data, plus praktik mengurangi cost pipeline. Sampai jumpa di episode 22!