Upgrade Mayor PostgreSQL Tanpa Downtime dengan Logical Replication

Foto oleh Diego Delso via Wikimedia Commons (CC BY-SA 4.0)
pg_upgrade mengharuskan server lama dihentikan saat me-relink direktori data, yang berarti jendela pemeliharaan dan jalur rollback yang canggung. Logical replication menjalankan server versi baru berdampingan dengan yang lama dan menjaganya tetap sinkron terus-menerus, jadi satu-satunya downtime adalah beberapa detik untuk memindahkan connection string aplikasi. Ini pilihan tepat ketika database tidak boleh offline selama durasi pg_upgrade.
Tidak. Angka yang tersimpan di kolom serial dan identity direplikasi sebagai data baris biasa, tetapi objek sequence yang menghasilkan nilai baru tidak. Di subscriber setiap sequence masih melaporkan nilai awalnya. Anda harus menjalankan setval di server baru saat cutover, idealnya dengan buffer ditambahkan, atau insert pertama Anda akan gagal dengan duplicate key violation.
pg_createsubscriber, diperkenalkan di PostgreSQL 17, mengubah physical streaming standby yang sudah ada menjadi logical subscriber tanpa menyalin ulang data. Ia mencatat sebuah LSN dan memutar mundur posisi apply alih-alih menjalankan COPY awal penuh. PostgreSQL 18 menambahkan flag --all untuk menyiapkan subscription bagi setiap database dalam satu perintah, mengubah sinkronisasi awal berjam-jam menjadi hitungan detik.
Hanya momen cutover yang memiliki downtime nyata, dan biasanya di bawah satu detik hingga beberapa detik. Anda menghentikan penulisan di primary lama, memastikan lag replikasi mencapai nol, mempromosikan sequence, dan mengarahkan aplikasi ke server baru. Semua sebelum itu — salin skema, salin data awal, streaming catch-up — terjadi saat database lama tetap sepenuhnya online.
Tidak. Large object yang diakses melalui API lo dan pg_largeobject tidak pernah direplikasi oleh logical replication. Kalau database Anda memakainya, Anda harus menyalinnya dengan langkah terpisah, atau memigrasikan datanya ke kolom bytea biasa sebelumnya. Ini salah satu celah yang menjebak tim yang mengira logical replication menghasilkan clone penuh.

