Topik wawancara representatif

Wawancara data engineering: apa yang sebenarnya dijamin oleh semantik exactly-once pada Kafka?

DataSulit
Tim Redaksi Offer.ccDipublikasikan Diperbarui

Pertanyaan

Sebuah consumer membaca event pesanan dari Kafka, mentransformasikannya, dan menulis ke topic lain. Pewawancara menanyakan bagaimana Anda menghindari duplikasi dan kehilangan data, lalu menanyakan mengapa desain exactly-once yang sama tidak secara otomatis mencakup database eksternal. Bagaimana Anda akan menjawab?

Konteks dan pertanyaan

Pertanyaan data-engineering dan stream-processing ini ditujukan untuk data engineer, streaming-platform engineer, dan peran infrastruktur data backend. Consumer dapat mengalami crash atau rebalance, dan producer dapat melakukan retry setelah kehilangan respons; hasil pertama-tama dikirim kembali ke Kafka. Jawaban harus memisahkan pemrosesan atomik Kafka dari efek samping eksternal sembarang, alih-alih memperlakukan exactly-once sebagai jaminan ajaib untuk seluruh sistem.

Hal yang dinilai oleh pewawancara

  • Apakah Anda dapat membagi exactly-once menjadi visibilitas output dan pembaruan atomik pada offset input.
  • Apakah Anda dapat menggunakan transactional producer, transactional.id, read_committed, dan commit offset manual secara benar.
  • Apakah Anda dapat menjelaskan abort, restart, fencing, dan pemulihan saat rebalance.
  • Apakah Anda menyadari bahwa database, search engine, atau layanan HTTP memerlukan transaksinya sendiri, kunci idempotensi, atau rekonsiliasi.

Pertanyaan klarifikasi sebelum menjawab

Pertama, konfirmasikan apakah output tetap berada di Kafka, apakah Kafka Streams digunakan, apakah consumer group melakukan rolling update selama deployment, dan apakah sistem eksternal harus melakukan commit dalam transaksi yang sama dengan Kafka. Kemudian bedakan antara "satu output yang terlihat per input" dan "satu efek samping eksternal"; hal yang terakhir memerlukan kerja sama dari sistem tujuan. Tanyakan tentang latensi, ukuran batch transaksi, jendela retry, dan lag yang dapat diterima.

Kerangka jawaban 30 detik

Saya akan menempatkan record input, output yang telah ditransformasi, dan offset consumer dalam satu transaksi Kafka. Producer mengaktifkan transactional.id; consumer menonaktifkan auto-commit dan menggunakan read_committed. Commit yang berhasil membuat ketiganya terlihat secara bersamaan, sedangkan abort menyembunyikan output dan membiarkan offset berada pada posisi sebelum transaksi. ID transaksi yang unik dan stabil akan melakukan fencing pada instans lama setelah restart. Ini hanya mencakup operasi baca dan tulis Kafka; database eksternal memerlukan hasil dan offset dalam satu transaksi penyimpanan, atau idempotensi, outbox, dan rekonsiliasi.

Solusi langkah demi langkah

1. Nyatakan batasan jaminan

Desain Kafka mendefinisikan exactly-once topic-ke-topic sebagai pembaruan atomik record output dan posisi consumer dalam satu transaksi. Ini tidak berarti fungsinya hanya berjalan sekali atau permintaan HTTP sembarang hanya tiba sekali. Nyatakan batasan ini sebelum membahas konfigurasi.

2. Tulis output dan offset secara atomik dengan transactional producer

Nonaktifkan auto-commit, proses sebuah batch, kirim output, dan kirimkan offset batch tersebut sebagai bagian dari transaksi. Kode semunya adalah:

