Petunjuk dan konteks
Rancang event log untuk layanan akun multi-tenant. Setiap perubahan saldo, izin, atau konfigurasi memancarkan sebuah event, dan konsumen dapat memutar ulang riwayat untuk merekonstruksi state. Kegagalan instans tidak boleh memindai riwayat tanpa batas dari awal. Sistem memerlukan operasi append dengan throughput tinggi, pengurutan per tenant, progres konsumen yang independen, pemulihan snapshot, dan biaya penyimpanan yang terbatas. Jelaskan penghapusan, event yang tidak berurutan, dan evolusi skema.
Topik ini cocok untuk wawancara system design tingkat senior, platform, dan infrastruktur data. Artikel Event Sourcing karya Martin Fowler mendefinisikan ide inti untuk menyimpan perubahan state sebagai urutan event. Dokumentasi desain Apache Kafka menjelaskan log terpartisi, posisi konsumen, dan pemadatan log (log compaction) yang mempertahankan nilai terbaru untuk setiap kunci. Pertanyaan wawancara system design publik juga mencantumkan log append-only terpartisi, replikasi, retensi, pemadatan, dan pemulihan sebagai area evaluasi. Sumber-sumber ini mendukung representasi topik tersebut, tetapi tidak menetapkan pertanyaan pasti dari perusahaan tertentu atau frekuensi wawancara. Kategorinya adalah system-design karena keterampilan intinya adalah konsistensi end-to-end, batas pemulihan, dan kompromi kapasitas.
Apa yang dievaluasi oleh pewawancara
Pertama, dapatkah kandidat memisahkan riwayat event dari current state yang dimaterialisasi? Snapshot adalah lapisan akselerasi turunan; snapshot tidak dapat menggantikan event yang tidak dapat diubah (immutable) atau melintasi batas event yang tidak konsisten.
Kedua, apakah partition key memenuhi persyaratan pengurutan dan skalabilitas? Pemartisian berdasarkan tenant atau aggregate ID mempertahankan urutan satu agregat, tetapi hot tenant, transaksi lintas-agregat, dan pengurutan global memerlukan batasan eksplisit.
Ketiga, apakah mereka memahami semantik pemadatan? Pemadatan kunci mempertahankan catatan terbaru per kunci dan cocok untuk aliran perubahan state; ini bukan riwayat event lengkap. Penghapusan memerlukan tombstone dan jendela retensi, dan konsumen tidak dapat berasumsi bahwa offset arbitrer memiliki arti yang sama sebelum dan sesudah pemadatan.
Terakhir, apakah mereka mencakup aspek operasional: validasi snapshot, versi event, checkpoint atomik, acknowledgement replikasi, biaya retensi, consumer lag, dan kompatibilitas skema harus dapat diobservasi.
Pertanyaan klarifikasi untuk diajukan terlebih dahulu
- Apakah event merupakan fakta audit yang tidak dapat diubah atau perubahan current-state yang dapat dibangun kembali? Pengauditan memerlukan riwayat lengkap; aliran state dapat menggunakan pemadatan kunci.
- Apa cakupan pengurutannya? Hanya dalam agregat yang sama, per tenant, atau global? Setiap jaminan mengubah strategi pemartisian dan throughput.
- Haruskah konsumen memutar ulang dari titik waktu mana pun? Jika ya, pemadatan kunci terbaru saja tidak cukup; simpan arsip atau log audit terpisah.
- Siapa yang membuat dan memvalidasi snapshot? Produser, konsumen, dan worker snapshot memiliki tanggung jawab berbeda; ikat snapshot ke posisi log dan versi skema.
- Bagaimana penghapusan direpresentasikan? Tombstone berversi atau event penghapusan bisnis memiliki arti pemadatan dan kepatuhan yang berbeda.
Kerangka jawaban 30 detik
“Saya mempartisi event setiap agregat berdasarkan aggregate ID, hanya melakukan append, dan memberikan acknowledgement kepada produser pada offset terkomit yang telah direplikasi. Event mencakup ID event, versi agregat, versi skema, dan stempel waktu (timestamp). Konsumen melakukan checkpoint offset setelah memperbarui materialized view mereka, menggunakan ID event untuk deduplikasi at-least-once, dan melakukan rekonstruksi dari snapshot ditambah event-event berikutnya. Snapshot menyimpan state, offset event terakhirnya, dan versi skema; pemulihan memvalidasinya sebelum memutar ulang offset berikutnya. Pemadatan kunci hanya berlaku untuk aliran current-state yang dapat dibangun kembali dan mempertahankan tombstone selama jendela waktu yang ditentukan; event audit masuk ke arsip yang tidak dipadatkan. Saya memantau replication lag, consumer lag, usia snapshot, backlog pemadatan, dan perbedaan verifikasi pemutaran ulang.”
Jawaban mendalam
1. Tentukan field event dan batas pengurutan
Sebuah event mencakup eventId, aggregateId, aggregateVersion, schemaVersion, payload, waktu pembuatan, dan sumber. aggregateVersion meningkat secara monoton untuk satu agregat. Operasi conditional append menolak versi lama, mencegah dua penulis konkuren saling menimpa data secara diam-diam.
Gunakan aggregate ID sebagai partition key agar satu agregat masuk ke dalam satu log yang terurut. Jangan menjanjikan urutan global lintas-partisi. Jika produk memerlukan fakta atomik lintas-agregat, enkode hasil transaksi sebagai satu event agregat atau gunakan transactional outbox daripada mengurutkan berdasarkan timestamp.
2. Append, replikasi, dan acknowledgement
Leader melakukan append secara lokal dan mereplikasi ke follower yang cukup; hanya setelah kondisi commit yang dikonfigurasi terpenuhi, leader memberikan acknowledgement penerimaan. ID event dan nomor urut produser mendukung deduplikasi saat percobaan ulang (retry). Segmen disk bergulir berdasarkan ukuran atau waktu, dan indeks membantu konsumen menemukan offset.
Buat acknowledgement secara eksplisit: ini berarti event berada dalam log terkomit yang dapat dipulihkan, bukan berarti setiap konsumen telah memprosesnya atau bahwa read model yang dimaterialisasi sudah mutakhir. Kegagalan konsumen tidak membatalkan (rollback) event yang telah di-append.
3. Checkpoint konsumen dan idempotensi
Setiap grup konsumen menyimpan offset partisinya sendiri. Proses event dan perbarui read model sebelum melakukan commit checkpoint; crash dapat mengulang pemrosesan, sehingga handler harus idempoten berdasarkan ID event atau versi agregat. Jika model dan checkpoint memerlukan penggabungan atomik, tulis keduanya dalam satu transactional store atau catat hasilnya di outbox.
Pertahankan log saat konsumen tertinggal; jangan lewati event yang belum diproses hanya untuk mengurangi lag. Tampilan yang dapat dibangun kembali dapat dimulai dari offset snapshot. Efek samping eksternal yang tidak dapat dibangun kembali memerlukan kompensasi atau peninjauan manual daripada pemutaran ulang secara buta.
4. Protokol snapshot
Snapshot menyimpan aggregate ID, state yang diserialisasi, offset terakhir yang diterapkan, versi agregat dan skema, checksum, serta waktu pembuatan. Hasilkan pada batas yang stabil: catat target offset, terapkan event hingga offset tersebut, dan tulis snapshot dengan batas yang sama. Pemulihan hanya menerima snapshot tervalidasi yang offset-nya termasuk dalam partisi agregat tersebut.
Pemulihan memuat snapshot dan memutar ulang dari snapshotOffset + 1. Jika skema sudah usang, jalankan migrasi berversi sebelum memublikasikan state; kegagalan migrasi harus memblokir state yang tidak valid. Snapshot berfungsi sebagai cache: menghapusnya tidak merusak log apa pun dan hanya menambah waktu pemulihan.
5. Pisahkan retensi, pemadatan, dan arsip
Retensi berbasis waktu menghapus data log lama dan cocok untuk event dengan jendela pemutaran ulang yang ditentukan. Pemadatan kunci mempertahankan nilai terbaru untuk setiap kunci dalam partisi dan memungkinkan konsumen state merekonstruksi current state dari log yang lebih pendek. Ini tidak dapat mendukung audit atau pemutaran ulang pada titik waktu arbitrer karena event-event perantara mungkin sudah hilang.
Tombstone merepresentasikan kunci yang dihapus. Pertahankan tombstone sampai setiap konsumen yang tercakup dalam kontrak dapat mengamatinya, lalu izinkan pemadatan untuk menghapusnya. Simpan event audit dalam arsip yang tidak dapat diubah (immutable) dengan kontrol akses, enkripsi, dan aturan retensi; log state yang dipadatkan bukanlah bukti audit yang lengkap.
6. Skema, ketidakteraturan urutan, dan poison event
Gunakan aturan skema yang kompatibel ke belakang (backward-compatible): tambahkan field opsional, pertahankan arti lama, dan biarkan konsumen mengabaikan field yang tidak dikenal. Perubahan yang merusak (breaking changes) memerlukan versi skema baru, periode dual-read, atau jendela migrasi. Catat versi skema dan kegagalan parsing; jangan pernah secara diam-diam mengomit offset dari event yang tidak dapat di-parse.
Event yang tidak berurutan untuk satu agregat biasanya menunjukkan pelanggaran pada produser atau replikasi. Tolak versi agregat yang lama dan tahan celah (gap) untuk perbaikan. Event lintas-agregat memerlukan aturan kompensasi waktu bisnis atau causal-ID; waktu kedatangan di server bukanlah pengganti kausalitas.
7. Kapasitas, pemulihan, dan observabilitas
Modelkan event per detik, ukuran payload rata-rata dan tail, faktor replikasi, jendela retensi, ukuran snapshot, penghematan pemadatan, dan kecepatan pemutaran ulang. Interval snapshot yang lebih pendek meningkatkan kecepatan pemulihan tetapi menambah write amplification dan penyimpanan. Pemadatan yang lebih agresif menurunkan biaya penyusunan ulang state tetapi melemahkan audit dan kueri historis.
Pantau latensi acknowledgement produser, replication lag, segmen disk, backlog pemadatan, consumer lag, usia snapshot, kecepatan pemutaran ulang, kesalahan skema, tingkat duplikasi, dan perbedaan checksum snapshot. Secara berkala, susun ulang sampel state dari snapshot dan event lalu bandingkan dengan materialized view; pertahankan offset dan sampel event saat ketidakcocokan muncul.
Contoh jawaban berkualitas tinggi
“Saya menggunakan aggregate ID sebagai partition key, melakukan append pada event yang immutable, dan menolak penulisan usang yang konkuren menggunakan versi agregat. Acknowledgement produser berarti event telah berada dalam log terkomit yang tereplikasi; setiap konsumen mengomit offset partisinya sendiri setelah menerapkan event, sehingga crash hanya menyebabkan pengulangan yang dapat dideduplikasi.
Snapshot berisi aggregate state, offset event terakhir, versi agregat dan skema, serta checksum. Pemulihan memvalidasi snapshot dan memutar ulang dari offset berikutnya; menghapus snapshot hanya memperlambat pemulihan. Pemadatan kunci dibatasi untuk aliran current-state yang dapat dibangun kembali, dengan tombstone dipertahankan sesuai kontrak konsumen. Aliran audit tetap menjadi arsip yang immutable.
Saya hanya menjanjikan pengurutan di dalam satu agregat, bukan lintas-partisi. Model kapasitas mencakup replikasi, retensi, pemadatan, dan write amplification snapshot. Metrik mencakup lag, usia snapshot, backlog pemadatan, kesalahan skema, dan perbedaan verifikasi pemutaran ulang, disertai pemeriksaan rekonstruksi berkala dari snapshot-plus-event.”
Kesalahan umum
- Memperlakukan snapshot sebagai sumber event → menghapus snapshot menghilangkan bukti pemulihan dan audit → snapshot adalah akselerator turunan yang terikat offset.
- Memperlakukan log yang dipadatkan sebagai riwayat lengkap → event perantara dan state point-in-time hilang → arsipkan event audit dan padatkan hanya aliran state.
- Mengurutkan semua event berdasarkan timestamp → clock skew menciptakan urutan palsu → tentukan urutan berdasarkan versi agregat dan kontrak partisi.
- Mengomit offset setelah efek samping arbitrer → duplikasi tidak dapat dihindari tetapi tidak terspesifikasi → buat handler menjadi idempoten dan tentukan penggabungan model/checkpoint.
- Menghapus tombstone secara langsung → konsumen yang tertinggal dapat membangkitkan kembali sebuah kunci → pertahankan melalui jendela kontrak konsumen.
- Melewati skema yang tidak dikenal lalu mengomit → log dan view menyimpang secara diam-diam → jeda, isolasi poison event, dan simpan bukti.
- Melewatkan pemodelan kapasitas → snapshot, pemadatan, atau pemutaran ulang menjadi bottleneck pemadaman (outage) → hitung lag, waktu pemulihan, dan write amplification.
- Mengklaim exactly-once menyelesaikan setiap duplikasi → efek samping eksternal masih dapat berulang → gunakan kunci idempotensi, transaksi, atau kompensasi.
Pertanyaan lanjutan dan jawaban
Mengapa tidak menyimpan current state saja di database?
Jika hanya pembacaan saat ini yang penting, database lebih sederhana. Event log menyediakan pemutaran ulang, audit, banyak konsumen, dan rekonstruksi berbagai tampilan yang berbeda, dengan konsekuensi biaya skema, pemutaran ulang, kapasitas, dan idempotensi efek samping. Pilih pendekatan ini untuk persyaratan tersebut daripada berasumsi bahwa event sourcing selalu lebih unggul secara universal.
Bagaimana konsumen baru dapat membangun kembali dari awal setelah pemadatan?
Konsumen tersebut hanya dapat merekonstruksi current state yang masih direpresentasikan setelah pemadatan; riwayat perantara yang dihapus tidak dapat direkonstruksi. Jika riwayat penting, simpan arsip yang tidak dipadatkan atau change log terpisah dan dokumentasikan kemampuan pemutaran ulang setiap topik.
Bagaimana jika ada event baru yang masuk saat snapshot sedang dibuat?
Pilih target offset yang stabil. Event setelah offset tersebut terus ditambahkan (append) tetapi bukan bagian dari snapshot; pemulihan memuat snapshot dan memutar ulang dari offset berikutnya. Jika penulisan snapshot gagal, pertahankan snapshot dan log lama daripada memublikasikan file parsial.
Bagaimana cara memigrasikan skema event?
Tentukan field kompatibilitas dan versi terlebih dahulu, lalu biarkan konsumen membaca versi lama dan baru selama jendela migrasi. Penulis menambahkan field baru dan berhenti menggunakan field lama hanya setelah semua konsumen ditingkatkan (upgrade). Perubahan yang merusak menggunakan tipe event baru atau migrasi offline, yang divalidasi oleh pengujian pemutaran ulang atas event-event historis.