Membangun antrean pesan dan microservice yang andal di seluruh database cloud terdistribusi seperti Spanner melibatkan tantangan unik. Dokumen ini menjelaskan konsep inti dan pola desain yang digunakan untuk menjalankan pemrosesan exactly-once dan pengakuan at-most-once dalam antrean Spanner.
Konsep inti dan dilema keidempotenan
"Idempotensi" berarti bahwa berapa pun kali suatu operasi dijalankan, hasilnya akan selalu sama. Saat membangun sistem pengiriman tugas atau antrean pesan, ada tiga metode pengiriman utama yang menangani idempotensi:
- At-least-once: Pesan dijamin akan dikirim dan diproses. Jika terjadi kegagalan jaringan, pesan mungkin dikirim dan diproses beberapa kali.
- At-most-once: Pesan dikirim paling banyak satu kali. Pemrosesan duplikat dicegah, tetapi jika terjadi kegagalan, pesan mungkin hilang atau dibatalkan.
- Tepat satu kali: Setiap pesan diproses tepat satu kali—tidak hilang maupun digandakan.
Antrean Spanner secara otomatis menyediakan pengiriman setidaknya sekali dan pengakuan paling banyak sekali. Namun, Anda dapat menggunakan pola desain dan contoh yang dijelaskan di bagian berikut untuk mengurangi pemrosesan dan pengiriman ulang duplikat secara terprogram.
Masalah status commit tidak diketahui
Dalam database satu mesin, transaksi berhasil atau gagal. Dalam database cloud terdistribusi, transaksi dapat hilang saat data direplikasi di beberapa pusat data fisik.
Saat aplikasi Anda memproses pesan dan melakukan konfirmasi seperti
DELETE FROM MessageQueue WHERE message_id = '123' ASSERT_ROWS_MODIFIED 1, permintaan
melakukan perjalanan di beberapa hop jaringan:
[Your App] <--(gRPC)--> [Spanner API] <--(Google network)--> [Spanner Storage]
Saat menerima commit, pemimpin penyimpanan Spanner akan melakukan sinkronisasi di seluruh replika penyimpanan. Setelah konsensus tercapai dan data ditulis ke disk, transaksi berhasil di-commit di database.
Namun, kegagalan jaringan sementara (seperti reboot proxy atau terputusnya jaringan) dapat terjadi setelah data di-commit ke disk, tetapi sebelum konfirmasi keberhasilan mencapai aplikasi Anda. Jika hal ini terjadi, aplikasi Anda akan menerima error waktu tunggu jaringan (DEADLINE_EXCEEDED atau UNAVAILABLE).
Karena koneksi terputus di luar band, aplikasi Anda tidak dapat mengetahui apakah koneksi terputus sebelum atau setelah commit berhasil. Masalah ini dikenal sebagai masalah "status commit tidak diketahui".
Alasan percobaan ulang otomatis bisa berbahaya
Jika aplikasi Anda mencegat error waktu tunggu jaringan dan mencoba kembali transaksi yang sama persis tanpa penghapusan duplikat, aplikasi berisiko menjalankan logika bisnis dua kali. Misalnya, jika transaksi melibatkan penagihan kartu kredit pelanggan atau pengiriman item, mencoba lagi transaksi yang berhasil (tetapi tidak dikonfirmasi) akan menghasilkan penagihan atau pengiriman duplikat.
Untuk melindungi pengguna Anda, library klien Google Cloud resmi tidak pernah otomatis mencoba kembali transaksi yang gagal karena error commit yang tidak diketahui. Mereka menampilkan error eksplisit yang memberi tahu Anda bahwa hasil transaksi tidak diketahui, sehingga kode aplikasi Anda harus menangani percobaan ulang dengan aman menggunakan pola idempoten.
Strategi dunia nyata untuk pemrosesan tepat satu kali
Untuk mencoba ulang transaksi dengan aman dan mengurangi pemrosesan duplikat serta pengiriman ulang pesan, terapkan salah satu pola desain utama berikut.
Memastikan baris diubah
Jika semua logika bisnis Anda (seperti memperbarui tabel lain) dan konfirmasi pesan antrean terjadi dalam satu transaksi Spanner, Anda dapat menggunakan ASSERT_ROWS_MODIFIED pada pernyataan DELETE. Ini adalah strategi
paling sederhana untuk pemrosesan tepat satu kali karena tidak memerlukan
tabel penghapusan duplikat terpisah.
Dengan menambahkan ASSERT_ROWS_MODIFIED 1 ke pernyataan Anda, konfirmasi hanya berhasil jika tepat satu pesan dihapus. Jika terjadi kegagalan jaringan dan aplikasi Anda mencoba lagi transaksi, pesan tidak lagi ada dalam antrean. Percobaan ulang menghapus 0 baris dan pernyataan gagal dengan OUT_OF_RANGE.
Error OUT_OF_RANGE bersifat permanen: baris sudah tidak ada, sehingga menjalankan kembali pernyataan akan selalu mengubah 0 baris dan gagal lagi. Pernyataan ini juga
tingkat pernyataan: hanya DELETE yang gagal. Transaksi tetap terbuka,
dan semua penulisan yang telah Anda lakukan di dalamnya masih di-buffer dan akan
di-commit jika Anda melanjutkan. Spanner tidak membatalkan atau mengembalikan transaksi untuk Anda. Untuk mendapatkan perlindungan tepat satu kali yang diinginkan, Anda harus
memastikan transaksi di-rollback:
- Biarkan error diteruskan: Jika Anda menggunakan API berbasis runner (seperti
ReadWriteTransactiondi Go), biarkan errorOUT_OF_RANGEditeruskan keluar dari fungsi transaksi Anda. Library klien akan membatalkan transaksi dan mengembalikannya, sehingga memastikan tidak ada penulisan logika bisnis yang di-commit. - Jangan tangkap dan lanjutkan: Jangan pernah menangkap error pernyataan di dalam fungsi transaksi dan melanjutkan. Jika Anda melakukannya, Spanner akan tetap melakukan commit pada pernyataan Anda yang lain, sehingga menyebabkan update duplikat persis yang seharusnya dicegah oleh pola ini.
- Rollback manual: Jika Anda mengelola transaksi secara manual (misalnya, dengan
ReadWriteStmtBasedTransactiondi Go, atau REST/gRPC API), Anda harus memanggil metode rollback yang sesuai secara eksplisit saat pernyataan gagal.
Kotak keluar transaksional
Jika logika bisnis Anda tidak dapat terjadi dalam satu transaksi Spanner (seperti alur kerja multi-langkah atau update yang mencakup beberapa sistem), atau jika pemrosesan pesan memerlukan panggilan layanan eksternal (seperti mengirim SMS atau memproses pembayaran), Anda tidak dapat menggabungkan logika bisnis dengan pengakuan antrean secara atomik.
Dalam skenario ini, jangan pernah memanggil API eksternal atau menjalankan operasi non-transaksional secara langsung di dalam blok transaksi Spanner. Jika Spanner mencoba lagi atau membatalkan transaksi, aplikasi Anda mungkin mengeksekusi logika bisnis tersebut beberapa kali.
Aplikasi Anda harus menerapkan strategi keidempotensiannya sendiri, tetapi Anda dapat menggunakan pola berikut untuk mengurangi pekerjaan duplikat:
Operasi cepat (selesai dalam masa sewa default 10 detik): Jalankan panggilan API eksternal atau logika bisnis saat pesan diterima. Konfirmasi pesan hanya setelah operasi berhasil. Jika operasi gagal, atau jika pekerja mengalami error sebelum mengonfirmasi, jangan mengonfirmasi pesan; masa berlaku sewa akan berakhir dan Spanner akan otomatis mengirimkan pesan untuk dicoba lagi.
Operasi yang berjalan lama (membutuhkan waktu lebih dari 10 detik): Konfirmasi pesan saat diterima, dan dalam transaksi yang sama, kirim ulang pesan baru yang dijadwalkan untuk pengiriman di masa mendatang (menggunakan penundaan pengiriman yang lebih besar daripada perkiraan waktu eksekusi). Atau, perpanjang masa berlaku pesan secara berkala menggunakan TVF
RENEWLEASE_QUEUE_NAME(). Jika logika bisnis atau panggilan eksternal berhasil, akui pesan yang baru dimasukkan dalam antrean atau diperpanjang.
Idempotensi sesi yang di-multiplex
Sesi Spanner dapat di- multiplex. Sesi yang di-multiplex melacak status transaksi dalam memori di seluruh sesi bersama. Tindakan ini memberikan perlindungan terhadap kegagalan jaringan sisi klien dengan memungkinkan library klien terhubung kembali dan menyelesaikan status commit yang tidak diketahui dengan lebih andal.
Namun, sesi yang di-multiplex tidak dapat menghilangkan hasil commit yang tidak diketahui jika server frontend mengalami error. Jika server tertentu yang menghosting tabel transaksi sesi Anda dimulai ulang segera setelah commit, status dalam memori akan hilang, sehingga menampilkan error hasil transaksi yang tidak diketahui saat terhubung kembali. Untuk mengatasi risiko ini, terapkan pola keidempotenan di halaman ini.
Langkah berikutnya
- Pelajari cara menggunakan antrean Spanner, termasuk praktik terbaik dan pemantauan.
- Jelajahi lainnya skenario dan contoh antrean Spanner.
- Konfigurasi kontrol akses dengan kontrol akses terperinci untuk antrean.