java
producer.initTransactions();
while (running) {
  ConsumerRecords<String, Order> records = consumer.poll(timeout);
  producer.beginTransaction();
  try {
    for (ConsumerRecord<String, Order> r : records) {
      producer.send(new ProducerRecord<>("orders-enriched", r.key(), enrich(r.value())));
    }
    producer.sendOffsetsToTransaction(offsets(records), groupMetadata);
    producer.commitTransaction();
  } catch (AbortableException e) {
    producer.abortTransaction();
    consumer.seekToCommitted();
  }
}

Melakukan commit output dan offset secara bersamaan mencegah duplikasi yang terlihat akibat kegagalan output-sebelum-offset dan mencegah kehilangan data akibat kegagalan offset-sebelum-output. Tangani kelas exception klien sesuai dengan versi yang di-deploy; me-retry setiap exception secara membabi buta adalah tindakan yang tidak aman.

3. Buat consumer hanya melihat transaksi yang telah di-commit

Atur isolation.level=read_committed dan pertahankan enable.auto.commit=false. read_uncommitted mengekspos record dari transaksi yang dibatalkan (aborted), memungkinkan consumer meneruskan output yang seharusnya di-rollback. read_committed menggunakan penanda transaksi untuk menyelaraskan visibilitas dengan hasil commit.

4. Tangani restart, fencing, dan rebalance

Berikan setiap instans consumer aktif sebuah transactional.id yang unik di seluruh cluster dan stabil. Ketika instans baru mendaftar dengan ID yang sama, Kafka membatalkan transaksi yang sedang berjalan dari instans lama dan melakukan fencing padanya, mencegah keduanya melakukan commit. Setelah abort, aplikasi harus membuat ulang atau secara eksplisit memundurkan (rewind) posisi consumer dan memproses ulang batch tersebut; aplikasi tidak boleh melanjutkan dari kursor lokal yang telah maju di dalam transaksi yang dibatalkan. Penetapan partisi memastikan satu anggota grup memiliki satu partisi pada satu waktu.

5. Jelaskan mengapa sistem eksternal harus bekerja sama

Jika hasilnya dikirim ke PostgreSQL, transaksi Kafka tidak secara otomatis mencakup commit database. Crash yang terjadi setelah penulisan database meninggalkan offset Kafka dalam keadaan belum di-commit, sehingga retry memerlukan kunci unik atau pembaruan versi kondisional; commit offset terlebih dahulu dapat menyebabkan hilangnya penulisan. Desain yang lebih kuat menempatkan hasil dan offset dalam satu transaksi database, atau menulis record outbox/connector yang dapat di-replay dan membiarkan tujuan melakukan deduplikasi serta rekonsiliasi. Tanpa kerja sama sistem tujuan, janjikan eksekusi at-least-once dengan duplikasi yang dapat dideteksi, bukan exactly-once end-to-end.

6. Verifikasi dengan fault injection, bukan snapshot konfigurasi

Simulasikan crash sebelum dan sesudah sendOffsetsToTransaction, hilangkan respons commit, picu rebalance, lakukan fencing pada instans lama, buat transaksi kedaluwarsa, dan buat sink tidak tersedia. Konsumsi output dengan read_committed dan periksa jumlah output yang terlihat per ID input, offset akhir, lag setelah restart, dan record transaksi yang dibatalkan. Untuk sink eksternal, uji konflik kunci unik, pemutaran ulang (replay), dan rekonsiliasi secara terpisah. Pantau tingkat abort, lag consumer, latensi pemrosesan, dan jumlah fencing.

Contoh jawaban berkualitas tinggi

Pertama-tama saya akan membatasi klaim tersebut. Jika input dan output berada di Kafka, saya akan menggunakan Kafka Streams atau perulangan transactional consume-transform-produce yang setara. Consumer menonaktifkan auto-commit; producer memiliki transactional.id yang stabil dan unik; dan output setiap batch beserta offset-nya di-commit dengan sendOffsetsToTransaction. Consumer downstream menggunakan read_committed, sehingga output yang dibatalkan tidak akan terlihat. Saat restart, ID transaksi yang sama akan melakukan fencing pada instans lama, dan pembatalan (abort) mengharuskan pemunduran ke offset terakhir yang di-commit. Jika tujuannya adalah PostgreSQL, saya tidak akan memperluas jaminan Kafka menjadi klaim end-to-end: saya akan menempatkan hasil dan offset dalam satu transaksi database, atau menggunakan outbox, kunci idempotensi, dan rekonsiliasi. Saya kemudian akan menginjeksi crash, hilangnya respons, rebalance, dan fencing serta memverifikasi output yang terlihat, offset, dan status sistem tujuan per ID input.