Foto oleh Diego Delso via Wikimedia Commons (CC BY-SA 4.0)
Ringkasan Utama
Untuk melakukan upgrade PostgreSQL antar versi mayor dengan downtime nyaris nol, replikasikan database yang sedang berjalan ke instance versi baru menggunakan logical replication: buat PUBLICATION di server lama, SUBSCRIPTION di server baru, tunggu sampai lag mencapai nol, lalu alihkan trafik. Jebakannya adalah logical replication menyalin baris tabel tetapi bukan sequence, large object, atau DDL, jadi Anda harus menyinkronkannya secara manual selama jendela cutover.
Cara klasik untuk upgrade versi mayor PostgreSQL adalah pg_upgrade, dan itu berfungsi baik sampai Anda punya database yang tidak boleh offline. pg_upgrade butuh server lama dihentikan saat me-relink direktori data, dan bahkan dalam mode --link Anda tetap menghadapi jendela pemeliharaan plus skenario rollback yang menakutkan kalau ada yang gagal di tengah jalan. Untuk sebuah layanan yang saya kelola dan diakses pelanggan sepanjang waktu, jendela itu tidak dapat diterima. Logical replication memberi bentuk yang berbeda sama sekali: server versi baru berjalan berdampingan dengan yang lama, tetap sinkron terus-menerus, dan satu-satunya downtime nyata adalah beberapa detik untuk memindahkan connection string aplikasi.
Berbeda dengan physical (streaming) replication, logical replication men-decode write-ahead log menjadi event perubahan tingkat baris dan memutarnya ulang sebagai statement INSERT, UPDATE, dan DELETE biasa di subscriber. Karena bekerja di tingkat baris logis bukan tingkat byte, publisher dan subscriber bisa menjalankan versi mayor yang berbeda. Properti tunggal itulah yang menjadikannya alat yang tepat untuk upgrade. Panduan ini mengasumsikan perpindahan dari primary Postgres 15 ke target Postgres 18, tetapi bentuknya identik untuk lompatan mayor-ke-mayor mana pun yang didukung logical decoding.
Di server lama, atur wal_level ke logical dan sediakan cukup replication slot serta WAL sender. Ini membutuhkan restart, jadi lakukan saat pemeliharaan rutin jauh sebelum upgrade sesungguhnya. Lalu salin skema ke server baru lebih dulu, karena logical replication mereplikasi data tetapi tidak pernah DDL. Setiap tabel yang ingin Anda replikasi harus sudah ada di subscriber dengan definisi yang cocok, dan setiap tabel butuh primary key atau REPLICA IDENTITY yang diset eksplisit agar event UPDATE dan DELETE bisa menemukan baris targetnya.
-- On the OLD server (publisher), in postgresql.conf:
wal_level = logical
max_replication_slots = 10
max_wal_senders = 10
-- then restart PostgreSQL
-- Copy schema only (no data) from old -> new. Run from a host that can reach both:
pg_dump -h old-host -U postgres --schema-only --no-owner mydb \
| psql -h new-host -U postgres mydb
-- Create the publication on the OLD server:
CREATE PUBLICATION upgrade_pub FOR ALL TABLES;Di server baru, buat SUBSCRIPTION yang menunjuk ke publisher. Secara default ia melakukan dua hal: salinan data awal (COPY paralel atas isi terkini setiap tabel) diikuti streaming perubahan baru secara terus-menerus. Untuk database besar, salinan awal adalah bagian yang lambat dan berjalan di bawah sebuah replication slot, jadi publisher akan menahan WAL selama itu. Pantau disk server lama di sini. Setelah salinan selesai, status setiap tabel berubah menjadi r (ready) di pg_subscription_rel dan streaming steady-state mengambil alih.
-- On the NEW server (subscriber):
CREATE SUBSCRIPTION upgrade_sub
CONNECTION 'host=old-host dbname=mydb user=repl password=secret'
PUBLICATION upgrade_pub;
-- Watch initial sync progress (all rows should reach state 'r'):
SELECT srrelid::regclass AS table, srsubstate
FROM pg_subscription_rel;
-- Watch replication lag from the OLD server:
SELECT slot_name,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), confirmed_flush_lsn)) AS lag
FROM pg_replication_slots;Di PostgreSQL 17 ke atas Anda bisa melewati COPY awal yang mahal sepenuhnya dengan pg_createsubscriber, yang mengubah physical standby yang sudah ada menjadi logical subscriber dengan mencatat sebuah LSN dan memutar mundur posisi apply. PostgreSQL 18 menambahkan flag --all yang menyiapkan subscription untuk setiap database dalam instance dalam satu perintah. Kalau Anda sudah menjalankan streaming standby, ini mengubah salinan awal berjam-jam menjadi hitungan detik.
Di sinilah orang sering terbakar. Logical replication menyalin angka yang ada di kolom serial dan identity Anda sebagai data biasa, tetapi tidak mereplikasi objek sequence yang menghasilkannya. Di subscriber, setiap sequence masih melaporkan nilai awalnya. Promosikan cluster baru tanpa memperbaiki ini dan insert pertama Anda langsung disambut deretan duplicate key violation. Large object (API pg_largeobject / lo) juga tidak pernah direplikasi — kalau Anda memakainya, butuh langkah salin terpisah. Dan karena DDL tidak direplikasi, Anda harus membekukan migrasi skema selama cutover atau menerapkan setiap perubahan ke kedua server secara manual.
Perbaikan sequence adalah sebuah skrip yang Anda hasilkan di publisher dan jalankan di subscriber, dan Anda menjalankannya selambat mungkin dalam cutover. Tambahkan buffer pengaman ke setiap nilai agar transaksi yang masih berjalan saat peralihan tidak bisa bertabrakan dengan sequence yang baru dipromosikan. Buffer seribu adalah asuransi murah; celah pada sequence tidak pernah jadi masalah.
-- Run on the OLD server to GENERATE setval statements, then pipe them
-- to the NEW server during cutover. The +1000 is a collision buffer.
SELECT format(
'SELECT setval(%L, %s);',
schemaname || '.' || sequencename,
COALESCE(last_value, 1) + 1000
)
FROM pg_sequences
WHERE last_value IS NOT NULL;
-- Example of what it produces (run these on the NEW server):
-- SELECT setval('public.orders_id_seq', 84213);
-- SELECT setval('public.users_id_seq', 15044);Jangan lupa menonaktifkan langkah migrasi skema aplikasi Anda sebelum jendela cutover dibuka. Satu ALTER TABLE yang dijalankan di primary lama saat replikasi aktif tidak akan sampai ke server baru, dan baris berikutnya yang bergantung pada perubahan kolom itu akan merusak replikasi dengan error yang menghentikan seluruh subscription sampai Anda memperbaiki skema secara manual.
Cutover adalah satu-satunya momen dengan downtime nyata (di bawah satu detik), jadi buat skripnya dan latih di staging dulu. Tujuannya adalah memastikan subscriber telah menangkap setiap perubahan terakhir sebelum ada klien yang menulis ke sana.
Biarkan primary lama tetap berjalan dan tidak disentuh selama satu dua hari setelah cutover. Itu adalah rollback instan Anda: kalau ada yang salah di versi baru, kembalikan saja connection string. Ingat saja bahwa server lama belum menerima penulisan yang masuk ke server baru, jadi rollback setelah trafik nyata berarti merekonsiliasi celah itu — itulah tepatnya alasan Anda melakukan smoke-test ketat di menit-menit pertama.
| Objek | Direplikasi otomatis? | Yang harus Anda lakukan |
|---|---|---|
| Baris tabel (INSERT/UPDATE/DELETE) | Ya | Tidak ada — ini tugas intinya |
| Sequence (generator serial / identity) | Tidak | Jalankan setval dengan buffer saat cutover |
| Large object (lo / pg_largeobject) | Tidak | Salin terpisah, atau migrasikan ke bytea |
| Perubahan skema / DDL | Tidak | Salin skema dulu; bekukan migrasi |
| View, materialized view, foreign table | Tidak | Buat ulang di subscriber secara manual |
| TRUNCATE | Ya (dengan catatan) | Pastikan pengelompokan foreign-key cocok |
Tak satu pun dari ini eksotis begitu Anda pernah melakukannya sekali, tetapi setiap baris Tidak itu adalah insiden produksi yang menunggu orang yang mengira logical replication adalah clone penuh. Bukan — ia adalah stream perubahan baris, dan objek yang dilewatinya adalah yang Anda tangani secara sengaja saat cutover. Perlu dicatat: PostgreSQL 19 dijadwalkan akhirnya menambahkan sinkronisasi sequence native ke logical replication, yang menghapus satu snag paling menyebalkan di seluruh prosedur ini. Sampai Anda menjalankan 19, skrip setval tetap ada di checklist cutover Anda.