Avro dan kesadaran evolusi schema

Peran Avro sebagai format streaming beserta cara evolusi schema dijaga tetap kompatibel.

Daftar isi

Avro adalah format data biner yang menyimpan schema bersama datanya dan menjadi pilihan umum untuk pesan streaming. Pada sistem streaming, sisi pengirim dan sisi penerima sering dikembangkan pada waktu yang berbeda, sehingga schema data tidak boleh berubah tanpa aturan. Topik ini membahas anatomi schema Avro beserta cara evolusi schema dijaga tetap kompatibel, ditutup dengan pengujian kompatibilitas yang dijalankan langsung.

Mengapa streaming memilih Avro#

Pesan streaming berpindah jaringan secara terus-menerus, sehingga formatnya perlu ringkas pada byte dan tegas pada strukturnya. Avro menyimpan data dalam bentuk biner tanpa nama bidang pada setiap pesan, karena nama dan tipe bidang sudah dinyatakan di schemanya. Hasilnya, ukuran pesan jauh lebih kecil daripada JSON yang mengulang nama kunci pada setiap barisnya.

Ciri yang membedakan Avro dari format lain adalah kebiasaan menyertakan schema bersama data. Berkas Avro menyertakan schemanya di dalam header, sedangkan ekosistem Kafka lazim menyimpan schema terpusat pada schema registry, yakni layanan yang membagikan schema kepada sisi pengirim dan penerima. Dengan penempatan itu, kedua sisi tidak perlu bertukar berkas schema secara manual, dan versi schema yang dipakai tercatat dengan jelas.

Avro menata data per baris, serupa dengan CSV, sehingga satu pesan satu kejadian mudah ditulis dan dibaca utuh. Karakter ini berbeda dari Parquet yang menata per kolom, sehingga keduanya menempati peran yang berbeda di pipeline. Avro berperan pada perpindahan pesan, sedangkan Parquet berperan pada penyimpanan analitik.

Anatomi schema Avro#

Schema Avro ditulis dalam JSON dan lazim disimpan sebagai berkas .avsc. Isinya mendeklarasikan record, yakni tipe data utama Avro berupa kumpulan bidang bernama, beserta tipe setiap bidangnya.

json skema-pesanan.avsc
{
  "type": "record",
  "name": "Pesanan",
  "namespace": "id.tutorial.data",
  "fields": [
    {"name": "id_pesanan", "type": "string"},
    {"name": "tanggal", "type": "string"},
    {"name": "jumlah", "type": "int"}
  ]
}

Tipe bidangnya berasal dari kumpulan tipe dasar, yaitu string, int, long, float, double, boolean, bytes, dan null. Kumpulan tersebut dapat ditambah tipe majemuk seperti array dan map. Contoh di atas mendeklarasikan record bernama Pesanan dengan tiga bidang, dan setiap bidang wajib punya nama serta tipe. Namespace menandai record agar tidak bertabrakan dengan record bernama sama dari sumber lain.

Atribut default belum dipakai pada skema pertama ini, dan atribut itulah yang berperan penting saat skema berevolusi. Bidang dengan default dapat ditambahkan pada versi berikutnya tanpa memecah pembacaan data lama, sebagaimana ditunjukkan pada bagian berikutnya. Kebiasaan menyediakan default untuk setiap bidang baru mempersiapkan evolusi schema dalam jangka panjang.

Evolusi schema dan kompatibilitas#

Evolusi schema adalah perubahan pada schema seiring kebutuhan data berkembang, misalnya menambah bidang, menghapus bidang, atau memperluas tipe. Perubahan hanya aman apabila sisi pembaca dan sisi penulis tetap sepakat, dan kesepakatan itu dirumuskan sebagai tingkat kompatibilitas.

Dokumentasi Avro merujuk dua arah utama. Backward compatibility berarti schema terbaru dapat membaca data yang ditulis schema lama, sedangkan forward compatibility berarti schema lama tetap dapat membaca data yang ditulis schema terbaru. Istilah reader dan writer menyebut kedua sisi tersebut, yaitu schema yang membaca data dan schema yang menulisnya.