Kesalahan umum

  • Menyebut idempotent producer sebagai exactly-once → ini terutama mencegah duplikasi entri log pada retry producer dan tidak membuat output serta offset menjadi atomik → tambahkan transaksi dan commit offset.
  • Hanya mengatur read_committed ini mengubah visibilitas tetapi tidak melakukan commit transaksi atau memulihkan dari crash → terapkan siklus hidup transaksi lengkap dengan auto-commit dinonaktifkan.
  • Berbagi satu transactional.id di antara instans aktif → instans akan saling mem-fence satu sama lain dan mengacaukan throughput → tetapkan ID unik yang stabil untuk setiap instans aktif.
  • Melanjutkan dari posisi lokal setelah pembatalan (abort) → posisi tersebut mungkin berada di dalam batch yang belum di-commit → muat ulang offset yang telah di-commit atau lakukan seek secara eksplisit.
  • Mengklaim Kafka membuat efek samping database terjadi satu kali → kedua sistem tidak memiliki commit atomik otomatis → gunakan transaksi di sistem tujuan, penulisan idempoten, outbox, atau rekonsiliasi.

Pertanyaan lanjutan dan tanggapan

Apakah exactly-once berarti fungsi bisnis hanya dieksekusi satu kali?

Tidak. Fungsi tersebut dapat dieksekusi dan kemudian dieksekusi lagi setelah kegagalan commit; jaminannya adalah output yang terlihat di Kafka dan commit offset selalu selaras. Sebisa mungkin jaga agar fungsi bebas dari efek samping eksternal, atau buat efek samping tersebut dapat diulang dan direkonsiliasi.

Mengapa read_committed masih dapat menambah latensi?

Consumer harus melewati record yang dibatalkan dan menunggu transaksi selesai. Transaksi yang terbuka atau berdurasi panjang meningkatkan latensi visibilitas dan lag. Batasi ukuran batch dan batas waktu (timeout), pantau durasi transaksi, dan pertimbangkan biayanya dibandingkan pemrosesan at-least-once untuk jalur dengan kebutuhan latensi rendah.

Apa yang terjadi jika terjadi rebalance di tengah transaksi?

Pemilik baru akan melanjutkan dari offset terakhir yang di-commit. Transaksi saat ini harus dibatalkan; instans lama dilepaskan atau di-fence; dan instans baru memproses ulang batch yang belum di-commit. Jangan melakukan commit offset kecuali anggota saat ini masih memiliki partisi tersebut.

Bagaimana cara Anda mengirim output Kafka ke PostgreSQL?

Tulis hasil bisnis dan posisi consumer dalam satu transaksi PostgreSQL, atau tulis baris outbox dengan ID event unik dan biarkan relay yang andal mengirimkannya. Jika keduanya tidak dapat berbagi transaksi, pilih pengiriman at-least-once, batasan unik (unique constraints), pembaruan versi kondisional, dan rekonsiliasi; jangan memasarkannya sebagai exactly-once.

Metrik mana yang membuktikan bahwa desain tersebut berfungsi?

Hitung output read_committed unik yang terlihat per ID event input, lalu korelasikan dengan offset yang di-commit, jumlah abort dan fence, lag consumer, volume replay setelah restart, dan konflik duplicate-key pada sink. Simpan sampel sebelum dan sesudah fault injection; snapshot konfigurasi tidak dapat membuktikan invarian tersebut.

Sumber publik

Pertanyaan terkait