Partisi adalah satuan pembagian data terkecil yang dijadwalkan Spark, dan hampir semua pengaturan kinerja pada akhirnya kembali ke jumlah serta ukuran partisi. Topik ini membahas cara memeriksa dan mengubah jumlah partisi, perbedaan repartition dan coalesce, partisi berbasis kunci dengan repartition("kolom"), dan masalah skew ketika isi partisi timpang. Setelah membacanya, keputusan menata ulang partisi dapat diambil dengan alasan yang terukur, bukan dengan tebakan.
Partisi sebagai satuan paralelisme#
Setiap DataFrame terbagi ke dalam sejumlah partisi, dan setiap partisi dieksekusi sebagai satu task pada satu stage. Jumlah partisi menentukan tingkat paralelisme maksimum, sedangkan ukuran rata-rata partisi menentukan bobot tiap task. Kombinasi keduanya harus seimbang, karena partisi terlalu banyak membuat task kecil-kecil yang biaya penjadwalannya mendominasi, sedangkan partisi terlalu sedikit membuat task raksasa yang memakan memori dan mengurangi paralelisme.
Jumlah partisi awal DataFrame dari data literal mengikuti spark.default.parallelism, yang pada mode lokal local[2] bernilai 2. Skrip berikut memeriksa jumlah itu lalu mengubahnya dengan dua metode yang pemakaiannya sering tertukar, yaitu repartition dan coalesce, dan menandai letak baris dengan F.spark_partition_id().
from pyspark.sql import SparkSession
from pyspark.sql import functions as F
spark = (
SparkSession.builder
.master("local[2]")
.appName("peraga-partisi")
.config("spark.sql.shuffle.partitions", "2")
.getOrCreate()
)
df = spark.createDataFrame(
[(1, "Ali", "10A", 85), (2, "Budi", "10A", 90), (3, "Citra", "10B", 78), (4, "Dewi", "10B", 92)],
"id long, nama string, kelas string, nilai long",
)
print("=== partisi awal ===")
print("jumlah partisi:", df.rdd.getNumPartitions())
print("=== repartition(3) ===")
lebar = df.repartition(3)
print("jumlah partisi:", lebar.rdd.getNumPartitions())
lebar.withColumn("partisi", F.spark_partition_id()).orderBy("partisi", "id").show()
print("=== coalesce(1) dari hasil repartition ===")
sempit = lebar.coalesce(1)
print("jumlah partisi:", sempit.rdd.getNumPartitions())
print("=== repartition by kolom kelas ===")
kunci = df.repartition("kelas")
kunci.withColumn("partisi", F.spark_partition_id()).orderBy("partisi", "id").show()
spark.stop()WARNING: Using incubator modules: jdk.incubator.vector
=== partisi awal ===
jumlah partisi: 2
=== repartition(3) ===
jumlah partisi: 3
+---+-----+-----+-----+-------+
| id| nama|kelas|nilai|partisi|
+---+-----+-----+-----+-------+
| 1| Ali| 10A| 85| 0|
| 2| Budi| 10A| 90| 1|
| 4| Dewi| 10B| 92| 1|
| 3|Citra| 10B| 78| 2|
+---+-----+-----+-----+-------+
=== coalesce(1) dari hasil repartition ===
jumlah partisi: 1
=== repartition by kolom kelas ===
+---+-----+-----+-----+-------+
| id| nama|kelas|nilai|partisi|
+---+-----+-----+-----+-------+
| 1| Ali| 10A| 85| 0|
| 2| Budi| 10A| 90| 0|
| 3|Citra| 10B| 78| 0|
| 4| Dewi| 10B| 92| 0|
+---+-----+-----+-----+-------+Keluaran menunjukkan tiga perilaku sekaligus. repartition(3) memperlebar partisi ke tiga dan mendistribusikan baris secara merata dengan round robin, terlihat dari baris yang tersebar di partisi 0 sampai 2. coalesce(1) menyempitkannya kembali menjadi satu tanpa shuffle, dan repartition("kelas") membagi data menurut hash kunci, yang pada data kecil ini justru menempatkan kedua kelas di partisi yang sama karena hanya ada dua nilai kunci pada ruang hash yang besar.
repartition versus coalesce#
Keduanya mengubah jumlah partisi, tetapi mekanisme dan biayanya berbeda. repartition(n) mengatur ulang data penuh melalui shuffle, sehingga bisa memperlebar maupun menyempitkan, dan hasilnya merata menurut ukuran. coalesce(n) hanya menggabungkan partisi yang ada pada executor yang sama tanpa pertukaran data, sehingga murah, tetapi hanya efektif untuk mempersempit dan hasilnya bisa timpang karena menggabungkan apa adanya.
Aturan pemakaiannya mengikuti arah perubahan. Untuk memperlebar partisi, hanya repartition yang mampu, dan biaya shufflenya dibenarkan oleh kebutuhan paralelisme. Untuk mempersempit sebelum penulisan keluaran, coalesce lebih disukai karena tanpa shuffle, dengan catatan isinya diterima apa adanya. Kombinasi keduanya pada skrip, yaitu repartition(3) diikuti coalesce(1), adalah pola yang sah ketika meratakan dahulu sebelum menyempitkan memang diperlukan, meskipun untuk kasus sederhana coalesce(1) langsung sudah cukup.
Bentuk repartition("kolom") membagi data menurut hash nilai kolomnya dan berguna ketika baris dengan kunci yang sama perlu bersama, misalnya sebelum penulisan terpartisi atau sebelum operasi yang sensitif terhadap letak kunci. Bentuk ini tetap sebuah shuffle penuh, sehingga jumlah partisi tujuannya mengikuti spark.sql.shuffle.partitions kecuali dinyatakan lain.
Skew: partisi yang isinya timpang#
Skew adalah kondisi ketika satu partisi memuat data jauh lebih banyak daripada yang lain, dan akibatnya satu task bertahan lama sementara task lain menunggu. Gejalanya mudah dikenali pada Spark UI, yaitu stage yang durasi totalnya ditentukan oleh satu task dengan durasi jauh lebih besar dari median. Penyebab umumnya adalah kunci yang distribusinya timpang, misalnya satu nilai kunci mendominasi data, atau partisi berbasis kunci yang jumlah kuncinya sedikit seperti contoh repartition("kelas") di atas.
Penanganannya beragam dan dibahas lebih lanjut pada modul produksi, mulai dari menambah kunci penggaraman (salting) pada data yang timpang, membiarkan AQE memecah partisi besar secara otomatis pada Spark modern, sampai menata ulang kueri agar kunci yang menonjol ditangani terpisah. Yang penting pada tahap ini adalah kemampuan mengenalinya dari pola task yang timpang, karena keputusan penanganannya selalu mengikuti bentuk datanya.
Kesalahan umum#
Kesalahan pertama adalah memanggil repartition berulang-ulang di tengah pipeline. Setiap panggilan adalah shuffle penuh, dan dua panggilan berurutan dengan tujuan berbeda membuang pekerjaan yang pertama. Partisi disusun pada titik yang memang memerlukannya, misalnya sebelum penulisan keluaran, bukan di setiap tahap.
Kesalahan kedua adalah memakai coalesce dengan angka yang jauh lebih besar dari partisi sekarang. coalesce hanya menggabungkan, sehingga permintaan yang memperlebar diabaikan diam-diam dan jumlah partisi tidak bertambah. Untuk memperlebar, gunakan repartition yang memang dirancang untuk itu.
Kesalahan ketiga adalah menilai kesehatan partisi dari jumlah baris semata. Baris yang panjangnya berbeda jauh membuat partisi seimbang secara baris tetap timpang secara byte, dan biaya eksekusi mengikuti byte. Pemeriksaan yang lebih jujur memakai ukuran data per partisi, misalnya melalui ringkasan Spark UI atau pengukuran ukuran berkas pada penulisan terpartisi yang dibahas pada modul berkas dan format.
Ringkasan#
- Jumlah partisi menentukan paralelisme dan ukuran rata-ratanya menentukan bobot task, dan keduanya harus seimbang.
repartitionmenata ulang penuh melalui shuffle dan dapat memperlebar maupun menyempitkan, sedangkancoalescemenggabungkan tanpa shuffle dan hanya efektif untuk menyempitkan.repartition("kolom")membagi data menurut hash kunci dan tetap merupakan shuffle, dan pada ruang kunci kecil hasilnya bisa timpang.- Skew terlihat sebagai satu task yang jauh lebih lama pada stage yang sama, dan penanganannya mengikuti bentuk distribusi datanya.
Referensi#
- Tuning Guide — Apache Spark — Hubungan paralelisme, jumlah partisi, dan ukuran task.
- pyspark.sql.DataFrame.repartition — Apache Spark — Dokumentasi bentuk repartition dengan jumlah maupun kolom.
- pyspark.sql.DataFrame.coalesce — Apache Spark — Dokumentasi penyempitan partisi tanpa shuffle.