Perubahan skema tanpa downtime dengan pola expand/contract
Rencanakan dan rilis perubahan skema tanpa downtime dengan pola expand/contract, backfill aman, rilis kompatibel, verifikasi, dan rollback.

Mengapa perubahan skema menyebabkan gangguan layanan
Perubahan skema menyebabkan gangguan saat versi aplikasi, worker latar belakang, dan database tidak lagi sepakat mengenai struktur serta nilai yang valid. Kegagalannya bisa jelas, misalnya setiap permintaan mengembalikan error, atau bertahap, seperti latensi kueri meningkat, penulisan gagal, replika tertinggal, dan antrean pekerjaan yang harus diputar ulang.
Deployment produksi jarang mengubah semua proses sekaligus. Rolling release membuat instance aplikasi lama dan baru berjalan bersamaan. Worker yang berjalan lama dapat mempertahankan build lama selama berjam-jam, klien seluler dapat tetap aktif berbulan-bulan, dan pekerjaan pelaporan atau integrasi mungkin memakai tabel tanpa melalui aplikasi utama. Semuanya berbagi satu database.
Kegagalan yang umum terjadi meliputi:
- Kode baru menulis ke kolom sebelum migrasi yang membuatnya selesai.
- Kode lama membaca tabel atau kolom yang diubah namanya atau dihapus oleh rilis berikutnya.
- Penulisan ulang tabel, backfill, atau pembuatan indeks memakai I/O dan CPU cukup besar hingga memperlambat trafik normal.
- Perintah skema menunggu lock saat permintaan mulai menumpuk di belakangnya.
- Constraint baru menolak penulisan dari proses yang belum ditingkatkan versinya.
Bagian yang berbahaya sering kali adalah memperoleh lock, bukan waktu eksekusi nominal. ALTER TABLE yang cepat dapat menunggu transaksi panjang. Saat menunggu, kueri berikutnya dapat mengantre di belakang lock skema yang tertunda, sehingga migrasi kecil berubah menjadi hambatan untuk seluruh aplikasi.
Tanpa downtime mengharuskan setiap keadaan database sementara tetap dapat dipakai oleh setiap versi aplikasi yang masih mungkin berjalan. Tambahkan struktur yang kompatibel terlebih dahulu, pindahkan trafik dan data secara terkendali, lalu hapus jalur lama hanya setelah konsumen terakhirnya hilang.
Pekerjaan ini layak dilakukan pada sistem dengan trafik langsung, rolling deployment, target ketersediaan ketat, atau prosedur pemulihan yang mahal. Alat internal kecil dengan database yang sepi mungkin lebih cocok memakai jendela pemeliharaan yang telah diuji. Keputusan harus mempertimbangkan biaya gangguan layanan dan kerumitan operasional migrasi.
Expand/contract dengan bahasa sederhana
Pola expand/contract mengubah satu perubahan yang tidak kompatibel menjadi rangkaian rilis yang kompatibel. Database sementara mendukung dua representasi saat kode dan data berpindah dari yang lama ke yang baru.
Urutannya terdiri dari tiga bagian:
- Expand dengan menambahkan kolom, tabel, indeks, atau constraint tanpa menghapus apa pun yang dibutuhkan kode saat ini.
- Transition dengan men-deploy kode yang kompatibel, memindahkan data historis, serta mengarahkan pembacaan dan penulisan ke representasi baru.
- Contract dengan menghapus kode lama dan objek database setelah verifikasi membuktikan bahwa semuanya tidak lagi digunakan.
Misalkan tabel PostgreSQL menyimpan nama seseorang di full_name, sedangkan aplikasi memerlukan kolom first_name dan last_name terpisah. Fase expand menambahkan kolom nullable sambil mempertahankan full_name. Rilis yang kompatibel menulis representasi yang dibutuhkan selama transisi. Backfill memisahkan nilai yang ada, dengan kebijakan jelas untuk nama yang tidak dapat dipisahkan secara andal. Pembacaan baru dipindahkan setelah kolom baru cukup lengkap. Fase contract kemudian menghapus full_name.
Urutan ini sesuai untuk rolling deployment karena build lama masih menemukan full_name dan build baru menemukan ketiga kolom. Ini juga mempertahankan jalur rollback aplikasi. Jika rilis baru bermasalah, build sebelumnya tetap dapat berjalan karena dependensi skemanya belum dihapus.
Rollback database berbeda dengan rollback aplikasi. Membalik migrasi setelah data ditransformasikan dapat membuang informasi atau mengembalikan nilai yang sudah tidak sesuai. Selama transisi, lebih baik kembalikan trafik aplikasi ke representasi yang sudah dikenal sambil membiarkan objek database tambahan tetap ada. Perbaiki migrasi berikutnya setelah insiden stabil.
Pola ini tidak berarti setiap perubahan memerlukan kode penulisan ganda. Menambahkan kolom opsional yang hanya dipakai kode baru mungkin cukup dengan satu migrasi tambahan dan satu deployment. Penggantian nama, perubahan representasi, pemecahan tabel, dan perubahan pada kolom wajib biasanya membutuhkan lebih banyak fase karena dua versi aplikasi tidak dapat berbagi skema dengan aman.
Klasifikasikan perubahan sebelum menentukan langkah
Rencana migrasi harus sesuai dengan risiko lock, penulisan ulang, kompatibilitas, dan konversi data yang sebenarnya. Menganggap setiap ALTER TABLE setara akan menghasilkan proses yang berlebihan atau rilis yang tidak aman.
Perubahan tambahan biasanya paling mudah. Kolom nullable, tabel terpisah, atau indeks yang dibuat dengan metode online sering dapat diperkenalkan sebelum kode aplikasi memakainya. Perintahnya tetap membutuhkan lock, jadi uji perilakunya pada tabel dan beban transaksi yang menyerupai produksi.
Perubahan destruktif mencakup penghapusan atau penggantian nama kolom, penyempitan tipe, penggantian tabel, dan penambahan constraint yang lebih ketat. Perubahan ini membatalkan asumsi yang dibuat kode lama. Letakkan pada fase contract setelah referensi kode dan konsumen eksternal dihapus.
Operasi yang mengubah data perlu dinilai secara terpisah. Mengonversi timestamp, menormalkan nomor telepon, menggabungkan catatan, atau memisahkan teks bebas dapat menghilangkan informasi. Tentukan cara menangani nilai tidak valid dan ambigu sebelum backfill dimulai. Jika transformasi tidak dapat dibalik, pertahankan sumbernya sampai hasilnya lolos pemeriksaan tingkat bisnis.
Tinjauan awal yang berguna mencakup lima pertanyaan:
- Lock apa yang diminta setiap pernyataan, dan berapa lama lock itu dapat menunggu atau ditahan?
- Apakah operasi akan menulis ulang tabel, menghasilkan WAL besar, atau meningkatkan lag replika?
- Aplikasi, pekerjaan, laporan, dan konsumen change-data-capture mana yang memakai objek terdampak?
- Dapatkah rilis saat ini dan rilis yang diusulkan berjalan pada setiap keadaan transisi?
- Sinyal apa yang menghentikan operasi, dan keadaan persis apa yang tersisa setelah dihentikan?
Jalankan migrasi yang persis sama pada data dengan volume dan distribusi realistis. Tabel uji dengan seribu baris rapi tidak banyak memberi gambaran tentang tabel produksi berisi ratusan juta baris, tuple lebar, baris mati, nilai yang timpang, dan transaksi berjalan lama.
Lakukan expand dengan aman di PostgreSQL
Expand PostgreSQL yang aman memakai perubahan metadata singkat, waktu tunggu lock terbatas, dan operasi online terpisah saat database mengharuskannya. Tambahkan struktur baru sebelum men-deploy kode yang bergantung padanya.
Menambahkan kolom nullable tanpa default biasanya merupakan operasi metadata singkat:
BEGIN;
SET LOCAL lock_timeout = '2s';
SET LOCAL statement_timeout = '30s';
ALTER TABLE customers
ADD COLUMN phone_e164 text;
COMMIT;
Timeout mencegah rilis menunggu tanpa batas di belakang transaksi terbuka. Jika lock tidak dapat diperoleh segera, biarkan migrasi gagal, periksa penghalangnya, lalu coba lagi pada waktu yang lebih aman. Jangan mencoba ulang otomatis dalam loop ketat karena permintaan lock berulang dapat terus mengganggu trafik produksi.
Versi PostgreSQL modern dapat menambahkan kolom dengan default konstan tanpa langsung menulis nilai itu ke setiap baris yang ada. Optimasi itu tidak membuat semua default aman. Ekspresi volatil dapat memerlukan penulisan ulang, dan ALTER TABLE tetap membutuhkan lock ACCESS EXCLUSIVE singkat. Pastikan perilakunya untuk versi PostgreSQL yang dipakai dan ekspresi yang tepat, jangan mengandalkan aturan umum.
CREATE INDEX biasa dapat memblokir penulisan. Gunakan pembuatan konkuren saat tabel harus tetap dapat ditulis:
CREATE INDEX CONCURRENTLY idx_customers_phone_e164
ON customers (phone_e164);
CREATE INDEX CONCURRENTLY tidak dapat dijalankan di dalam blok transaksi. Proses ini lebih lama, melakukan pekerjaan tambahan, dan dapat menunggu transaksi lama, tetapi insert, update, serta delete normal tetap dapat berjalan. Proses ini tetap memakai CPU, I/O, dan WAL, jadi pantau latensi database serta replika saat berjalan.
Pembuatan konkuren yang gagal dapat meninggalkan indeks tidak valid. Periksa keadaan indeks sebelum mencoba lagi, lalu hapus atau bangun ulang objek tidak valid itu dengan sengaja. Alat migrasi yang membungkus setiap berkas dalam transaksi memerlukan mode nontransaksional yang didukung untuk operasi indeks konkuren.
Tabel baru sering lebih mudah diperkenalkan daripada transformasi di tempat. Untuk relasi satu-ke-banyak atau banyak-ke-banyak, tambahkan tabel target dan indeksnya sambil mempertahankan kolom sumber. Tunda penghapusan sumber hingga penulisan baru, data historis, pembacaan, dan konsumen downstream telah berpindah.
Perubahan tipe memerlukan perhatian lebih. Sebagiannya hanya mengubah metadata, sementara lainnya menulis ulang setiap baris atau memperoleh lock ketat terlalu lama. Untuk konversi berisiko, tambahkan kolom dengan tipe target, isi dalam batch, alihkan akses aplikasi, lalu hapus kolom asli kemudian. Tim juga memiliki tempat untuk mencatat kegagalan konversi, alih-alih membuat satu ALTER COLUMN TYPE besar berhasil atau gagal sekaligus.
Deploy kode yang tetap kompatibel
Kode aplikasi yang kompatibel dapat menangani nilai transisi yang belum ada dan tidak pernah memerlukan migrasi destruktif dalam rollout yang sama. Expand database harus selesai sebelum instance aplikasi pertama mulai memakai objek baru.
Penulisan ganda berguna saat kedua representasi harus tetap terkini. Lakukan kedua penulisan dalam transaksi database yang sama bila memungkinkan. Penulisan kedua secara asinkron dapat gagal setelah penulisan pertama berhasil, menciptakan perbedaan yang dapat terlihat oleh pembacaan berikutnya.
Logika penulisan ganda juga memerlukan satu sumber otoritas. Jika phone_e164 berasal dari phone, tentukan masukan mana yang diprioritaskan saat keduanya diberikan dan terapkan normalisasi yang sama di handler API, worker, impor, serta alat administrasi. Jika tidak, dua jalur kode yang terlihat benar dapat menyimpan hasil berbeda.
Pembacaan harus berpindah setelah penulisan. Pertahankan pembacaan pada kolom yang sudah mapan saat penulisan baru mengisi kedua bentuk dan backfill menangani baris historis. Setelah verifikasi, deploy jalur baca yang mengutamakan kolom baru dan memakai nilai lama hanya menurut aturan fallback yang jelas. Ukur penggunaan fallback. Fallback yang diam dapat menyembunyikan data tidak lengkap selamanya.
Urutan rilis yang umum:
- Rilis 1 menambahkan objek database baru tanpa mengubah perilaku aplikasi.
- Rilis 2 menulis representasi transisi sambil melanjutkan pembacaan yang sudah ada.
- Rilis 3 mengalihkan pembacaan setelah backfill dan pemeriksaan konsistensi lolos.
- Rilis 4 berhenti memelihara representasi lama setelah kriteria rollback berakhir.
- Rilis 5 menghapus referensi kode lama, lalu pembersihan database dilakukan kemudian.
Pisahkan kontrak API publik dari perubahan skema fisik. Kolom database yang diubah namanya tidak mengharuskan nama kolom pada respons web, seluler, atau integrasi segera berubah. Ubah kontrak tersebut melalui kebijakan kompatibilitasnya sendiri, terutama jika klien tidak dapat ditingkatkan bersama server.
Data semua penulis. Handler HTTP hanya satu sumber perubahan. Konsumen antrean, pekerjaan terjadwal, skrip impor, alat perbaikan data, trigger database, dan operasi administrasi langsung dapat terus menghasilkan baris dengan bentuk lama. Jika memungkinkan, beri nama aplikasi pada koneksi database dan catat penggunaan jalur transisi agar proses yang terlewat terlihat.
Proses berjalan lama dapat mempertahankan asumsi usang melalui prepared statement, metadata tersimpan, atau lapisan object-relational mapping. Uji rolling restart dan perilaku connection pool sebelum fase contract. Proses yang belum mengirim trafik baru-baru ini tetap dapat gagal saat pekerjaan langka pertama kali berjalan.
Lakukan backfill data tanpa membebani database
Backfill yang aman memperbarui batch kecil yang dapat dilanjutkan dan melambat saat kesehatan produksi memburuk. Backfill dimulai hanya setelah penulis langsung dapat menjaga representasi baru.
Pilih batch berdasarkan waktu berlalu dan dampak database, bukan jumlah baris universal. Seribu baris sempit dapat selesai dalam milidetik, sementara seribu baris dengan nilai besar atau transformasi mahal dapat menghasilkan I/O signifikan. Mulailah dengan hati-hati dan targetkan transaksi yang selesai dalam hitungan detik. Commit di antara batch agar lock dan versi baris lama tidak menumpuk dalam satu transaksi.
PostgreSQL tidak mendukung ORDER BY dan LIMIT langsung pada UPDATE biasa. Pilih satu batch dalam common table expression, lalu perbarui baris tersebut:
WITH batch AS (
SELECT id
FROM my_table
WHERE id > $1
AND new_col IS NULL
ORDER BY id
LIMIT 1000
)
UPDATE my_table AS target
SET new_col = transform_expression(target.old_col)
FROM batch
WHERE target.id = batch.id
AND target.new_col IS NULL
RETURNING target.id;
Aplikasi mencatat id terbesar yang selesai sebagai cursor. Update bersyarat membuat proses ulang idempoten, sehingga crash setelah commit tidak merusak baris yang telah diproses. Simpan progres dengan cukup cermat agar cursor tidak maju melewati batch yang belum di-commit.
Cursor id menaik menghindari pemindaian berulang dari awal tabel, tetapi tidak menangkap koreksi terlambat atau baris yang dimasukkan di bawah cursor. Akhiri dengan catch-up pass pada seluruh nilai NULL yang tersisa. Jika pengenal tidak berurutan atau baris dapat berpindah status kelayakan, gunakan tabel kerja atau checkpoint eksplisit lain, bukan menganggap satu pemindaian maju sudah selesai.
Beberapa worker dapat mengklaim baris dengan FOR UPDATE SKIP LOCKED, tetapi paralelisme meningkatkan tekanan penulisan dan mempersulit pelacakan progres. Jangan gabungkan baris yang dilewati dengan cursor yang maju permanen melewatinya. Antrean pengenal yang diklaim atau pemindaian kelayakan berulang lebih aman untuk worker paralel.
Lakukan throttling berdasarkan pengukuran produksi seperti latensi kueri, koneksi aktif, lock wait, pembuatan WAL, keterlambatan replay replika, dan pertumbuhan baris mati. Jeda saat ambang terlewati, lalu lanjutkan dari checkpoint. Tidur dengan durasi tetap memang sederhana, tetapi umpan balik dari database lebih tanggap terhadap perubahan trafik.
Hindari mengubah setiap baris jika hanya sebagian yang perlu dikerjakan. Filter berdasarkan kolom baru, keadaan sumber, atau penanda migrasi. Jika transformasi mahal, hitung di luar transaksi update saat konsistensi mengizinkan, lalu lakukan penulisan bersyarat yang singkat. Simpan jumlah dan sampel nilai yang ditolak, jangan diam-diam mengarang data.
Autovacuum dan replika harus menyerap pekerjaan setelah setiap update. Backfill dapat selesai dengan sukses pada primary sementara replika tertinggal jauh atau bloat tabel menurunkan kinerja kueri kemudian. Batas laju harus memperhitungkan biaya tertunda itu, bukan hanya waktu eksekusi batch saat ini.
Verifikasi data dan trafik produksi
Migrasi siap memasuki contract hanya jika pemeriksaan data, telemetri aplikasi, dan bukti dependensi sama-sama menunjukkan bahwa jalur baru menjadi otoritatif. Penghitung pekerjaan yang selesai saja tidak membuktikan kebenaran.
Mulailah dengan kelengkapan dan konsistensi. IS DISTINCT FROM PostgreSQL membandingkan nilai sambil menangani NULL secara eksplisit, tidak seperti <> yang menghasilkan hasil tidak diketahui saat salah satu sisi bernilai NULL:
SELECT count(*)
FROM customers
WHERE normalize_phone(phone) IS DISTINCT FROM phone_e164;
Jangan berulang kali menjalankan hitungan seluruh tabel tanpa indeks pada tabel yang sangat besar dan sibuk. Gunakan validasi terkontrol satu kali, rentang pengenal terbatas, sampel, atau proses verifikasi sementara yang berjalan maju melalui tabel. Metode yang tepat bergantung pada biaya jika salah dan kapasitas database yang tersedia.
Verifikasi harus mencakup:
- Tidak ada nilai yang hilang secara tidak terduga pada baris yang memerlukan kolom baru.
- Nilai baru sesuai dengan transformasi yang disepakati, termasuk masukan kosong dan tidak valid.
- Baris serta update baru tetap konsisten setelah pemrosesan historis selesai.
- Penggunaan fallback baca sudah mencapai ambang yang direncanakan, biasanya nol untuk trafik yang dikendalikan server.
- Tingkat error, latensi kueri, lock, dan keterlambatan replika tetap dalam batas rilis.
Bandingkan juga hasil bisnis, bukan hanya kolom. Jika migrasi mengubah harga, izin, status akun, atau pengenal, validasi total dan invariant yang bergantung pada pengguna. Dua kolom dapat sama secara mekanis padahal keduanya menerapkan aturan bisnis yang salah.
Amati satu siklus operasional penuh sebelum pembersihan. Interval yang benar ditentukan oleh perilaku sistem nyata, bukan aturan tetap satu minggu. Periode ini mungkin perlu mencakup pemrosesan akhir bulan, pekerjaan penagihan yang jarang, percobaan ulang antrean yang tertunda, atau masa aktif maksimum klien seluler lama. Catat bukti bahwa setiap konsumen telah berpindah.
Lakukan canary pada pengalihan pembacaan jika arsitektur aplikasi memungkinkan. Arahkan sebagian kecil trafik ke jalur baca baru, bandingkan hasilnya, lalu perluas secara bertahap. Pertahankan tindakan rollback tetap sederhana: arahkan pembacaan kembali ke representasi yang sudah mapan tanpa membalik backfill.
Tambahkan constraint setelah data siap
Constraint harus menjadi ketat hanya setelah semua penulis mematuhi aturan dan data yang ada telah divalidasi. Menerapkan NOT NULL, check, atau foreign key selama expand dapat memblokir trafik atau menolak penulisan dari proses lama.
PostgreSQL dapat menambahkan check constraint sebagai NOT VALID, yang menerapkan aturan untuk baris baru atau yang diubah tanpa langsung memindai semua baris historis. Validasi secara terpisah setelah backfill:
ALTER TABLE customers
ADD CONSTRAINT customers_phone_e164_present
CHECK (phone_e164 IS NOT NULL) NOT VALID;
ALTER TABLE customers
VALIDATE CONSTRAINT customers_phone_e164_present;
Setelah validasi berhasil, rilis PostgreSQL yang didukung dapat memakai bukti tersebut saat menetapkan kolom menjadi NOT NULL, sehingga tidak perlu pemindaian tabel penuh lagi. Perubahan akhir tetap membutuhkan lock tabel yang kuat, jadi gunakan lock timeout terbatas dan rencana percobaan ulang:
ALTER TABLE customers
ALTER COLUMN phone_e164 SET NOT NULL;
ALTER TABLE customers
DROP CONSTRAINT customers_phone_e164_present;
Check sementara dapat dipertahankan jika berguna, tetapi menyimpan constraint setara hanya menambah kekacauan katalog tanpa mengubah aturan.
Foreign key dapat mengikuti urutan serupa dengan NOT VALID dan VALIDATE CONSTRAINT. Penulisan baru diperiksa setelah constraint dibuat, sementara validasi historis dilakukan kemudian. Tambahkan indeks pendukung dengan sengaja jika perilaku delete atau update pada relasi yang dirujuk dapat menyebabkan pemindaian mahal.
Validasi aplikasi harus mendahului penegakan database, tetapi tidak menggantikannya. Kode memberi error yang lebih jelas bagi pengguna, sedangkan database melindungi data yang ditulis lewat semua jalur. Selama rollout, perhatikan pelanggaran constraint untuk menemukan penulis yang terlewat saat audit dependensi.
Contract jalur lama dengan aman
Fase contract harus menghapus dependensi aplikasi sebelum menghapus objek database. Setelah telemetri dan verifikasi memastikan jalur baru otoritatif, pembersihan dapat dilakukan melalui rilis terpisah.
Pertama, hentikan pembacaan kolom lama dan hapus logika fallback. Kemudian nonaktifkan penulisannya dan amati produksi cukup lama untuk menangkap jalur langka. Hapus feature flag, trigger, compatibility view, skrip perbaikan, dan pekerjaan terjadwal yang menyebut representasi lama. Cari di sumber yang diekspor dan kode migrasi, tetapi periksa juga laporan, kueri integrasi, serta konfigurasi change-data-capture di luar repositori utama.
Urutan pembersihan yang aman:
- Hapus pembacaan fallback dan pastikan tidak lagi muncul dalam telemetri.
- Hentikan penulisan lama dan hapus kode sinkronisasi.
- Hapus referensi aplikasi dari semua versi yang dapat di-deploy.
- Hapus indeks serta constraint usang dengan metode online yang sesuai.
- Hapus kolom atau tabel lama pada rilis database berikutnya.
Menghapus kolom PostgreSQL terutama merupakan perubahan katalog, tetapi tetap membutuhkan lock ACCESS EXCLUSIVE. Karena itu, pernyataan singkat dapat menunggu transaksi panjang dan memblokir pekerjaan berikutnya. Terapkan lock timeout, periksa transaksi berjalan lama sebelumnya, dan jadwalkan upaya pada periode dengan risiko lebih rendah.
Gunakan DROP INDEX CONCURRENTLY untuk indeks usang jika memblokir penulisan tidak dapat diterima. Seperti pembuatan konkuren, perintah ini tidak dapat dijalankan di dalam blok transaksi dan memiliki batasan yang harus ditangani alat migrasi.
Jangan gabungkan pembersihan kode dan penghapusan fisik dalam satu rilis. Pemisahan ini memungkinkan aplikasi yang sudah dibersihkan berjalan terhadap database yang masih memiliki objek tidak terpakai. Jika masalah aplikasi muncul, rollback tetap mungkin tanpa membuat ulang skema atau merekonstruksi data.
Sebelum menghapus tabel, periksa kepemilikan sequence, view, fungsi, grant, trigger, publikasi replikasi, dan kueri eksternal. Hindari CASCADE sebagai jalan pintas dalam migrasi produksi karena dapat menghapus dependensi yang tidak termasuk perubahan yang dimaksudkan.
Tangani rollback dan langkah yang gagal
Perencanaan rollback harus menentukan tindakan aman untuk setiap fase, bukan mengandalkan satu migrasi turun generik. Objek tambahan, perpindahan data, pengalihan pembacaan, dan penghapusan memiliki sifat pemulihan yang berbeda.
Jika expand gagal memperoleh lock, biarkan aplikasi tidak berubah dan coba lagi setelah transaksi penghalang diatasi. Jika pembuatan indeks konkuren gagal, periksa apakah proses itu meninggalkan indeks tidak valid dan bersihkan objek tersebut sebelum percobaan berikutnya.
Jika backfill menimbulkan beban, jeda prosesnya. Batch idempoten yang sudah di-commit dapat tetap ada. Kurangi ukuran atau laju batch, tangani transformasi yang mahal, lalu lanjutkan dari checkpoint. Membalik jutaan update yang benar biasanya menambah risiko tanpa membantu produksi pulih.
Jika jalur baca baru menghasilkan hasil yang salah, arahkan pembacaan kembali ke representasi lama sambil mempertahankan data baru untuk diagnosis. Lanjutkan penulisan ganda hanya jika sudah diketahui benar. Saat penulisnya sendiri bermasalah, nonaktifkan atau rollback aplikasi sebelum memperbaiki baris terdampak.
Setelah contract, pemulihan mungkin memerlukan pemulihan data, bukan sekadar men-deploy build lama. Tentukan titik tanpa jalan kembali secara eksplisit. Ambil backup atau snapshot yang diwajibkan kebijakan pemulihan sistem, uji pemulihan sebelum rilis, dan pertahankan objek lama selama masa retensi yang disepakati bila biaya penyimpanan memungkinkan.
Perintah skema mungkin bersifat transaksional, tetapi efek eksternal tidak selalu tercakup. Operasi indeks konkuren, pesan antrean, perubahan cache, dan deployment aplikasi tidak berbagi satu transaksi atomik. Runbook harus menjelaskan keadaan yang dapat diamati setelah setiap kegagalan parsial dan perintah yang aman untuk melanjutkannya.
Hindari jebakan migrasi umum
Sebagian besar migrasi tanpa downtime yang gagal menerapkan keadaan baru terlalu cepat atau melupakan konsumen dari keadaan lama. Jebakan berikut perlu ditinjau secara eksplisit sebelum persetujuan.
- Menambahkan
NOT NULLsaat instance aplikasi lama masih dapat melewatkan kolom tersebut. - Menjalankan backfill besar dalam satu transaksi, sehingga lock dan versi baris tertahan terlalu lama.
- Mengganti nama kolom seolah-olah perubahan tambahan, padahal kode lama masih memakai nama aslinya.
- Mengalihkan pembacaan sebelum semua jalur penulisan dan baris historis mengisi representasi baru.
- Menganggap deployment berhasil sebagai bukti bahwa laporan, worker, replika, dan integrasi kompatibel.
Kegagalan halus lainnya berasal dari sinkronisasi dua arah. Trigger menyalin old_col ke new_col, sementara kode aplikasi menyalin new_col kembali ke old_col. Perbedaan normalisasi atau urutan trigger dapat membuat loop, menimpa nilai yang disengaja, atau membuat kepemilikan tidak jelas. Pilih satu arah dan dokumentasikan representasi mana yang otoritatif pada setiap rilis.
Default dapat menyembunyikan pembaruan penulis yang terlewat. Jika kolom wajib baru menerima default kosong atau umum, kode lama tampak kompatibel sambil menyimpan data yang tidak valid secara makna. Gunakan transisi nullable ketika ketiadaan nilai memberi informasi diagnostik, lalu terapkan aturan sebenarnya setelah setiap penulis memberikan nilai bermakna.
Feature flag sendiri tidak membuat perintah skema yang tidak kompatibel menjadi aman. Jalur kode yang dinonaktifkan mungkin tetap dimuat, dipersiapkan, atau dijalankan oleh proses lama. Objek database harus tetap ada sampai tidak ada versi aktif atau yang dapat di-deploy lagi yang merujuknya.
Kepemilikan migrasi juga penting. Tugaskan satu orang atau tim untuk menangani transisi hingga contract, termasuk tanggal verifikasi dan penghapusan. Jika tidak, kolom sementara, flag, dan pekerjaan sinkronisasi dapat tertinggal berbulan-bulan dan meningkatkan biaya setiap perubahan berikutnya.
Ganti kolom telepon tanpa downtime
Mengganti customers.phone dengan customers.phone_e164 yang dinormalisasi memerlukan kolom tambahan, kebijakan konversi yang jelas, kode kompatibel, backfill terbatas, pengalihan pembacaan, dan pembersihan tertunda. Kebijakan konversi harus ditentukan sebelum SQL karena tidak setiap nilai tersimpan dapat dinormalisasi secara otomatis.
Mulailah dengan mengklasifikasikan nilai yang ada. Nomor valid dapat dikonversi saat konteks negara yang diperlukan diketahui. Nilai kosong dapat menjadi NULL. Nomor ambigu atau cacat harus masuk ke laporan pengecualian, jangan ditebak. Tentukan apakah produk mengharuskan setiap pelanggan memiliki nomor telepon karena hal itu menentukan apakah NOT NULL layak ditambahkan nanti.
Tambahkan kolom dengan lock timeout singkat:
BEGIN;
SET LOCAL lock_timeout = '2s';
ALTER TABLE customers
ADD COLUMN phone_e164 text;
COMMIT;
Deploy kode yang menormalkan masukan baru dan menulis phone serta phone_e164 dalam satu transaksi. Awalnya, pertahankan pembacaan pada phone. Perbarui setiap penulis, termasuk impor akun, alat dukungan, pekerjaan worker, dan tes yang membuat fixture pelanggan.
Lakukan backfill baris yang memenuhi syarat dalam transaksi singkat. Catat pengenal terakhir yang diproses, jumlah yang dikonversi, jumlah yang dilewati, dan alasan setiap kategori kegagalan. Batasi laju pekerjaan berdasarkan latensi produksi dan keterlambatan replika. Setelah pemindaian maju selesai, pindai lagi nilai NULL yang memenuhi syarat untuk menangkap insert bersamaan atau baris yang terlewat setelah restart.
Jalankan pemeriksaan konsistensi dengan aturan normalisasi yang sama seperti aplikasi, lalu ambil sampel manual untuk awalan internasional, ekstensi, nilai kosong, catatan kontak duplikat, dan data impor lama. Jumlah baris membuktikan cakupan, bukan nomor telepon yang benar.
Deploy jalur baca yang mengembalikan phone_e164 saat tersedia dan memakai phone hanya untuk pengecualian yang dicatat. Pantau penggunaan fallback dan error normalisasi. Selesaikan pengecualian yang tersisa, jangan biarkan fallback menjadi perilaku permanen.
Saat kolom baru sudah otoritatif, hapus fallback dan hentikan penulisan phone. Amati pekerjaan langka serta trafik integrasi selama siklus operasional yang sesuai. Tambahkan constraint tervalidasi hanya jika aturan produk memang memerlukannya.
Terakhir, hapus referensi kode ke phone. Hapus indeks atau constraint-nya secara terpisah, kemudian hapus kolom pada migrasi berikutnya dengan waktu tunggu lock terbatas. Jika pengalihan pembacaan gagal sebelum penghapusan tersebut, rollback perilaku aplikasi selagi kedua kolom masih tersedia.
Contoh ini juga menunjukkan masalah domain yang tidak dapat diselesaikan mekanisme skema: memisahkan atau menormalkan data yang dimasukkan manusia tidak selalu tanpa kehilangan informasi. Rencana migrasi harus mempertahankan pengecualian dan memberi pemilik cara untuk menyelesaikannya.
Periksa setiap rilis sebelum dikirim
Checklist rilis harus membuktikan kompatibilitas, membatasi dampak produksi, dan menyebut tindakan pemulihan untuk fase saat ini. Simpan bukti bersama perubahan agar operator tidak perlu menyusun ulang maksudnya saat insiden.
Sebelum deployment, pastikan:
- Versi aplikasi berfungsi dengan keadaan database sebelum dan sesudah rilis ini.
- Lock timeout dan statement timeout ditetapkan untuk perintah skema yang dapat menunggu di belakang trafik.
- Pekerjaan backfill atau validasi memiliki kontrol progres, jeda, lanjutkan, dan pembatasan laju.
- Dashboard mencakup error, latensi, lock, beban database, WAL, dan keterlambatan replika.
- Tindakan rollback telah diuji tanpa bergantung pada objek yang sudah dihapus.
Catat kondisi selesai yang eksplisit. Contohnya nol kegagalan konsistensi baru selama satu siklus pekerjaan penuh, nol pembacaan fallback dari trafik yang dikendalikan server, semua konsumen yang diketahui telah ditingkatkan, dan kueri validasi terkontrol yang berhasil. Persentase selesai berguna saat backfill, tetapi 100 persen diproses tidak sama dengan 100 persen benar.
Tinjau urutan migrasi secara terpisah dari code review. Kumpulan SQL dan perubahan aplikasi yang benar tetap dapat gagal jika deployment menjalankannya dalam urutan yang salah. Nyatakan langkah mana yang hanya boleh dimulai setelah langkah lain selesai.
Kondisi berhenti harus berupa angka bila memungkinkan. Tentukan latensi kueri, lock wait, keterlambatan replika, tingkat error, dan durasi batch yang dapat diterima. Saat ambang terlewati, operator harus tahu apakah perlu menjeda pekerjaan, membatalkan pernyataan yang menunggu, atau mengarahkan ulang pembacaan tanpa meminta persetujuan baru saat insiden.
Migrasi selesai hanya setelah representasi baru menangani pembacaan dan penulisan, data historis lolos verifikasi, objek lama dihapus, dan alat operasional sementara sudah tidak ada.
Jadikan proses dapat diulang
Runbook migrasi yang dapat digunakan kembali menjadikan expand/contract pekerjaan rilis biasa dengan pemilik yang jelas dan gerbang yang terukur. Runbook harus cukup singkat untuk diikuti saat deployment langsung dan cukup spesifik untuk menjelaskan keadaan kegagalan parsial.
Gunakan lima bagian dalam runbook:
- Expansion: operasi skema yang tepat, lock yang diharapkan, timeout, dan kebutuhan transaksi.
- Compatibility: kode, penulis, pembaca, flag, klien, serta urutan deployment yang terdampak.
- Backfill: kebijakan transformasi, batching, checkpoint, throttling, dan penanganan pengecualian.
- Verification: pemeriksaan SQL, invariant bisnis, telemetri, dan ambang selesai.
- Contraction: penghapusan dependensi, periode observasi, pembersihan fisik, dan batas pemulihan.
Tugaskan pemilik dan tanggal selesai yang diharapkan untuk setiap objek transisi. Lacak kolom, indeks, flag, trigger, dan pekerjaan di tempat yang sama. Pembersihan adalah bagian dari migrasi, bukan pemeliharaan opsional.
Bagi tim yang membangun dengan Koder.ai, Planning Mode dapat dipakai untuk merinci fase dan checkpoint ini sebelum perubahan produksi dimulai. Ekspor kode sumber juga memungkinkan SQL migrasi serta logika kompatibilitas mendapat review yang sama seperti kode aplikasi lainnya. Koder.ai mendukung deployment, hosting, snapshot, dan rollback, tetapi rollback aplikasi tidak boleh dianggap membalik transformasi data yang sudah di-commit. Pertahankan kompatibilitas skema sampai rencana pemulihan database tidak lagi bergantung pada representasi lama.
Jadwalkan pekerjaan dengan banyak penulisan pada saat trafik lebih rendah bila memungkinkan, tetapi jangan menjadikan waktu sebagai satu-satunya pengaman. Transaksi terbatas, throttling berbasis umpan balik, progres yang dapat diamati, dan tindakan jeda yang telah diuji membuat migrasi online tetap mudah dikelola saat trafik atau data berperilaku berbeda dari yang diharapkan.
Pertanyaan umum
Mengapa perubahan skema dapat menyebabkan gangguan layanan?
Perubahan skema dapat mengganggu produksi ketika versi aplikasi lama dan baru mengharapkan struktur database yang berbeda. Saat rolling deployment, keduanya bisa berjalan bersamaan, sehingga menghapus atau mengganti nama kolom terlalu cepat dapat membuat pembacaan atau penulisan gagal.
Apa itu pola migrasi expand/contract?
Expand/contract membagi perubahan yang tidak kompatibel menjadi tahap yang aman. Tambahkan struktur baru terlebih dahulu, lalu pindahkan kode dan data ke sana, kemudian hapus struktur lama setelah semua konsumen berhenti memakainya.
Bagaimana mengganti nama atau mengganti kolom database tanpa downtime?
Tambahkan kolom baru terlebih dahulu dan pertahankan kolom lama. Deploy kode yang dapat bekerja dengan kedua kolom, lakukan backfill baris lama dalam batch kecil, alihkan pembacaan setelah validasi, lalu hapus kolom lama pada rilis berikutnya.
Bisakah saya menambahkan kolom PostgreSQL tanpa memblokir trafik?
Biasanya bisa. Kolom nullable tanpa nilai default sering kali hanya merupakan perubahan metadata singkat di PostgreSQL, tetapi tetap membutuhkan lock tabel. Tetapkan lock timeout singkat agar migrasi gagal alih-alih menunggu transaksi panjang.
Bagaimana membuat indeks tanpa memblokir penulisan?
Gunakan CREATE INDEX CONCURRENTLY saat tabel harus tetap dapat ditulis. Proses ini lebih lama dan menambah beban database, serta tidak dapat dijalankan di dalam blok transaksi. Pantau latensi, WAL, dan keterlambatan replika selama proses berjalan.
Kapan aplikasi harus menulis ke kolom lama dan baru sekaligus?
Tulis kedua nilai dalam transaksi database yang sama ketika kedua representasi harus tetap terkini. Tentukan kolom mana yang diprioritaskan jika nilainya berbeda, dan gunakan aturan normalisasi yang sama di API, worker, impor, serta alat dukungan.
Bagaimana melakukan backfill tabel PostgreSQL besar dengan aman?
Proses batch singkat yang dapat dilanjutkan, lalu commit setelah setiap batch. Simpan checkpoint, perbarui hanya baris yang masih perlu dikerjakan, dan perlambat atau jeda pekerjaan ketika latensi kueri, lock wait, volume WAL, atau lag replika meningkat.
Bagaimana mengetahui backfill sudah selesai dan benar?
Jangan mengalihkan pembacaan hanya karena backfill selesai. Periksa nilai wajib sudah ada, bandingkan representasi lama dan baru, pantau pembacaan fallback, lalu pastikan penulisan baru tetap konsisten setelah pemrosesan data historis.
Kapan saya harus menambahkan NOT NULL, check constraint, atau foreign key?
Tambahkan constraint ketat setelah data yang ada lolos validasi dan setiap penulis aktif mengirim nilai yang valid. PostgreSQL memungkinkan beberapa constraint ditambahkan sebagai NOT VALID, diterapkan untuk baris baru, lalu data historis divalidasi secara terpisah.
Kapan aman menghapus jalur skema lama?
Hapus pembacaan fallback terlebih dahulu, lalu hentikan penulisan lama dan amati sistem selama satu siklus operasional penuh. Setelah semua aplikasi, pekerjaan, laporan, integrasi, dan klien tidak lagi merujuk objek lama, hapus kode terkait dan hapus kolom atau tabel database pada rilis berikutnya.