Masalah berkas kecil

Kompaksi dan ukuran berkas target.

Daftar isi

Masalah berkas kecil adalah keadaan tabel yang tersimpan sebagai ribuan berkas partisi berukuran sangat kecil, dan keadaan ini menurunkan kinerja pembacaan maupun penulisan secara bersamaan. Topik ini membahas dari mana masalah itu berasal, angka ukuran yang lazim dijadikan target, dan kompaksi sebagai cara pemulihannya. Setelah membacanya, tanda-tanda tabel yang penuh berkas kecil dapat dikenali, dan penanganannya dapat dilakukan dengan metode yang murah.

Dari mana berkas kecil berasal#

Setiap task penulis menghasilkan satu berkas partisi per folder tujuannya, sehingga jumlah berkas keluaran mengikuti hasil kali jumlah partisi eksekusi dengan jumlah folder partisi. Pipeline yang membaca banyak berkas kecil dari sumbernya lalu menulis ulang tanpa penataan menghasilkan berkas kecil lagi, dan penulisan berkala dengan append yang tiap kali hanya memuat sedikit baris menambahnya terus-menerus. Pada cluster dengan ratusan partisi, satu penulisan kecil pun dapat melahirkan ratusan berkas.

Dampaknya terasa pada dua arah. Pembacaan membuka setiap berkas secara terpisah, dan biaya pembukaan, pembacaan footer, serta penjadwalan task mendominasi ketika berkasnya kecil-kecil. Sistem penyimpanan juga menderita karena metadatanya membengkak dan operasi daftar berkas melambat. Pada ekstremnya, jumlah berkas menjadi batas praktis ukuran tabel, jauh sebelum batas byte-nya tercapai.

Mengukur dan memadatkan#

Kompaksi adalah penulisan ulang isi tabel menjadi berkas yang lebih besar dan lebih sedikit, dan cara termudahnya adalah membaca tabel lalu menulisnya kembali dengan jumlah partisi yang disengaja. Skrip berikut membuat delapan baris data, menulisnya dengan repartition(8), lalu menulis versi padatnya dengan coalesce(1), dan membandingkan jumlah serta ukuran berkas keduanya.

python kompaksi.py
import os

from pyspark.sql import SparkSession

spark = (
    SparkSession.builder
    .master("local[2]")
    .appName("kompaksi")
    .config("spark.sql.shuffle.partitions", "2")
    .getOrCreate()
)

df = spark.createDataFrame(
    [(i, "siswa-%02d" % i, "10A") for i in range(1, 9)],
    "id long, nama string, kelas string",
)
target = "/tmp/lokasi/pq_kecil"

df.repartition(8).write.mode("overwrite").parquet(target + "_kecil")
df.coalesce(1).write.mode("overwrite").parquet(target + "_rapat")

def jumlah_berkas(path):
    return sum(1 for f in os.listdir(path) if f.endswith(".parquet"))

def total_byte(path):
    return sum(os.path.getsize(os.path.join(path, f)) for f in os.listdir(path) if f.endswith(".parquet"))

print("sebelum kompaksi :", jumlah_berkas(target + "_kecil"), "berkas,", total_byte(target + "_kecil"), "byte")
print("sesudah kompaksi :", jumlah_berkas(target + "_rapat"), "berkas,", total_byte(target + "_rapat"), "byte")

spark.stop()
text Keluaran
WARNING: Using incubator modules: jdk.incubator.vector
sebelum kompaksi : 5 berkas, 4529 byte
sesudah kompaksi : 1 berkas, 1083 byte

Keluaran memuat satu pelajaran tambahan yang tidak disengaja tetapi sangat nyata. Permintaan repartition(8) tidak menghasilkan delapan berkas, melainkan lima, karena AQE memadatkan partisi kecil saat menulis dan menurunkan jumlah task penulis. Pada data nyata, perilaku ini justru membantu, tetapi kejadian ini juga menegaskan bahwa jumlah berkas keluaran adalah hasil negosiasi antara jumlah partisi yang diminta, AQE, dan jumlah folder partisinya, bukan angka yang diperintahkan mentah-mentah.

