Chapter sebelumnya menutup dengan keputusan TokoKita beralih ke arah scale-out. Apache Spark adalah mesin komputasi yang paling banyak dipakai untuk arah tersebut: satu mesin yang sama menjalankan pipeline batch, query SQL, dan pemrosesan stream di atas cluster. Sebelum mengolah data pesanan sungguhan, kita perlu memahami cara Spark membagi pekerjaan lewat arsitektur driver, executor, dan cluster manager. Chapter ini juga membedah satu perilaku yang paling sering mengejutkan pendatang baru dari dunia pandas: lazy evaluation, yaitu kebiasaan Spark menunda eksekusi sampai hasil benar-benar diperlukan.

Arsitektur Driver, Executor, dan Cluster Manager #

Spark memisahkan pekerjaan ke dalam dua peran utama. Driver adalah proses yang menjalankan kode program kita: membangun rencana eksekusi, memecah pekerjaan menjadi task, lalu mengirimkan task tersebut ke executor. Executor adalah proses di mesin pekerja yang menjalankan task di atas data lokal, lalu mengembalikan hasilnya ke driver. Keduanya tidak bernegosiasi soal sumber daya — cluster manager seperti YARN, Kubernetes, atau standalone bawaan Spark yang mengurus mesin mana dipakai siapa.

Diagram di atas juga menjelaskan mengapa gagal satu mesin tidak menghentikan pekerjaan: cluster manager cukup menjadwalkan ulang task dari executor yang gagal ke executor lain, dan Spark menghitung ulang partition dari sumber data. Untuk belajar, kita tidak butuh cluster sungguhan — mode local[1] menjalankan driver dan satu executor di dalam satu proses JVM di laptop, dan seluruh konsep tetap berlaku. Detail lengkap tiap komponen ada di dokumentasi cluster overview Spark.

SparkSession: Titik Masuk Program #

Sejak Spark 2.0, seluruh interaksi dimulai dari satu objek: SparkSession. Objek inilah yang membuat DataFrame, menjalankan SQL, dan membaca sumber data. Kita membentuknya lewat pola builder — tentukan master, konfigurasi, lalu panggil getOrCreate() supaya sesi yang sama dipakai ulang bila sudah ada. Konfigurasi yang sama bisa juga dibaca dari berkas spark-defaults.conf atau diberikan lewat spark-submit saat berproduksi; pola builder di kode memberi kita satu titik tempat seluruh pengaturan sesi terlihat jelas.

Transformation, Action, dan Lazy Evaluation #

Operasi Spark terbagi dua keluarga. Transformation seperti filter() atau select() hanya menyusun rencana eksekusi — tidak satu baris pun diproses. Action seperti show(), collect(), atau count() yang memicu eksekusi nyata dari seluruh rantai rencana tersebut. Perilaku inilah yang disebut lazy evaluation, dan tabel berikut merangkum anggota masing-masing keluarga:

KeluargaPerilakuContoh
Transformationmenyusun rencana, menunda eksekusifilter(), select(), withColumn(), groupBy()
Actionmemicu eksekusi dan mengembalikan hasilshow(), collect(), count(), write.parquet()

Skrip berikut membuktikan kedua perilaku tersebut dengan program Spark pertama kita — data jumlah pesanan empat pelanggan TokoKita:

python
"""Program Spark pertama: cek versi, transformation lazy, dan action."""
from pyspark.sql import SparkSession

spark = (
    SparkSession.builder
    .master("local[1]")
    .config("spark.ui.enabled", "false")
    .config("spark.sql.shuffle.partitions", "2")
    .getOrCreate()
)
spark.sparkContext.setLogLevel("ERROR")

print("Versi Spark :", spark.version)

data = [("Budi", 3), ("Siti", 5), ("Dewi", 2), ("Andi", 4)]
df = spark.createDataFrame(data, ["pelanggan", "jumlah_pesanan"])

# Transformation: baris-baris ini hanya menyusun rencana eksekusi
hasil = (
    df.filter(df.jumlah_pesanan >= 3)
    .select("pelanggan", "jumlah_pesanan")
)
print("Transformation terdaftar, eksekusi belum dimulai")

# explain() menampilkan rencana eksekusi — masih tanpa memproses data
hasil.explain()

# Action: collect() dan show() memicu eksekusi nyata
total = hasil.groupBy().sum("jumlah_pesanan").collect()[0][0]
print("Total pesanan pelanggan aktif:", total)

hasil.show()
spark.stop()

Output yang Dihasilkan:

text
Versi Spark : 4.2.0
Transformation terdaftar, eksekusi belum dimulai
== Physical Plan ==
*(1) Filter (isnotnull(jumlah_pesanan#1L) AND (jumlah_pesanan#1L >= 3))
+- *(1) Scan ExistingRDD[pelanggan#0,jumlah_pesanan#1L]


Total pesanan pelanggan aktif: 12
+---------+--------------+
|pelanggan|jumlah_pesanan|
+---------+--------------+
|     Budi|             3|
|     Siti|             5|
|     Andi|             4|
+---------+--------------+

Mari kita telaah baris-baris kode di atas:

  1. Baris 4-10 (builder SparkSession): master("local[1]") menjalankan Spark dalam satu JVM dengan satu core; spark.sql.shuffle.partitions dikecilkan menjadi 2 agar latihan kecil tidak membuat 200 partition kosong.
  2. Baris 15-16 (df = spark.createDataFrame(...)): pembuatan DataFrame juga belum memproses apa pun — Spark hanya mencatat sumber data beserta skemanya.
  3. Baris 19-22 (hasil = ...): rantai filter() dan select() adalah transformation; keduanya hanya menambah langkah pada rencana eksekusi. Baris 23 mencetak bukti bahwa program masih berjalan tanpa eksekusi.
  4. Baris 26 (hasil.explain()): mencetak physical plan — rencana eksekusi berupa Filter di atas Scan. Angka seperti #1L adalah penanda kolom internal; angkanya berubah-ubah antar sesi, tetapi struktur rencananya stabil. Sampai titik ini, data belum tersentuh.
  5. Baris 29 (...collect()[0][0]): collect() adalah action. Baru pada panggilan inilah Spark menjalankan rencana secara utuh dan mengembalikan hasil 12 — total pesanan Budi, Siti, dan Andi yang lolos filter >= 3.
  6. Baris 32-33 (hasil.show() dan spark.stop()): show() adalah action kedua yang memicu eksekusi ulang rencana (hasilnya ditampilkan sebagai tabel), lalu stop() mengembalikan sumber daya sesi.

Ketika action di baris 29 berjalan, driver memecah rencana menjadi job, job menjadi stage, dan stage menjadi task — task inilah unit yang dikirim ke executor seperti pada diagram arsitektur di awal chapter. Pada mode local[1] seluruh task dieksekusi berurutan di satu JVM sehingga output terasa seperti program Python biasa; di cluster, task yang sama berjalan serentak di banyak mesin tanpa satu pun baris kode kita berubah.

Kenapa Spark memilih desain malas seperti ini? Karena rencana yang lengkap bisa dioptimasi secara menyeluruh — optimizer Catalyst milik Spark menyusun ulang, menggabungkan, dan memangkas langkah-langkah sebelum satu byte pun diproses. Rantai filter() lalu select() kita, misalnya, dipangkas menjadi satu pass pada tabel di atas. Dengan arsitektur dan perilaku eksekusi yang sudah jelas, kita siap berkenalan dengan API kerja sehari-hari di atas Spark: Spark DataFrames.