Beberapa perubahan aman dan beberapa lainnya tidak. Menambah bidang dengan default menjaga kompatibilitas backward karena bidang yang hilang pada data lama diisi default. Menambah bidang tanpa default memecahnya, karena data lama tidak memiliki nilai untuk bidang itu. Memperluas tipe angka dari int ke long aman karena spesifikasi Avro memperbolehkan promosi angka pada arah tersebut, sedangkan pergantian nama bidang dianggap dua perubahan sekaligus, yaitu menghapus satu bidang dan menambah bidang lain.

Berkas berikut adalah versi kedua dari schema pesanan, yang menambah bidang saluran dengan default dan memperluas jumlah dari int ke long.

json skema-pesanan-v2.avsc
{
  "type": "record",
  "name": "Pesanan",
  "namespace": "id.tutorial.data",
  "fields": [
    {"name": "id_pesanan", "type": "string"},
    {"name": "tanggal", "type": "string"},
    {"name": "jumlah", "type": "long"},
    {"name": "saluran", "type": "string", "default": "web"}
  ]
}

Menguji kompatibilitas dengan aturan sederhana#

Aturan kompatibilitas pada bagian sebelumnya cukup sederhana untuk diimplementasikan sendiri dengan modul json, tanpa pustaka Avro.

python uji-kompatibilitas.py
import json

# jalur promosi angka: tipe writer -> tipe reader yang sah
PROMOSI = {
    "int": {"long", "float", "double"},
    "long": {"float", "double"},
    "float": {"double"},
}


def baca_bidang(path):
    """Baca berkas .avsc dan kembalikan bidang sebagai dict berdasarkan nama."""
    with open(path, encoding="utf-8") as f:
        skema = json.load(f)
    return {b["name"]: b for b in skema["fields"]}


def uji(reader, writer, label):
    """Uji schema reader terhadap data yang ditulis schema writer."""
    print(label)
    aman = True
    for nama, bidang in reader.items():
        if nama not in writer:
            if "default" in bidang:
                print(f"  {nama}: tidak ada di writer, dipakai default -> aman")
            else:
                print(f"  {nama}: tidak ada di writer tanpa default -> GAGAL")
                aman = False
            continue
        tipe_writer = writer[nama]["type"]
        tipe_reader = bidang["type"]
        if tipe_writer == tipe_reader:
            print(f"  {nama}: {tipe_writer} -> {tipe_reader}, cocok")
        elif tipe_writer in PROMOSI and tipe_reader in PROMOSI[tipe_writer]:
            print(f"  {nama}: {tipe_writer} -> {tipe_reader}, cocok (promosi angka)")
        else:
            print(f"  {nama}: {tipe_writer} -> {tipe_reader}, GAGAL")
            aman = False
    print(f"  => {'KOMPATIBEL' if aman else 'INKOMPATIBEL'}")
    print()


v1 = baca_bidang("skema-pesanan.avsc")
v2 = baca_bidang("skema-pesanan-v2.avsc")

# schema v3 menirukan pergantian nama jumlah menjadi kuantitas tanpa default
v3 = {
    "id_pesanan": {"type": "string"},
    "tanggal": {"type": "string"},
    "kuantitas": {"type": "long"},
    "saluran": {"type": "string", "default": "web"},
}

uji(v2, v1, "A. backward, schema v2 membaca data v1")
uji(v1, v2, "B. forward, schema v1 membaca data v2")
uji(v3, v1, "C. reader v3 (nama berganti) membaca data v1")
text Keluaran
A. backward, schema v2 membaca data v1
  id_pesanan: string -> string, cocok
  tanggal: string -> string, cocok
  jumlah: int -> long, cocok (promosi angka)
  saluran: tidak ada di writer, dipakai default -> aman
  => KOMPATIBEL