Metode kompaksi di atas menulis ulang seluruh tabel, dan pada tabel terpartisi penulisannya dilakukan per folder partisi agar pemangkasan tetap berfungsi setelahnya. Bentuk lain yang lebih hemat adalah memadatkan hanya partisi yang bertambah, misalnya folder hari terakhir, karena sisanya tidak berubah.

Ukuran berkas target#

Ukuran yang lazim dijadikan acuan adalah kisaran ratusan megabita per berkas, dan angka yang sering disebut adalah sekitar 128 megabita sampai 1 gigabita, mengikuti ukuran blok penyimpanan dan pola bacaan format kolom. Berkas sebesar itu memuat cukup baris sehingga biaya pembukaannya bisa diabaikan, dan tetap cukup kecil untuk dibagi antar task secara nyaman.

Ukuran target itu bukan tujuan yang diperintahkan langsung ke Spark, melainkan hasil dari jumlah partisi eksekusi yang dipilih pada penulisan. Jika isi data per penulisan diperkirakan dan dibagi dengan ukuran target, jumlah partisi penulisan yang tepat dapat dihitung, misalnya data 10 gigabita dengan target 500 megabita menyisakan sekitar 20 partisi. AQE juga membantu memadatkan partisi kecil saat berjalan, tetapi angka awal yang masuk akal tetap menjadi tanggung jawab penulis pipeline.

Untuk tabel yang diperbarui terus-menerus, pengelolaan berkas kecil adalah pekerjaan rutin, dan format tabel seperti Delta Lake menyediakan mekanisme pemadatannya sebagai operasi bawaan, sehingga jadwal kompaksi tidak lagi ditulis sendiri.

Kesalahan umum#

Kesalahan pertama adalah menganggap lebih banyak berkas selalu lebih paralel. Pembacaan Parquet dapat membagi satu berkas besar menjadi beberapa task menurut row group-nya, sehingga paralelisme tidak bergantung pada jumlah berkas sekecil apa pun. Memecah data menjadi ribuan berkas hanya menambah biaya pembukaan tanpa menambah paralelisme.

Kesalahan kedua adalah memadatkan tanpa memikirkan folder partisi, misalnya coalesce(1) pada tabel terpartisi lalu menulis ulang penuh. Satu berkas per folder memang terbentuk, tetapi penulisan penuh yang dilakukan berkali-kali menyangkal manfaatnya. Padatkan per partisi yang memang perlu, dan jadwalkan pemadatannya pada waktu yang tidak membebani sistem penyimpanan.

Kesalahan ketiga adalah menyimpan pipeline perantara dengan jumlah partisi bawaan yang besar. Ratusan partisi pada data kecil melahirkan ratusan berkas hampir kosong pada setiap tahap, dan masalah itu menumpuk antar tahap. Turunkan jumlah partisi penulisan pada data kecil, dan biarkan AQE bekerja pada data yang ukurannya berubah-ubah.

Ringkasan#

  • Berkas kecil lahir dari perkalian partisi eksekusi, folder partisi, dan penulisan berkala yang kecil-kecil, lalu membebani pembacaan dan sistem penyimpanan sekaligus.
  • Kompaksi menulis ulang tabel dengan jumlah partisi yang disengaja, dan coalesce lebih murah daripada repartition untuk mengecilkan saat menulis.
  • Ukuran target yang lazim adalah ratusan megabita per berkas, dan jumlah partisi penulisan dihitung dari pembagian isi data dengan target itu.
  • AQE ikut memadatkan partisi kecil saat penulisan, sehingga jumlah berkas keluaran adalah hasil negosiasi, bukan perintah mentah.

Referensi#