Mengapa skema penting

Biaya payload tanpa kontrak pada sistem lintas tim dan apa yang diselesaikan skema tercatat.

Daftar isi

Pesan yang berpindah antar aplikasi di atas Kafka hanyalah rangkaian byte, sehingga makna isinya sepenuhnya bergantung pada kesepakatan antara tim yang menulis producer dan tim yang menulis consumer. Topik ini menunjukkan apa yang terjadi ketika kesepakatan itu hanya berupa kebiasaan, yaitu payload JSON yang dikirim tanpa kontrak tertulis, kemudian memperkenalkan skema tercatat sebagai kontrak yang dapat diperiksa mesin. Setelah membaca topik ini, batas antara perubahan payload yang aman dan yang merusak dapat ditetapkan dengan pasti.

Biaya payload tanpa kontrak#

JSON sering dipilih sebagai payload Kafka karena mudah dibaca dan tidak menuntut alat khusus. Kemudahan itu tidak disertai jaminan bentuk karena JSON tidak membawa definisi tipe yang mengikat, sehingga perubahan kecil pada producer tidak tertangkap sampai consumer memproses pesannya. Dalam satu tim, perubahan semacam itu biasanya cepat terlihat karena kode producer dan consumer saling bersentuhan. Pada sistem lintas tim, keduanya berada di dua layanan yang dikembangkan dan dirilis secara terpisah, sehingga perubahan dapat masuk ke produksi tanpa pernah melewati pemeriksaan bersama.

Demo berikut memperlihatkan dua perubahan diam-diam pada payload JSON yang sama. Producer mengirim data pesanan dan consumer menghitung total dengan mengalikan jumlah dengan harga. Perubahan yang disimulasikan adalah perubahan tipe pada harga, kemudian perubahan nama pada item.

python konsumen-tanpa-kontrak.py
import json

def proses_pesanan(payload):
    pesan = json.loads(payload)
    total = pesan["jumlah"] * pesan["harga"]
    print(f"total untuk {pesan['item']} = {total}")

# versi pertama dari producer: jumlah dan harga berupa angka
proses_pesanan('{"item": "pulpen", "jumlah": 3, "harga": 2500}')

# versi kedua: harga berubah menjadi string tanpa pemberitahuan
proses_pesanan('{"item": "pulpen", "jumlah": 3, "harga": "2500"}')

# versi ketiga: nama field "item" diganti menjadi "nama"
try:
    proses_pesanan('{"nama": "pulpen", "jumlah": 3, "harga": 2500}')
except KeyError as e:
    print(f"KeyError: field {e} tidak ditemukan")
text Keluaran
total untuk pulpen = 7500
total untuk pulpen = 250025002500
KeyError: field 'item' tidak ditemukan

Keluaran pertama adalah hasil yang diharapkan, yaitu 3 dikali 2500 sama dengan 7500. Keluaran kedua adalah korupsi data yang diam-diam, karena operator * pada Python mengalikan dua bilangan bulat tetapi mengulang string, sehingga "2500" * 3 menghasilkan string sepanjang 12 karakter dan tidak ada pesan error yang muncul. Laporan yang dibangun di atas data seperti ini salah tanpa jejak. Keluaran ketiga bersifat keras berupa KeyError, dan kegagalan itu menghentikan consumer sampai kode disesuaikan dengan payload barunya.

Bentuk perubahan yang sering terjadi#

Pergeseran bentuk data terhadap kesepakatan awal dikenal sebagai schema drift, dan payload JSON tidak memiliki mekanisme untuk menahannya. JSON memang tetap dapat dibaca apa pun bentuknya, tetapi sifat fleksibel itulah yang membuat consumer menerima data yang sudah tidak sesuai dengan asumsinya. Empat bentuk perubahan berikut sering muncul pada sistem yang berbagi payload tanpa kontrak, dan urutannya disusun berdasarkan seberapa cepat dampaknya terlihat.

Perubahan pada producerYang dilihat consumerDampak
Tipe field berubah, misalnya angka menjadi stringPerhitungan tetap berjalanData rusak masuk laporan tanpa pesan error
Field diganti namaKeyError saat mengakses field lamaConsumer berhenti sampai kode disesuaikan
Field wajib hilangKeyError atau nilai kosongData parsial diproses seolah utuh
Field baru ditambahkanDiabaikan diam-diamInformasi baru tidak sampai ke consumer lama

Dua baris pertama sudah ditunjukkan oleh demo sebelumnya. Baris ketiga biasanya muncul ketika producer baru menyambung ke topic yang sama tanpa mengirim satu field wajib, sehingga consumer menerima pesan parsial di tengah aliran data yang tampak normal. Baris terakhir tampak berdampak kecil tetapi sama menyesatkan, karena consumer lama terus berjalan seolah tidak ada informasi baru, sedangkan consumer baru yang dirancang untuk menggunakan informasi itu tidak pernah menerimanya. Bentuk payload tanpa kontrak dengan demikian hanya ditentukan oleh tim yang menulis producer, sedangkan tim consumer harus mengikuti setiap perubahan tanpa bisa menahannya.

