Setiap bulan pipeline TokoKita menambah data baru ke data lake, dan tagihan object storage naik mengikutinya pelan-pelan. Biaya penyimpanan adalah hasil kali tiga faktor: ukuran data, harga per GB, dan berapa lama data disimpan — dan ketiganya bisa ditekan tanpa membuang satu baris pun. Chapter Kompresi Data membandingkan codec dari sisi rasio dan kecepatan; chapter Object Storage memperkenalkan storage class dan lifecycle policy. Bab ini menyatukan keduanya menjadi keputusan biaya: format apa yang dipakai, bagaimana menata berkas, dan kapan data boleh dihapus.
Empat Format, Satu Dataset, Hitungan Dolar #
Satu dataset pesanan satu juta baris ditulis ke empat format, lalu query GMV yang sama dijalankan tiga kali per format. Kolom biaya menerjemahkan ukuran menjadi dolar: rasio ukuran dikalikan harga 1 TB selama 12 bulan di storage class standar (USD 0,023/GB-bulan):
HARGA_STANDARD = 0.023 # USD per GB per bulan (storage class standar)
QUERY = "SELECT kota, sum(qty * harga) AS gmv FROM {} GROUP BY kota ORDER BY kota"
def ukur_baca(sumber, csv_mode):
fn = f"read_csv('{sumber}')" if csv_mode else f"read_parquet('{sumber}')"
runs = []
for _ in range(3):
t0 = time.perf_counter()
con.execute(QUERY.format(fn)).fetchall()
runs.append(time.perf_counter() - t0)
return min(runs)
ukuran_csv = os.path.getsize(path_csv)
print(f"{'format':20s} {'ukuran':>9s} {'rasio':>6s} {'biaya/thn*':>10s} "
f"{'baca terbaik':>13s}")
for label, sumber, csv_mode in [
("csv", str(path_csv), True),
("csv.gz (level 6)", str(path_gz), True),
("parquet + snappy", str(BASE / "pesanan-snappy.parquet"), False),
("parquet + zstd", str(BASE / "pesanan-zstd.parquet"), False),
]:
ukuran = os.path.getsize(sumber)
rasio = ukuran / ukuran_csv
# biaya setahun untuk volume data yang sama, disimpan di storage class
# standar, jika data mentahnya seukuran 1 TB versi csv
biaya = rasio * 1000 * HARGA_STANDARD * 12
print(f"{label:20s} {ukuran / 1024 / 1024:6.1f} MB {rasio:6.2f} "
f"{biaya:9.0f}$ {ukur_baca(sumber, csv_mode):11.3f} s")
print("* biaya per tahun bila volume data mentah setara 1 TB versi csv, "
"disimpan di storage class standar")Output yang Dihasilkan (bagian 1 dari 3):
=== Bagian 1: satu dataset, empat format ===
format ukuran rasio biaya/thn* baca terbaik
csv 27.9 MB 1.00 276$ 0.116 s
csv.gz (level 6) 7.6 MB 0.27 75$ 0.356 s
parquet + snappy 7.3 MB 0.26 72$ 0.015 s
parquet + zstd 4.4 MB 0.16 43$ 0.016 s
* biaya per tahun bila volume data mentah setara 1 TB versi csv, disimpan di storage class standarMari kita telaah bagian-bagian penting kode di atas:
- Baris 59-66 (
ukur_baca): query yang sama dijalankan tiga kali per format dan run tercepat dilaporkan, karena derau eksternal hanya bisa menambah waktu. - Baris 71-76 (daftar format): keempat format memuat data yang identik — syarat perbandingan yang adil.
- Baris 81 (hitung biaya): rasio ukuran dikalikan
1000 GB × harga × 12 bulan; kolom ini mengubah byte menjadi estimasi dolar per tahun. - Baris 82-83 (cetak baris): ukuran, rasio, biaya, dan waktu baca tercetak satu baris per format.
Parquet + zstd memenangkan dua kategori sekaligus: paling kecil (16% dari csv; biaya setahun $43 versus $276) dan nyaris paling cepat dibaca. csv.gz memangkas ukuran menjadi 27%, tetapi waktu bacanya lebih dari 22 kali lipat Parquet — dekompresi teks baris demi baris dibayar pada setiap query. csv polos kalah di semua kategori kecuali kompatibilitas, karena hampir semua alat bisa membacanya. Pada volume 1 TB, selisih csv versus Parquet + zstd mencapai $233 per tahun untuk data yang sama persis; format berubah dari preferensi teknis menjadi keputusan anggaran.
Masalah Berkas Kecil dan Kompaksi #
Bentuk pemborosan kedua datang dari cara menulis, bukan format. Pipeline yang menulis satu Parquet kecil setiap beberapa menit menghasilkan 500 berkas berisi 2.000 baris; footer, skema, dan statistik per berkas dibayar 500 kali. Penulisan 500 berkas tersebut dan fungsi ukur_glob dipangkas dari tampilan (pola ukurnya sama dengan ukur_baca), dan bagian berikut mengukur keduanya lalu menggabungkannya:
berkas_kecil = sorted((BASE / "kecil").glob("*.parquet"))
ukuran_kecil = sum(b.stat().st_size for b in berkas_kecil)
t_kecil, hasil_kecil = ukur_glob(f"{BASE}/kecil/*.parquet")
print(f"500 berkas kecil : total {ukuran_kecil / 1024 / 1024:8.1f} MB, "
f"baca {t_kecil:.3f} s")
# kompaksi: gabungkan 500 berkas menjadi 1
t0 = time.perf_counter()
tabel_utuh = pq.read_table(BASE / "kecil")
pq.write_table(tabel_utuh, BASE / "pesanan-kompaksi.parquet",
compression="snappy")
durasi_kompaksi = time.perf_counter() - t0
ukuran_besar = os.path.getsize(BASE / "pesanan-kompaksi.parquet")
t_besar, hasil_besar = ukur_glob(str(BASE / "pesanan-kompaksi.parquet"))
print(f"1 berkas kompaksi: total {ukuran_besar / 1024 / 1024:8.1f} MB, "
f"baca {t_besar:.3f} s (proses kompaksi {durasi_kompaksi:.2f} s)")
overhead = (ukuran_kecil / ukuran_besar - 1) * 100
print(f"total 500 berkas {overhead:.0f}% lebih besar dari hasil kompaksi; "
f"hasil query identik: {hasil_kecil == hasil_besar}")Output yang Dihasilkan (bagian 2 dari 3):
=== Bagian 2: 500 berkas kecil vs 1 berkas hasil kompaksi ===
500 berkas kecil : total 12.8 MB, baca 0.035 s
1 berkas kompaksi: total 7.3 MB, baca 0.016 s (proses kompaksi 0.32 s)
total 500 berkas 74% lebih besar dari hasil kompaksi; hasil query identik: TrueTotal 500 berkas 74% lebih besar dari satu berkas hasil kompaksi — metadata per berkas menumpuk pada volume kecil. Query membaca 500 berkas dalam 0,035 detik versus 0,016 detik setelah kompaksi, dan hasil querynya identik. Kompaksi sendiri memakan 0,32 detik sekali jalan; selisih 19 milidetik per query membuat biayanya tertutup setelah sekitar 17 pembacaan. Karena itu kompaksi dijadwalkan berkala — harian atau mingguan — bukan setiap run: menulis berkas kecil memang menguntungkan latensi ingestion, dan sisi throughput-nya dibahas chapter Batch Size dan Throughput.
Matematika Retensi dan Tiering #
Bentuk pemborosan ketiga: berapa lama data disimpan. Simulasi berikut membandingkan dua kebijakan selama 24 bulan pada 120 GB data baru per bulan. Kebijakan A menyimpan semuanya di kelas standar tanpa menghapus; kebijakan B menurunkan kelas tiap kohort bulanan (bulan pertama standar, umur 1-3 bulan infrequent access, setelahnya Glacier Instant Retrieval) dan menghapus kohort berumur 12 bulan:
GB_PER_BULAN = 120.0
HARGA = {"STANDARD": 0.023, "STANDARD_IA": 0.0125, "GLACIER_IR": 0.004}
total_semua_standard = 0.0
total_tiering = 0.0
for bulan in range(1, 25): # bulan tagihan ke-1..24
for umur in range(bulan): # umur kohort: 0..bulan-1
total_semua_standard += GB_PER_BULAN * HARGA["STANDARD"]
if umur < 12: # kohort dihapus setelah berumur 12 bulan
if umur < 1:
kelas = "STANDARD"
elif umur < 3:
kelas = "STANDARD_IA"
else:
kelas = "GLACIER_IR"
total_tiering += GB_PER_BULAN * HARGA[kelas]
print(f"kebijakan A (semua standard, tanpa hapus) : ${total_semua_standard:,.0f}")
print(f"kebijakan B (tiering + hapus umur 12 bln) : ${total_tiering:,.0f} "
f"({(1 - total_tiering / total_semua_standard) * 100:.0f}% lebih murah)")Output yang Dihasilkan (bagian 3 dari 3):
=== Bagian 3: matematika retensi dan tiering (24 bulan) ===
kebijakan A (semua standard, tanpa hapus) : $828
kebijakan B (tiering + hapus umur 12 bln) : $207 (75% lebih murah)Loop dua tingkat berjalan per kohort: setiap bulan tagihan membayar semua kohort yang masih hidup sesuai umurnya. Hasilnya $828 versus $207 — 75% lebih murah tanpa mengubah satu byte data. Kebijakan B menuntut dua syarat: konsumen data mentah menyetujui penghapusan setelah 12 bulan, dan pembacaan data lama membayar biaya retrieval kelas dingin. Di object storage nyata, aturan seperti ini diotomatisasi lewat lifecycle policy dari chapter Object Storage; matematika di atas adalah bahan untuk memutuskan kebijakannya.
Spesifikasi format Parquet terdokumentasi di dokumentasi resmi Parquet , dan karakteristik algoritma zstd di repositori resmi zstd .
Format, layout, dan retensi kini menjadi keputusan yang dihitung, bukan diraba. Satu pertanyaan terakhir menutup section ini: dari mana semua angka tersebut berasal, dan bagaimana orang lain bisa mempercayai serta mengulangnya — metodologi chapter Benchmarking.