B. forward, schema v1 membaca data v2
  id_pesanan: string -> string, cocok
  tanggal: string -> string, cocok
  jumlah: long -> int, GAGAL
  => INKOMPATIBEL

C. reader v3 (nama berganti) membaca data v1
  id_pesanan: string -> string, cocok
  tanggal: string -> string, cocok
  kuantitas: tidak ada di writer tanpa default -> GAGAL
  saluran: tidak ada di writer, dipakai default -> aman
  => INKOMPATIBEL

Skrip membaca kedua berkas .avsc dengan json.load, lalu menguji setiap bidang reader terhadap writer. Bidang yang tidak ada di writer dianggap aman hanya apabila reader menyediakan default. Perbedaan tipe diterima hanya apabila berada pada jalur promosi angka int, long, float, dan double. Bidang writer yang tidak dikenal reader sengaja tidak diuji karena reader mengabaikannya.

Tiga pengujian dijalankan. Pengujian A adalah backward, yaitu schema v2 membaca data v1, dan hasilnya kompatibel karena saluran punya default sementara jumlah memenuhi promosi angka. Pengujian B adalah forward, yaitu schema v1 membaca data v2, dan hasilnya inkompatibel karena int tidak dapat membaca long. Pengujian C menirukan pergantian nama tanpa default, dan hasilnya juga inkompatibel. Pola gagal yang tampak pada hasil di atas sama persis dengan aturan spesifikasi, sehingga pengujian seperti ini layak dijalankan sebelum schema baru dipasang.

Validasi dengan fastavro#

Pada pekerjaan nyata, validasi data dan pembacaan berkas Avro dikerjakan pustaka seperti fastavro, dan bentuk pemakaiannya tampak seperti berikut.

python Contoh: validasi data dengan fastavro
# tidak-dijalankan
# validasi data terhadap schema memerlukan pustaka fastavro
import json

import fastavro

with open("skema-pesanan.avsc", encoding="utf-8") as f:
    skema = json.load(f)

catatan = {"id_pesanan": "PSN-1042", "tanggal": "2026-01-12", "jumlah": 4}
fastavro.validation.validate(catatan, skema, raise_errors=True)

Pustaka fastavro perlu dipasang terpisah melalui pip, sehingga potongan di atas tidak dijalankan di artikel ini dan hanya berfungsi sebagai ilustrasi. Fungsi validate memeriksa kecocokan data terhadap schema dan melempar error apabila ada bidang yang tipenya tidak sesuai. Pustaka yang sama juga menyediakan pembaca dan penulis berkas .avro yang sesungguhnya.

Kesalahan umum#

Kesalahan evolusi schema biasanya baru terlihat saat penerima mulai gagal membaca data, sehingga pencegahannya harus dilakukan pada saat schema disunting.

  • Menambah bidang tanpa default. Data lama tidak memuat bidang itu sehingga pembaca baru gagal membacanya. Perbaikannya adalah menyertakan default pada setiap bidang baru.
  • Mengganti nama bidang dalam satu langkah. Perubahan terbaca sebagai penghapusan dan penambahan sekaligus. Perbaikannya adalah menambahkan bidang baru dengan default terlebih dahulu, memindahkan isinya, lalu menghapus bidang lama pada versi berikutnya.
  • Mengubah tipe di luar jalur promosi angka, misalnya int ke string. Pembacaan gagal di kedua arah. Perbaikannya adalah menambahkan bidang baru bertipe yang diinginkan, mengisi nilainya, lalu menghapus bidang lama.

Ringkasan#

  • Avro menyimpan schema bersama data binernya, sehingga struktur pesan streaming menjadi tegas dan ukurannya ringkas.
  • Kompatibilitas backward berarti schema terbaru membaca data lama, dan aturan amannya adalah menambah bidang dengan default serta membatasi perubahan tipe pada promosi angka.
  • Aturan tersebut cukup sederhana untuk diuji dengan modul json sebelum schema yang baru benar-benar dipasang.

Referensi#