Kontrak skema sebagai jawaban#

Kontrak skema adalah definisi bentuk payload yang ditulis secara eksplisit, disimpan di satu tempat, dan diperiksa oleh mesin pada setiap pesan. Dengan kontrak, perubahan tipe dan perubahan nama field tidak lagi menemukan consumer secara diam-diam, melainkan ditolak pada titik paling awal. Demo berikut menggunakan jsonschema, yaitu pustaka Python yang memvalidasi data terhadap skema JSON, untuk menahan ketiga payload yang sama dari demo pertama.

python validasi-jsonschema.py
import json
from jsonschema import validate, ValidationError

skema = {
    "type": "object",
    "properties": {
        "item": {"type": "string"},
        "jumlah": {"type": "integer"},
        "harga": {"type": "integer"}
    },
    "required": ["item", "jumlah", "harga"],
    "additionalProperties": False
}

def terima_pesanan(payload):
    pesan = json.loads(payload)
    try:
        validate(instance=pesan, schema=skema)
        print(f"pesan diterima: {pesan['item']} x{pesan['jumlah']}")
    except ValidationError as e:
        print(f"pesan ditolak: {e.message}")

terima_pesanan('{"item": "pulpen", "jumlah": 3, "harga": 2500}')
terima_pesanan('{"item": "pulpen", "jumlah": 3, "harga": "2500"}')
terima_pesanan('{"nama": "pulpen", "jumlah": 3, "harga": 2500}')
text Keluaran
pesan diterima: pulpen x3
pesan ditolak: '2500' is not of type 'integer'
pesan ditolak: 'item' is a required property

Payload pertama lolos karena cocok dengan skema, sedangkan dua payload berikutnya ditolak dengan pesan yang menyebut pelanggarannya secara spesifik. Validasi semacam ini ditempatkan pada sisi producer sebelum pesan dikirim, sehingga payload yang menyimpang tidak pernah sampai ke broker. Pemeriksaan di sisi consumer tetap berguna sebagai lapisan kedua, tetapi lapisan pertama harus berada sedekat mungkin dengan sumber datanya.

Skema yang digunakan validator juga tidak boleh menjadi konstanta yang disalin ke banyak layanan, karena salinan yang meleset membangkitkan ulang masalah yang sama dalam bentuk baru. Skema perlu disimpan di satu tempat yang dijadikan rujukan bersama, disunting lewat proses yang disepakati, dan diberi nomor versi supaya perubahannya dapat dilacak. Nomor versi itulah yang menjadi dasar penilaian kompatibilitas ketika skema berubah, karena perbandingan hanya sah dilakukan antar versi pada satu tempat yang sama.

Kesalahan umum#

Tiga kesalahan berikut muncul berulang ketika kontrak mulai diberlakukan. Ketiganya membuat kontrak tampak dipatuhi, tetapi penyaringannya muncul terlambat atau tidak lengkap. Sumbernya sama, yaitu asumsi bahwa payload tetap aman tanpa pemeriksaan eksplisit.

  1. Validasi hanya dipasang di sisi consumer. Kontrak yang hanya diperiksa di ujung terakhir menemukan payload rusak setelah pesan masuk ke broker, sehingga sumber masalahnya sulit dilacak. Pasang validasi pada producer sebelum pesan dikirim, lalu perlakukan pemeriksaan di consumer sebagai lapisan penangkal.
  2. Contoh payload digunakan sebagai kontrak. Contoh hanya memperlihatkan satu kasus dan tidak menyatakan tipe, batasan nilai, maupun field mana yang wajib. Ganti contoh dengan skema eksplisit yang dapat diperiksa mesin, lalu jadikan contoh payload sebagai ilustrasi pelengkap.
  3. Broker dianggap ikut memeriksa payload. Broker Kafka meneruskan byte tanpa menafsirkan isinya, sehingga tidak ada pemeriksaan skema yang terjadi di broker. Pemeriksaan harus ditempatkan pada kode producer sebelum pengiriman dan pada kode consumer saat pembacaan.

Ringkasan#

  • JSON tanpa kontrak membuat perubahan tipe dan nama field menemukan consumer tanpa peringatan, dengan dampak berupa data rusak yang diam-diam atau consumer yang berhenti.
  • Skema eksplisit yang diperiksa mesin menolak payload menyimpang pada titik paling awal, sebelum pesan sampai ke broker.
  • Validasi pada producer merupakan lapisan pertama, dan penyimpanan skema secara terpusat merupakan prasyarat agar kontrak dipegang oleh semua tim.

Referensi#