Format teks seperti CSV dan JSON mengulang nama field di setiap baris dan menulis angka sebagai deretan karakter — boros ruang dan lambat di-parse ketika data mencapai jutaan baris. Apache Avro menjawabnya dengan format biner berbasis baris yang selalu dipasangkan dengan schema: struktur data didefinisikan sekali dalam JSON, lalu data disimpan sebagai byte padat tanpa pengulangan nama field. Avro lahir dari ekosistem Hadoop dan kini menjadi format pesan populer di Kafka. Chapter ini menguji semuanya secara nyata dengan fastavro dan data transaksi toko yang sama.

Schema Avro yang Ditulis dalam JSON #

Schema Avro disimpan sebagai dokumen JSON, biasanya dalam berkas .avsc. Schema mendefinisikan tipe record berisi daftar fields, dan setiap field wajib punya name serta type — misalnya int untuk angka dan string untuk teks. Karena schema-nya JSON, schema mudah disimpan di repository Git atau di Schema Registry.

Library fastavro menyediakan fungsi writer() untuk menulis Object Container File (OCF) berakhiran .avro dan reader() untuk membacanya kembali. Data Python biasa — dictionary — langsung diterima tanpa konversi manual:

python
from fastavro import parse_schema, reader, writer

schema = {
    "type": "record",
    "name": "Transaksi",
    "namespace": "toko.nusantara",
    "fields": [
        {"name": "id", "type": "int"},
        {"name": "kasir", "type": "string"},
        {"name": "produk", "type": "string"},
        {"name": "qty", "type": "int"},
        {"name": "harga", "type": "int"},
    ],
}

records = [
    {"id": 1, "kasir": "Budi", "produk": "Kopi Robusta", "qty": 2, "harga": 45000},
    {"id": 2, "kasir": "Siti", "produk": "Teh Melati", "qty": 3, "harga": 15000},
    {"id": 3, "kasir": "Dewi", "produk": "Susu UHT 1L", "qty": 2, "harga": 18000},
]

with open("transaksi.avro", "wb") as berkas:
    writer(berkas, parse_schema(schema), records)

with open("transaksi.avro", "rb") as berkas:
    pembaca = reader(berkas)
    print("schema tertanam:", pembaca.writer_schema["name"])
    for record in pembaca:
        print(record)

Output yang Dihasilkan:

text
schema tertanam: toko.nusantara.Transaksi
{'id': 1, 'kasir': 'Budi', 'produk': 'Kopi Robusta', 'qty': 2, 'harga': 45000}
{'id': 2, 'kasir': 'Siti', 'produk': 'Teh Melati', 'qty': 3, 'harga': 15000}
{'id': 3, 'kasir': 'Dewi', 'produk': 'Susu UHT 1L', 'qty': 2, 'harga': 18000}

Mari kita telaah baris-baris kode di atas:

  1. Baris 3-15 (schema = {...}): tipe record bernama Transaksi dengan lima field bertipe int dan string — namespace berfungsi seperti nama paket agar Transaksi tidak bentrok dengan record lain.
  2. Baris 17-21 (records = [...]): data transaksi berupa dictionary biasa; id dan harga sudah integer sehingga tidak perlu konversi seperti pada CSV dan XML.
  3. Baris 23-24 (writer(berkas, parse_schema(schema), records)): parse_schema() memvalidasi schema, lalu writer() menulis berkas biner .avro — schema ikut tertanam di header berkas.
  4. Baris 26-29 (reader(berkas)): pembacaan tidak menyertakan schema karena reader() mengambil schema tertanam dari header — baris writer_schema["name"] membuktikannya; hasilnya dictionary dengan tipe data asli.

Schema Evolution: Menambah Field dengan Default #

Kekuatan utama Avro adalah schema evolution: penulis (writer) dan pembaca (reader) boleh memakai versi schema berbeda, selama aturan kompatibilitas dipenuhi. Aturan paling penting: field baru wajib punya default agar data lama tetap terbaca. Kita definisikan schema versi kedua dengan field diskon, lalu membaca berkas lama yang ditulis tanpa field tersebut:

