Episode ini membahas cara mengoptimalkan performa Spark: tuning konfigurasi memory, shuffle, dan parallelism, broadcast join dan caching, membaca execution plan Catalyst, serta strategi menghindari data skew dan shuffle overload pada workload produksi.

Kode Spark yang benar belum tentu cepat. Di episode 9 ini kalian akan belajar bahwa performa Spark sangat ditentukan oleh konfigurasi dan pemahaman terhadap cara engine bekerja. Dua query yang menghasilkan output sama bisa berbeda puluhan kali lipat durasinya tergantung bagaimana Spark diminta mengeksekusinya.
Mengapa ini penting? Karena di produksi, resource cluster tidak gratis dan waktu adalah biaya. Sebuah job ETL yang berjalan dua jam alih-alih dua puluh menit berarti pemborosan besar. Engineer yang mampu mengoptimasi Spark bukan hanya menghemat uang, tapi juga memperlambat waktu tunggu seluruh tim data.
Episode ini membahas empat pilar optimasi: tuning konfigurasi dasar, broadcast join dan caching, membaca execution plan Catalyst, serta strategi menghadapi data skew dan shuffle overload.
Memori executor terbagi menjadi beberapa wilayah. Dua yang paling penting: execution memory untuk operasi seperti join dan shuffle, dan storage memory untuk cache. Keduanya berbagi satu pool yang bisa saling meminjam, dikontrol oleh spark.memory.fraction:
spark-submit --executor-memory 8g --conf spark.memory.fraction=0.6 job.pyAturan praktis: sisakan memori untuk JVM overhead dan sistem. Jika executor kehabisan memori, Spark akan memindahkan (spill) data ke disk dan job melambat drastis. Pastikan --executor-memory seimbang dengan jumlah core agar tidak membayar resource yang menganggur.
Shuffle adalah biaya terbesar. Dua parameter yang sering diatur:
spark.sql.shuffle.partitions: jumlah partisi output setelah shuffle — default 200.spark.shuffle.compress: kompresi data shuffle — biarkan true.spark-submit --conf spark.sql.shuffle.partitions=100 job.pyNilai ideal bergantung pada jumlah core dan ukuran data. Terlalu sedikit membuat tiap task berat, terlalu banyak membuat banyak task kecil yang overhead-nya lebih besar dari manfaatnya.
Paralelisme ditentukan oleh jumlah partisi, bukan jumlah core. Aturan umum: targetkan 2-3 partisi per core untuk workload CPU-bound, dan lebih banyak lagi untuk workload I/O karena task akan menunggu jaringan atau disk. Inspeksi paralelisme berjalan dengan:
df.rdd.getNumPartitions()Jika satu sisi join berukuran kecil (biasanya di bawah 10MB default, bisa diset sampai 200MB dengan autoBroadcastJoinThreshold), Spark bisa mengirim salinan ke seluruh executor. Dengan begitu, join dilakukan secara lokal tanpa shuffle sama sekali:
from pyspark.sql import functions as F
dimensi = spark.read.parquet("data/dim_kota.parquet")
hasil = transaksi.join(F.broadcast(dimensi), "kota_id")F.broadcast(dimensi) memberi tahu Catalyst untuk memperlakukan dimensi sebagai broadcast relation. Pada join yang membandingkan tabel transaksi raksasa dengan tabel dimensi kecil, teknik ini bisa memangkas waktu hingga puluhan kali lipat.
Data yang dipakai berulang kali dalam satu aplikasi sebaiknya di-cache agar tidak dihitung ulang dari sumber:
df.persist()
print(df.count())
print(df.filter("stok > 0").count())
df.unpersist()Tanpa persist(), setiap action memicu rekalkulasi penuh dari sumber. Namun ingat: caching memakan storage memory. Cache hanya data yang benar-benar dipakai ulang, dan panggil df.unpersist() saat selesai agar memori dilepas.
Saat partisi tidak seimbang atau terlalu banyak, sesuaikan dengan repartition() atau coalesce():
df = df.repartition(200, "kota_id")
df = df.coalesce(8).write.parquet("data/hasil.parquet")repartition memicu shuffle penuh dan berguna saat menambah paralelisme; coalesce menggabungkan partisi tanpa shuffle penuh dan berguna saat mengecilkan jumlah file sebelum menulis.
Optimasi terbaik dimulai dari diagnosis. df.explain() menampilkan bagaimana Spark akan mengeksekusi query:
df.groupBy("kota").agg(F.sum("jumlah")).explain("extended")Output menampilkan parsed logical plan, analyzed plan, optimized plan, hingga physical plan. Perhatikan tiga hal saat membacanya:
Catalyst melakukan banyak hal tanpa campur tangan kalian:
Tugas kalian bukan menggantikan Catalyst, tapi memastikan data memberi sinyal yang benar — misalnya menyediakan statistik tabel yang akurat agar join reordering bekerja baik.
Data skew terjadi ketika satu partisi menangani jauh lebih banyak data daripada yang lain — misalnya satu kota dengan 90 persen transaksi. Gejalanya: satu task memakan waktu lama sementara task lain selesai cepat. Solusi umum:
spark.sql.adaptive.enabled=true yang secara otomatis menangani skew join di Spark 3+.spark-submit --conf spark.sql.adaptive.enabled=true --conf spark.sql.adaptive.skewJoin.enabled=true job.pySetiap join, groupBy, dan reduceByKey memicu shuffle. Untuk mengurangi beban:
groupByKey dengan reduceByKey atau agregasi partial.collect() pada data besar — ini mengirim semua data ke driver dan mematikan memori driver.Beberapa operasi perlu dihindari atau didesain ulang:
collect() → kirim semua data ke driver
cartesian() → meledak secara eksponensial
groupByKey() → shuffle semua nilai mentah
udf pada baris → tanpa pushdown, jauh lebih lambat dari ekspresi SparkKhusus untuk UDF: jika bisa diekspresikan dengan fungsi bawaan pyspark.sql.functions, pakai itu. UDF Python memaksa data melintasi batas JVM-Python dan menghilangkan optimasi Catalyst.
Tip
Bangun kebiasaan diagnosis berurutan: mulai dari df.explain(), lalu periksa Spark UI untuk durasi per stage dan ukuran shuffle, baru kemudian ubah konfigurasi. Menebak tanpa data akan membuat kalian mengubah parameter yang salah.
Episode 9 menuntaskan keterampilan optimasi: tuning memori dan shuffle menentukan batas resource, broadcast join dan caching mengurangi pekerjaan berulang, membaca physical plan adalah keterampilan diagnosis, dan menghindari skew serta shuffle overload adalah seni menjaga pipeline tetap seimbang.
Inti yang harus dibawa pulang:
explain() dan Spark UI.unpersist().collect(), cartesian(), dan groupByKey pada data besar.Di episode 10 selanjutnya kita akan membahas Spark Streaming dan Structured Streaming — perbedaan DStream versus Structured Streaming, membangun pipeline streaming dengan readStream dan writeStream, trigger modes, watermarking, dan output modes, serta sink ke Kafka, file, console, dan storage.