Ketika Dua Versi Aplikasi Berbagi Satu Database: Transisi Dual-Write dan Dual-Read
Bayangkan skenario ini: tim Anda baru saja menambahkan kolom baru ke tabel database produksi. Perubahan skema berjalan lancar. Sekarang Anda perlu men-deploy versi aplikasi baru yang menulis ke kolom tersebut. Namun versi aplikasi lama masih berjalan, menangani permintaan, dan tidak tahu apa-apa tentang kolom baru ini.
Jika aplikasi baru mulai menulis hanya ke kolom baru, aplikasi lama tidak akan melihat data tersebut. Pengguna yang permintaannya jatuh ke aplikasi lama akan mendapatkan hasil yang tidak lengkap atau tidak konsisten. Anda tidak bisa menghentikan aplikasi lama secara instan. Anda tidak bisa beralih semuanya ke versi baru dalam satu kali langkah. Anda membutuhkan periode transisi di mana kedua versi hidup berdampingan dan berbagi database yang sama.
Di sinilah pola dual-write dan dual-read menjadi diperlukan. Pola ini tidak elegan. Tidak sederhana. Tapi ini adalah cara praktis untuk memigrasi struktur data di sistem yang sedang berjalan tanpa downtime.
Masalah Inti: Dua Versi, Satu Database
Ketika Anda memperluas skema dengan menambahkan kolom atau tabel baru, database dapat menampung struktur lama dan baru. Namun aplikasi yang membaca dan menulis data tersebut tidak begitu fleksibel. Versi aplikasi lama hanya memahami struktur lama. Versi aplikasi baru memahami keduanya, tetapi perlu menjaga agar versi lama tetap berfungsi hingga semua instance telah ditingkatkan.
Pendekatan naif adalah membuat aplikasi baru hanya menulis ke kolom baru. Itu akan langsung merusak aplikasi lama. Aplikasi lama membaca dari kolom lama, tidak menemukan apa pun, dan gagal. Pendekatan yang benar adalah membuat aplikasi baru menulis ke kedua tempat hingga aplikasi lama tidak ada lagi.
Diagram urutan berikut mengilustrasikan alur penulisan dan pembacaan selama periode transisi:
Dual-Write: Menulis ke Dua Tempat Sekaligus
Dual-write berarti versi aplikasi baru menulis data ke struktur lama dan struktur baru pada setiap operasi penulisan. Ketika pengguna membuat sebuah record, aplikasi baru mengisi kolom lama seperti biasa, lalu juga menulis data yang sama ke kolom baru.
Ini terdengar mudah, tetapi ada dua detail yang sangat penting.
Berikut contoh JavaScript yang mengimplementasikan dual-write dan dual-read untuk pembaruan profil pengguna:
async function updateUserProfile(userId, name, email) {
// Dual-write: tulis ke kolom lama dulu, lalu kolom baru
await db.query(
'UPDATE users SET name = ?, email = ? WHERE id = ?',
[name, email, userId]
);
await db.query(
'UPDATE users SET profile_data = ? WHERE id = ?',
[JSON.stringify({ name, email }), userId]
);
}
async function getUserProfile(userId) {
// Dual-read: utamakan kolom baru, fallback ke kolom lama
const row = await db.query(
'SELECT profile_data, name, email FROM users WHERE id = ?',
[userId]
);
if (row.profile_data) {
return JSON.parse(row.profile_data);
}
return { name: row.name, email: row.email };
}
Pertama, urutan penulisan harus konsisten. Tulis ke kolom lama terlebih dahulu, lalu kolom baru. Jika proses gagal setelah menulis ke kolom lama tetapi sebelum menulis ke kolom baru, aplikasi lama masih bisa membaca data tersebut. Kolom baru akan kehilangan record itu, tetapi Anda bisa memperbaikinya nanti dengan backfill. Jika Anda menulis ke kolom baru terlebih dahulu dan proses gagal, aplikasi lama akan langsung melihat data yang tidak lengkap. Itu adalah insiden produksi yang siap terjadi.
Kedua, nilainya harus identik. Data yang ditulis ke kolom lama dan kolom baru harus mewakili informasi yang sama. Jika ada perbedaan transformasi atau logika antara kedua penulisan, Anda akan memiliki inkonsistensi data yang sangat sulit di-debug nantinya. Jaga logika penulisan tetap identik. Kolom baru mungkin menyimpan data dalam format atau struktur yang berbeda, tetapi artinya harus sama.
Dual-Read: Membaca dari Dua Tempat, Mengutamakan yang Lama
Setelah dual-write berjalan, aplikasi baru dapat menulis data yang bisa dibaca oleh kedua versi. Tapi bagaimana dengan pembacaan? Aplikasi baru bisa mulai membaca dari kolom baru segera, tetapi itu menimbulkan masalah. Aplikasi lama masih menulis data hanya ke kolom lama. Jika aplikasi baru hanya membaca dari kolom baru, ia akan kehilangan data yang ditulis oleh aplikasi lama.
Solusinya adalah dual-read. Aplikasi baru membaca dari kedua tempat tetapi memprioritaskan kolom lama selama transisi. Ini memastikan data yang ditulis oleh aplikasi lama selalu terlihat oleh aplikasi baru. Seiring waktu, setelah Anda memverifikasi bahwa data mengalir dengan benar ke kolom baru, Anda dapat secara bertahap mengalihkan pembacaan ke kolom baru.
Pergeseran bertahap inilah yang membuat feature flags menjadi berguna. Anda dapat mengonfigurasi aplikasi baru untuk membaca dari kolom baru untuk sebagian kecil permintaan. Jika tidak ada yang rusak, tingkatkan persentasenya. Jika muncul kesalahan, balikkan flag dan semua pembacaan kembali ke kolom lama. Tidak perlu redeployment.
Bagaimana dengan Data yang Ditulis oleh Aplikasi Lama?
Selama transisi ini, aplikasi lama masih berjalan dan hanya menulis ke kolom lama. Aplikasi baru harus menangani ini. Ketika aplikasi baru membaca record yang ditulis oleh aplikasi lama, ia menemukan data hanya di kolom lama. Aplikasi baru harus bisa membaca data itu dari kolom lama, menggunakannya, dan secara opsional menyalinnya ke kolom baru sebagai bagian dari proses latar belakang.
Ini tidak sama dengan dual-write. Ini adalah proses backfill yang berjalan terpisah, mengisi kolom baru dengan data yang ditulis sebelum aplikasi baru mulai menulis ke kedua tempat. Backfill adalah operasi batch yang berjalan setelah pola dual-write dan dual-read stabil.
Kompleksitas Sebenarnya: Mengoordinasikan Transisi
Bagian tersulit dari fase ini bukanlah kodenya. Ini adalah koordinasinya. Anda perlu tahu instance mana yang menjalankan versi mana. Anda perlu tahu kapan instance lama terakhir telah dinonaktifkan. Anda perlu memonitor inkonsistensi data antara kolom lama dan baru.
Selama dual-write, setiap operasi penulisan menjadi dua kali penulisan. Itu berarti lebih banyak beban database, lebih banyak waktu transaksi, dan lebih banyak potensi titik kegagalan. Pantau metrik database Anda selama fase ini. Jika latensi penulisan meningkat, Anda mungkin perlu melakukan batch penulisan atau menggunakan replikasi asinkron.
Selama dual-read, setiap operasi pembacaan mungkin perlu memeriksa dua lokasi. Itu menambah kompleksitas pada logika query Anda dan dapat memperlambat jalur pembacaan. Gunakan caching dengan hati-hati. Jangan cache data dari kolom lama jika kolom baru seharusnya menjadi sumber kebenaran.
Daftar Periksa Praktis untuk Transisi
- Pastikan perluasan skema (kolom atau tabel baru) sudah di-deploy dan diverifikasi.
- Deploy versi aplikasi baru dengan logika dual-write: tulis ke kolom lama dulu, lalu kolom baru.
- Verifikasi bahwa data yang ditulis oleh aplikasi baru terlihat oleh aplikasi lama.
- Aktifkan dual-read di aplikasi baru: baca dari kolom lama secara default, tetapi siap untuk beralih.
- Gunakan feature flag untuk secara bertahap mengalihkan pembacaan dari kolom lama ke kolom baru.
- Pantau inkonsistensi data antara kolom lama dan baru.
- Mulai proses backfill untuk menyalin data lama ke kolom baru.
- Setelah backfill selesai dan semua pembacaan menggunakan kolom baru, hapus logika dual-write.
- Nonaktifkan kolom atau tabel lama setelah memastikan tidak ada aplikasi yang membacanya.
Kesimpulan
Dual-write dan dual-read bukanlah pola permanen. Mereka adalah jembatan sementara yang memungkinkan Anda memigrasi struktur data sambil menjaga sistem tetap berjalan untuk semua pengguna. Tujuannya adalah mencapai keadaan di mana hanya struktur baru yang penting, dan struktur lama dapat dihapus. Sampai saat itu, setiap penulisan menuju ke dua tempat, setiap pembacaan memeriksa dua sumber, dan tim Anda tetap waspada terhadap inkonsistensi. Fase ini memang tidak nyaman, tetapi ini adalah satu-satunya cara untuk mengubah skema database yang sedang berjalan tanpa menghentikan dunia.