python
from fastavro import parse_schema, reader

schema_v2 = {
    "type": "record",
    "name": "Transaksi",
    "namespace": "toko.nusantara",
    "fields": [
        {"name": "id", "type": "int"},
        {"name": "kasir", "type": "string"},
        {"name": "produk", "type": "string"},
        {"name": "qty", "type": "int"},
        {"name": "harga", "type": "int"},
        {"name": "diskon", "type": "int", "default": 0},
    ],
}

with open("transaksi.avro", "rb") as berkas:
    for record in reader(berkas, reader_schema=parse_schema(schema_v2)):
        print(record)

Output yang Dihasilkan:

text
{'id': 1, 'kasir': 'Budi', 'produk': 'Kopi Robusta', 'qty': 2, 'harga': 45000, 'diskon': 0}
{'id': 2, 'kasir': 'Siti', 'produk': 'Teh Melati', 'qty': 3, 'harga': 15000, 'diskon': 0}
{'id': 3, 'kasir': 'Dewi', 'produk': 'Susu UHT 1L', 'qty': 2, 'harga': 18000, 'diskon': 0}

Parameter reader_schema memberi tahu reader() bentuk data yang diinginkan pembaca. Karena data lama tidak memuat diskon, nilai 0 dari default diisi otomatis — data lama dan baru hidup berdampingan tanpa penulisan ulang.

Ukuran File: Avro vs JSON Lines #

Seberapa padat hasilnya? Kita tulis 1.000 transaksi sintetis ke .jsonl dan .avro dengan schema yang sama, lalu bandingkan ukuran berkasnya:

python
import json
import os

from fastavro import parse_schema, writer

records_besar = []
for i in range(1, 1001):
    records_besar.append(
        {
            "id": i,
            "kasir": ["Budi", "Siti", "Dewi"][i % 3],
            "produk": ["Kopi Robusta", "Teh Melati", "Susu UHT 1L"][i % 3],
            "qty": 1 + i % 5,
            "harga": 15000 + (i % 10) * 1000,
        }
    )

with open("transaksi_besar.avro", "wb") as berkas:
    writer(berkas, parse_schema(schema), records_besar)

with open("transaksi_besar.jsonl", "w", encoding="utf-8") as berkas:
    for r in records_besar:
        berkas.write(json.dumps(r, ensure_ascii=False) + "\n")

print("1000 baris .jsonl:", os.path.getsize("transaksi_besar.jsonl"), "byte")
print("1000 baris .avro :", os.path.getsize("transaksi_besar.avro"), "byte")

Output yang Dihasilkan:

text
1000 baris .jsonl: 79892 byte
1000 baris .avro : 23267 byte

Berkas Avro sekitar 3,4 kali lebih kecil. Dua penyebab utamanya: nama field hanya ditulis sekali di header (JSONL mengulang lima nama field di setiap baris), dan angka disimpan sebagai bilangan biner berukuran variabel. Perlu dicatat, untuk berkas sangat kecil header schema justru membuat Avro lebih besar — keuntungan muncul saat data bertambah banyak.

Kenapa Avro di Ekosistem Kafka dan Hadoop #

Empat alasan menjelaskan posisi Avro di dokumentasi resminya dan di industri:

  • Schema Registry: Confluent Schema Registry menyimpan schema Avro dan cukup mengirim schema id kecil di setiap pesan Kafka — pesan tetap ringkas, struktur tetap jelas.
  • Berbasis baris: satu pesan memuat satu record utuh, pas untuk streaming yang memproses pesan satu per satu.
  • Schema evolution: kombinasi writer schema dan reader schema membuat producer dan consumer naik versi secara terpisah.
  • Splittable: berkas .avro memuat penanda sink berkala sehingga bisa dipecah dan diproses paralel oleh Hadoop MapReduce atau Spark.

Avro memadatkan data per baris. Tetapi analitik warehouse jarang membaca seluruh kolom — query hanya butuh beberapa kolom dari jutaan baris, dan di situlah format kolom Parquet unggul.