Read replica dari chapter sebelumnya membuat beban baca tahan lama — tetapi begitu primary mati, seluruh aplikasi Anda ikut lumpuh menulis, sehatinya replica tak berguna. Failover adalah proses mengangkat standby menjadi primary baru: satu perintah promosi, dan tulisan mengalir lagi. Kabar baiknya, mekanismenya benar-benar Anda kuasai di chapter ini dengan pasangan server lokal — kita akan mematikan primary secara sadar, mempromosikan standby, lalu menyaksikan akibat yang jarang diperlihatkan tutorial manapun: bekas primary yang menyala kembali tidak tahu dunia sudah berpindah, dan dalam sekejap kita memelihara dua primary dengan isi tabel berbeda. Dari situlah konsep timeline, fencing, dan switchover versus failover mendapat arti yang sesungguhnya.
Promosi: Standby Berhenti Menjadi Standby #
Promosi pada dasarnya memerintahkan standby: “berhentilah menunggu WAL dari primary, akhirilah recovery, dan mulailah menerima tulisan”. Tidak ada penyalinan data — semua yang dibutuhkan sudah ada di standby karena streaming replication. Yang berubah hanyalah mode operasi: file standby.signal dihapus, recovery diakhiri dengan end-of-recovery checkpoint, dan server membuka gerbang tulisan di atas timeline baru.
Istilah timeline perlu diperkenalkan sejak sekarang karena menjadi tulang punggung seluruh bab ini. Timeline adalah nomor garis sejarah WAL: setiap cluster lahir di timeline 1, dan setiap promosi memulai garis baru — timeline 2, 3, dan seterusnya — dari titik promosi. Nomor ini tersimpan di delapan digit pertama setiap nama file WAL (00000002... adalah segmen timeline 2) dan di file riwayat 00000002.history yang mencatat titik percabangan. File-file itu akan kita lihat langsung sebentar lagi.
Kita bangun pasangan demo yang kini sudah familier — primary port 54365, standby port 54366, direktori /home/z/my-project/tmp/rp_failover:
#!/usr/bin/env bash
# Menyiapkan pasangan primary-standby untuk demo failover.
# Primary di port 54365, standby di port 54366 (database toko).
# Skrip promosi.sh dan divergensi.sh melanjutkan pasangan ini.
set -eu
PGBIN=/home/z/my-project/tools/dl/pgdeb/usr/lib/postgresql/17/bin
BASE=/home/z/my-project/tmp/rp_failover
PRI=$BASE/pgdata_pusat # primary, port 54365
STD=$BASE/pgdata_cabang # standby, port 54366
SOCK=$BASE/sock
# Buang sisa percobaan sebelumnya bila ada.
"$PGBIN/pg_ctl" -D "$PRI" -m immediate stop >/dev/null 2>&1 || true
"$PGBIN/pg_ctl" -D "$STD" -m immediate stop >/dev/null 2>&1 || true
rm -rf "$BASE"
mkdir -p "$SOCK"
# Polling: tunggu sampai sebuah query mengembalikan nilai yang diharapkan.
tunggu() { # port database sql nilai_yang_diharapkan
local port="$1" db="$2" sql="$3" harapan="$4" i hasil
for i in $(seq 1 150); do
hasil=$(psql -X -q -t -A -h 127.0.0.1 -p "$port" -U postgres -d "$db" \
-c "$sql" 2>/dev/null) || true
if [ "$hasil" = "$harapan" ]; then return 0; fi
sleep 0.2
done
echo "TIMEOUT: menunggu '$sql' menjadi $harapan (port $port)" >&2
return 1
}
echo "== Langkah 1: initdb dan jalankan primary di port 54365 =="
"$PGBIN/initdb" -D "$PRI" --no-locale -E UTF8 -U postgres >/dev/null 2>&1
"$PGBIN/pg_ctl" -D "$PRI" \
-o "-p 54365 -k $SOCK -c listen_addresses=127.0.0.1" \
-l "$BASE/primary.log" start >/dev/null 2>&1
tunggu 54365 postgres "SELECT 1;" "1"
echo "primary siap"
echo "== Langkah 2: data awal di primary =="
psql -X -h 127.0.0.1 -p 54365 -U postgres -d postgres \
-c "CREATE DATABASE toko;"
psql -X -h 127.0.0.1 -p 54365 -U postgres -d toko \
-c "CREATE TABLE produk (id int PRIMARY KEY, nama text, harga numeric);" \
-c "INSERT INTO produk VALUES (1, 'Kopi Tubruk', 15000), (2, 'Teh Celup', 8000);"
echo "== Langkah 3: pg_basebackup -R, lalu jalankan standby di port 54366 =="
PGDATABASE=postgres pg_basebackup -h 127.0.0.1 -p 54365 -U postgres \
-D "$STD" -R -X stream -c fast >/dev/null 2>&1
"$PGBIN/pg_ctl" -D "$STD" \
-o "-p 54366 -k $SOCK -c listen_addresses=127.0.0.1 -c cluster_name=standby_cabang" \
-l "$BASE/standby.log" start >/dev/null 2>&1
tunggu 54366 toko "SELECT count(*) FROM produk;" "2"
echo "standby menyala dan konsisten"
echo "== Langkah 4: satu tulisan terakhir sebelum bencana =="
psql -X -h 127.0.0.1 -p 54365 -U postgres -d toko \
-c "INSERT INTO produk VALUES (3, 'Gula Pasir', 12000);"
tunggu 54366 toko "SELECT count(*) FROM produk;" "3"
psql -X -h 127.0.0.1 -p 54366 -U postgres -d toko \
-c "SELECT pg_is_in_recovery() AS sedang_recovery;"Output yang Dihasilkan:
== Langkah 1: initdb dan jalankan primary di port 54365 ==
primary siap
== Langkah 2: data awal di primary ==
CREATE DATABASE
CREATE TABLE
INSERT 0 2
== Langkah 3: pg_basebackup -R, lalu jalankan standby di port 54366 ==
standby menyala dan konsisten
== Langkah 4: satu tulisan terakhir sebelum bencana ==
INSERT 0 1
sedang_recovery
-----------------
t
(1 row)Baris terakhir sengaja menampilkan pg_is_in_recovery() — fungsi yang menjawab “apakah server ini sedang menjalankan recovery?” dengan t (true) atau f (false). Inilah indikator paling andal untuk membedakan standby dari primary, dan kita akan memakainya sebagai penanda sebelum-sesudah promosi.
Failover di Praktik: Crash, Promosi, Tulisan #
Skrip berikut memainkan skenario bencana secara utuh: satu tulisan terakhir yang sempat tereplikasi, primary dimatikan mendadak (-m immediate meniru crash — tanpa checkpoint penutup), standby masih melayani baca, lalu pg_ctl promote mengubah segalanya:
#!/usr/bin/env bash
# Simulasi failover: primary mati mendadak, standby dipromosikan
# menjadi primary baru yang menerima tulisan.
# Skrip ini melanjutkan pasangan server dari siapkan-pasangan.sh:
# primary di port 54365, standby di port 54366, database toko.
# Setelah skrip ini: primary lama mati, standby sudah menjadi
# primary baru (timeline 2) dan tetap berjalan.
set -eu
PGBIN=/home/z/my-project/tools/dl/pgdeb/usr/lib/postgresql/17/bin
BASE=/home/z/my-project/tmp/rp_failover
PRI=$BASE/pgdata_pusat
STD=$BASE/pgdata_cabang
# Polling: tunggu sampai sebuah query mengembalikan nilai yang diharapkan.
tunggu() { # port database sql nilai_yang_diharapkan
local port="$1" db="$2" sql="$3" harapan="$4" i hasil
for i in $(seq 1 150); do
hasil=$(psql -X -q -t -A -h 127.0.0.1 -p "$port" -U postgres -d "$db" \
-c "$sql" 2>/dev/null) || true
if [ "$hasil" = "$harapan" ]; then return 0; fi
sleep 0.2
done
echo "TIMEOUT: menunggu '$sql' menjadi $harapan (port $port)" >&2
return 1
}
echo "== Langkah 1: tulisan terakhir yang sempat tereplikasi =="
psql -X -h 127.0.0.1 -p 54365 -U postgres -d toko \
-c "INSERT INTO produk VALUES (4, 'Susu Kental', 9000);"
tunggu 54366 toko "SELECT count(*) FROM produk;" "4"
echo "keempat baris sudah aman di standby"
echo "== Langkah 2: bencana — primary mati mendadak (immediate) =="
"$PGBIN/pg_ctl" -D "$PRI" -m immediate stop >/dev/null 2>&1
echo "primary mati"
echo "== Langkah 3: standby masih melayani pembacaan =="
psql -X -h 127.0.0.1 -p 54366 -U postgres -d toko \
-c "SELECT count(*) AS baris_tersedia FROM produk;"
echo "== Langkah 4: promosikan standby menjadi primary =="
"$PGBIN/pg_ctl" -D "$STD" -W promote >/dev/null 2>&1
tunggu 54366 toko "SELECT pg_is_in_recovery();" "f"
echo "promosi selesai"
echo "== Langkah 5: primary baru menerima tulisan =="
psql -X -h 127.0.0.1 -p 54366 -U postgres -d toko \
-c "INSERT INTO produk VALUES (5, 'Keju Slice', 21000);"
psql -X -h 127.0.0.1 -p 54366 -U postgres -d toko \
-c "SELECT id, nama, harga FROM produk ORDER BY id;"
echo "== Langkah 6: bukti timeline baru di primary hasil promosi =="
psql -X -h 127.0.0.1 -p 54366 -U postgres -d toko \
-c "CHECKPOINT;" \
-c "SELECT timeline_id FROM pg_control_checkpoint();"
echo "file riwayat timeline di pg_wal:"
ls "$STD/pg_wal" | grep historyOutput yang Dihasilkan:
== Langkah 1: tulisan terakhir yang sempat tereplikasi ==
INSERT 0 1
keempat baris sudah aman di standby
== Langkah 2: bencana — primary mati mendadak (immediate) ==
primary mati
== Langkah 3: standby masih melayani pembacaan ==
baris_tersedia
----------------
4
(1 row)
== Langkah 4: promosikan standby menjadi primary ==
promosi selesai
== Langkah 5: primary baru menerima tulisan ==
INSERT 0 1
id | nama | harga
----+-------------+-------
1 | Kopi Tubruk | 15000
2 | Teh Celup | 8000
3 | Gula Pasir | 12000
4 | Susu Kental | 9000
5 | Keju Slice | 21000
(5 rows)
== Langkah 6: bukti timeline baru di primary hasil promosi ==
CHECKPOINT
timeline_id
-------------
2
(1 row)
file riwayat timeline di pg_wal:
00000002.historyMari kita bedah momen-momen kuncinya:
- Langkah 1 — memastikan replikasi sempat tuntas:
Susu Kental(baris ke-4) di-commit di primary dan dipastikan sudah tiba di standby sebelum bencana. Baris inilah yang menyelamatkan data; bila crash terjadi satu detik lebih awal, baris ini hilang bersama primary — inilah RPO (recovery point objective) replikasi asinkron yang dibahas di chapter Replication Lag . - Langkah 3 — dunia tidak berhenti untuk pembaca: walau primary mati, standby tetap melayani
SELECTdengan empat baris utuh. Aplikasi baca Anda tetap hidup selama failover berlangsung — hanya tulisan yang menunggu. - Langkah 4 — satu perintah, satu dunia baru:
pg_ctl promote -Wmengakhiri recovery di standby. Pollingpg_is_in_recovery()sampai bernilaifmemastikan promosi benar-benar selesai sebelum kita melangkah. - Langkah 5 — gerbang tulisan terbuka:
INSERTyang semula ditolak dengancannot execute INSERT in a read-only transaction(chapter Read Replica ) kini diterima. Server yang sama, data yang sama, peran yang baru. - Langkah 6 — jejak arkeologi timeline: setelah
CHECKPOINTdipaksa (agar nilai terbaru pasti terbaca),pg_control_checkpoint()melaporkantimeline_id = 2, dan di direktoripg_walberdiri file00000002.history— arsip percabangan yang mencatat “timeline 2 lahir dari timeline 1 di titik WAL sekian”.
Bekas Primary yang Bangun: Divergensi dan Split Brain #
Cerita failover di kebanyakan tutorial berhenti di promosi. Nyatanya, bab yang paling berbahaya baru dimulai ketika bekas primary menyala kembali — entah karena listrik kantor utama pulih, entah karena teknisi yang tidak tahu kejadian baru saja menekan tombol power. Server itu bangun sebagai primary timeline 1, tidak tahu sedikit pun bahwa timeline 2 sudah ada, dan dengan polos menerima tulisan. Skrip berikut membiarkan kejadian itu terjadi agar Anda menyaksikan akibatnya:
#!/usr/bin/env bash
# Membuktikan divergensi setelah failover: primary lama yang dinyalakan
# kembali TIDAK otomatis tahu bahwa dunia sudah berpindah timeline.
# Ia bangun sebagai primary timeline 1, menerima tulisan, dan kini ada
# dua primary dengan data berbeda — split brain.
# Skrip ini melanjutkan keadaan dari promosi.sh:
# primary lama (54365) mati, primary baru hasil promosi (54366) aktif.
set -eu
PGBIN=/home/z/my-project/tools/dl/pgdeb/usr/lib/postgresql/17/bin
BASE=/home/z/my-project/tmp/rp_failover
PRI=$BASE/pgdata_pusat # primary lama, port 54365
STD=$BASE/pgdata_cabang # primary baru (hasil promosi), port 54366
SOCK=$BASE/sock
# Polling: tunggu sampai sebuah query mengembalikan nilai yang diharapkan.
tunggu() { # port database sql nilai_yang_diharapkan
local port="$1" db="$2" sql="$3" harapan="$4" i hasil
for i in $(seq 1 150); do
hasil=$(psql -X -q -t -A -h 127.0.0.1 -p "$port" -U postgres -d "$db" \
-c "$sql" 2>/dev/null) || true
if [ "$hasil" = "$harapan" ]; then return 0; fi
sleep 0.2
done
echo "TIMEOUT: menunggu '$sql' menjadi $harapan (port $port)" >&2
return 1
}
echo "== Langkah 1: nyalakan kembali primary lama =="
"$PGBIN/pg_ctl" -D "$PRI" \
-o "-p 54365 -k $SOCK -c listen_addresses=127.0.0.1" \
-l "$BASE/primary-restart.log" start >/dev/null 2>&1
tunggu 54365 toko "SELECT pg_is_in_recovery();" "f"
echo "primary lama bangun sebagai server tulis penuh"
echo "== Langkah 2: timeline kedua server kini berbeda =="
psql -X -h 127.0.0.1 -p 54365 -U postgres -d toko \
-c "SELECT timeline_id FROM pg_control_checkpoint();" \
-c "SELECT pg_is_in_recovery() AS sedang_recovery;"
psql -X -h 127.0.0.1 -p 54366 -U postgres -d toko \
-c "SELECT timeline_id FROM pg_control_checkpoint();" \
-c "SELECT pg_is_in_recovery() AS sedang_recovery;"
echo "== Langkah 3: dua-duanya menerima tulisan — split brain =="
psql -X -h 127.0.0.1 -p 54365 -U postgres -d toko \
-c "INSERT INTO produk VALUES (6, 'Susu UHT', 17500);"
psql -X -h 127.0.0.1 -p 54366 -U postgres -d toko \
-c "INSERT INTO produk VALUES (7, 'Kopi Hitam', 13500);"
echo "== Langkah 4: isi tabel yang kini berbeda dua dunia =="
echo "primary LAMA (timeline 1):"
psql -X -h 127.0.0.1 -p 54365 -U postgres -d toko \
-c "SELECT id, nama, harga FROM produk ORDER BY id;"
echo "primary BARU hasil promosi (timeline 2):"
psql -X -h 127.0.0.1 -p 54366 -U postgres -d toko \
-c "SELECT id, nama, harga FROM produk ORDER BY id;"Output yang Dihasilkan:
== Langkah 1: nyalakan kembali primary lama ==
primary lama bangun sebagai server tulis penuh
== Langkah 2: timeline kedua server kini berbeda ==
timeline_id
-------------
1
(1 row)
sedang_recovery
-----------------
f
(1 row)
timeline_id
-------------
2
(1 row)
sedang_recovery
-----------------
f
(1 row)
== Langkah 3: dua-duanya menerima tulisan — split brain ==
INSERT 0 1
INSERT 0 1
== Langkah 4: isi tabel yang kini berbeda dua dunia ==
primary LAMA (timeline 1):
id | nama | harga
----+-------------+-------
1 | Kopi Tubruk | 15000
2 | Teh Celup | 8000
3 | Gula Pasir | 12000
4 | Susu Kental | 9000
6 | Susu UHT | 17500
(5 rows)
primary BARU hasil promosi (timeline 2):
id | nama | harga
----+-------------+-------
1 | Kopi Tubruk | 15000
2 | Teh Celup | 8000
3 | Gula Pasir | 12000
4 | Susu Kental | 9000
5 | Keju Slice | 21000
7 | Kopi Hitam | 13500
(6 rows)Output itu layak dibaca pelan-pelan, karena inilah mimpi buruk administrator database. Dua server sama-sama menjawab sedang_recovery = f — dua-duanya primary. Tabel produk di timeline 1 berisi Susu UHT yang tidak dikenal timeline 2; tabel di timeline 2 berisi Keju Slice dan Kopi Hitam yang tidak dikenal timeline 1. Keduanya “benar” menurut sejarah masing-masing, dan tidak ada mekanisme di PostgreSQL yang akan mendamaikan mereka secara otomatis. Keadaan dua primary aktif sekaligus ini disebut split brain, dan perbaikannya selalu menyakitkan: satu sisi harus dibuang atau direwind, dan tulisan di sisi yang kalah hilang.
Setelah difencing, bekas primary punya dua jalan kembali bertugas: di-re-clone dari nol dengan pg_basebackup seperti chapter Streaming Replication
, atau — bila divergensinya kecil — di-rewind dengan pg_rewind, alat bawaan PostgreSQL yang memutar balik perubahan pasca-titik-percabangan lalu menjadikannya standby timeline baru. pg_rewind menuntut wal_log_hints atau checksum data aktif sejak cluster dibuat; di demo kita dua-duanya tidak aktif, sehingga re-clone adalah jalur yang jujur disebut.
Switchover versus Failover #
Promosi yang kita lakukan meniru failover: bencana tidak direncanakan, primary mati mendadak. Adik kandungnya yang jauh lebih jinak adalah switchover — pergantian primary yang direncanakan: primary lama diturunkan dengan rapi (pg_ctl stop -m fast), dipastikan replikasi tuntas, standby dipromosikan, lalu aplikasi dialihkan. Failover yang dipicu kegagalan dan switchover yang dipicu jadwal memakai perintah promosi yang sama, tetapi profil risikonya jauh berbeda:
| Aspek | Failover (tak direncanakan) | Switchover (direncanakan) |
|---|---|---|
| Pemicu | Primary gagal / tidak responsif | Jadwal maintenance, patching, upgrade minor |
| Nasib bekas primary | Divergen — wajib fencing lalu rewind/re-clone | Bersih — turun rapi sebelum promosi, tinggal dibangunkan sebagai standby baru |
| Potensi kehilangan tulisan | Ada (WAL yang belum terkirim) | Nyaris nol (replikasi dipastikan tuntas sebelum penurunan) |
| Split brain | Risiko nyata tanpa fencing | Risiko rendah — tidak ada dua primary hidup bersamaan |
| Pelaksana | Biasanya otomatis (tooling HA) karena manusia terlalu lambat | Biasanya manual atau terjadwal |
Dua baris pertama tabel itu menjelaskan mengapa switchover adalah latihan wajib sebelum failover: tim yang belum pernah melakukan switchover di jam tenang hampir pasti gagal melakukan failover di jam panik. Praktik umum production: switchover bulanan (bahkan mingguan) sebagai drill.
Keputusan: Manual atau Otomatis #
pg_ctl promote di demo kita dikerjakan tangan manusia yang tahu persis sedang memelihara dua server mainan. Di produksi, keputusan “primary sudah mati, saatnya promosi” jauh lebih sulit: primary yang lambat 30 detik bukan primary yang mati, dan promosi yang keliru terhadap primary yang sehat justru menciptakan split brain. Keputusan ini punya tiga pilihan penempatan, masing-masing dengan taruhannya: manual penuh (manusia menilai dan menekan tombol — aman dari alarm palsu, lambat di jam 3 pagi), otomatis penuh (perangkat lunak memutus lewat konsensus — cepat, tapi salah kaprah desainnya berbahaya), dan campuran (otomatis mendeteksi dan mengusulkan, manusia menyetujui). Sistem yang sehat biasanya menambahkan satu bahan lagi: quorum — keputusan failover hanya sah bila mayoritas node sepakat bahwa primary memang hilang, sehingga satu node yang salah paham tidak bisa menjatuhkan primary yang sehat.
Deteksi, fencing, promosi, routing, pemulihan, quorum — enam kebutuhan itu tidak diselesaikan pg_ctl promote sendirian. Merakitnya menjadi satu sistem yang berjalan sendiri adalah pekerjaan lapisan otomasi seperti Patroni, dan itulah topik penutup section ini: chapter Arsitektur High Availability
.
Mematikan Demo #
Skrip penutup mematikan kedua primary hasil split brain dan membersihkan direktori demo:
#!/usr/bin/env bash
# Menghentikan kedua server demo failover (dua primary hasil split brain)
# dan menghapus direktori datanya. Server utama course (socket 54329)
# tidak disentuh.
set -eu
PGBIN=/home/z/my-project/tools/dl/pgdeb/usr/lib/postgresql/17/bin
BASE=/home/z/my-project/tmp/rp_failover
"$PGBIN/pg_ctl" -D "$BASE/pgdata_cabang" -m fast stop >/dev/null 2>&1 || true
echo "primary baru berhenti"
"$PGBIN/pg_ctl" -D "$BASE/pgdata_pusat" -m fast stop >/dev/null 2>&1 || true
echo "primary lama berhenti"
rm -rf "$BASE"
echo "direktori data demo dihapus"Output yang Dihasilkan:
primary baru berhenti
primary lama berhenti
direktori data demo dihapusReferensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
bereskan-demo.sh
Unduh berkas#!/usr/bin/env bash
# Menghentikan kedua server demo failover (dua primary hasil split brain)
# dan menghapus direktori datanya. Server utama course (socket 54329)
# tidak disentuh.
set -eu
PGBIN=/home/z/my-project/tools/dl/pgdeb/usr/lib/postgresql/17/bin
BASE=/home/z/my-project/tmp/rp_failover
"$PGBIN/pg_ctl" -D "$BASE/pgdata_cabang" -m fast stop >/dev/null 2>&1 || true
echo "primary baru berhenti"
"$PGBIN/pg_ctl" -D "$BASE/pgdata_pusat" -m fast stop >/dev/null 2>&1 || true
echo "primary lama berhenti"
rm -rf "$BASE"
echo "direktori data demo dihapus"
divergensi.sh
Unduh berkas#!/usr/bin/env bash
# Membuktikan divergensi setelah failover: primary lama yang dinyalakan
# kembali TIDAK otomatis tahu bahwa dunia sudah berpindah timeline.
# Ia bangun sebagai primary timeline 1, menerima tulisan, dan kini ada
# dua primary dengan data berbeda — split brain.
# Skrip ini melanjutkan keadaan dari promosi.sh:
# primary lama (54365) mati, primary baru hasil promosi (54366) aktif.
set -eu
PGBIN=/home/z/my-project/tools/dl/pgdeb/usr/lib/postgresql/17/bin
BASE=/home/z/my-project/tmp/rp_failover
PRI=$BASE/pgdata_pusat # primary lama, port 54365
STD=$BASE/pgdata_cabang # primary baru (hasil promosi), port 54366
SOCK=$BASE/sock
# Polling: tunggu sampai sebuah query mengembalikan nilai yang diharapkan.
tunggu() { # port database sql nilai_yang_diharapkan
local port="$1" db="$2" sql="$3" harapan="$4" i hasil
for i in $(seq 1 150); do
hasil=$(psql -X -q -t -A -h 127.0.0.1 -p "$port" -U postgres -d "$db" \
-c "$sql" 2>/dev/null) || true
if [ "$hasil" = "$harapan" ]; then return 0; fi
sleep 0.2
done
echo "TIMEOUT: menunggu '$sql' menjadi $harapan (port $port)" >&2
return 1
}
echo "== Langkah 1: nyalakan kembali primary lama =="
"$PGBIN/pg_ctl" -D "$PRI" \
-o "-p 54365 -k $SOCK -c listen_addresses=127.0.0.1" \
-l "$BASE/primary-restart.log" start >/dev/null 2>&1
tunggu 54365 toko "SELECT pg_is_in_recovery();" "f"
echo "primary lama bangun sebagai server tulis penuh"
echo "== Langkah 2: timeline kedua server kini berbeda =="
psql -X -h 127.0.0.1 -p 54365 -U postgres -d toko \
-c "SELECT timeline_id FROM pg_control_checkpoint();" \
-c "SELECT pg_is_in_recovery() AS sedang_recovery;"
psql -X -h 127.0.0.1 -p 54366 -U postgres -d toko \
-c "SELECT timeline_id FROM pg_control_checkpoint();" \
-c "SELECT pg_is_in_recovery() AS sedang_recovery;"
echo "== Langkah 3: dua-duanya menerima tulisan — split brain =="
psql -X -h 127.0.0.1 -p 54365 -U postgres -d toko \
-c "INSERT INTO produk VALUES (6, 'Susu UHT', 17500);"
psql -X -h 127.0.0.1 -p 54366 -U postgres -d toko \
-c "INSERT INTO produk VALUES (7, 'Kopi Hitam', 13500);"
echo "== Langkah 4: isi tabel yang kini berbeda dua dunia =="
echo "primary LAMA (timeline 1):"
psql -X -h 127.0.0.1 -p 54365 -U postgres -d toko \
-c "SELECT id, nama, harga FROM produk ORDER BY id;"
echo "primary BARU hasil promosi (timeline 2):"
psql -X -h 127.0.0.1 -p 54366 -U postgres -d toko \
-c "SELECT id, nama, harga FROM produk ORDER BY id;"
promosi.sh
Unduh berkas#!/usr/bin/env bash
# Simulasi failover: primary mati mendadak, standby dipromosikan
# menjadi primary baru yang menerima tulisan.
# Skrip ini melanjutkan pasangan server dari siapkan-pasangan.sh:
# primary di port 54365, standby di port 54366, database toko.
# Setelah skrip ini: primary lama mati, standby sudah menjadi
# primary baru (timeline 2) dan tetap berjalan.
set -eu
PGBIN=/home/z/my-project/tools/dl/pgdeb/usr/lib/postgresql/17/bin
BASE=/home/z/my-project/tmp/rp_failover
PRI=$BASE/pgdata_pusat
STD=$BASE/pgdata_cabang
# Polling: tunggu sampai sebuah query mengembalikan nilai yang diharapkan.
tunggu() { # port database sql nilai_yang_diharapkan
local port="$1" db="$2" sql="$3" harapan="$4" i hasil
for i in $(seq 1 150); do
hasil=$(psql -X -q -t -A -h 127.0.0.1 -p "$port" -U postgres -d "$db" \
-c "$sql" 2>/dev/null) || true
if [ "$hasil" = "$harapan" ]; then return 0; fi
sleep 0.2
done
echo "TIMEOUT: menunggu '$sql' menjadi $harapan (port $port)" >&2
return 1
}
echo "== Langkah 1: tulisan terakhir yang sempat tereplikasi =="
psql -X -h 127.0.0.1 -p 54365 -U postgres -d toko \
-c "INSERT INTO produk VALUES (4, 'Susu Kental', 9000);"
tunggu 54366 toko "SELECT count(*) FROM produk;" "4"
echo "keempat baris sudah aman di standby"
echo "== Langkah 2: bencana — primary mati mendadak (immediate) =="
"$PGBIN/pg_ctl" -D "$PRI" -m immediate stop >/dev/null 2>&1
echo "primary mati"
echo "== Langkah 3: standby masih melayani pembacaan =="
psql -X -h 127.0.0.1 -p 54366 -U postgres -d toko \
-c "SELECT count(*) AS baris_tersedia FROM produk;"
echo "== Langkah 4: promosikan standby menjadi primary =="
"$PGBIN/pg_ctl" -D "$STD" -W promote >/dev/null 2>&1
tunggu 54366 toko "SELECT pg_is_in_recovery();" "f"
echo "promosi selesai"
echo "== Langkah 5: primary baru menerima tulisan =="
psql -X -h 127.0.0.1 -p 54366 -U postgres -d toko \
-c "INSERT INTO produk VALUES (5, 'Keju Slice', 21000);"
psql -X -h 127.0.0.1 -p 54366 -U postgres -d toko \
-c "SELECT id, nama, harga FROM produk ORDER BY id;"
echo "== Langkah 6: bukti timeline baru di primary hasil promosi =="
psql -X -h 127.0.0.1 -p 54366 -U postgres -d toko \
-c "CHECKPOINT;" \
-c "SELECT timeline_id FROM pg_control_checkpoint();"
echo "file riwayat timeline di pg_wal:"
ls "$STD/pg_wal" | grep history
siapkan-pasangan.sh
Unduh berkas#!/usr/bin/env bash
# Menyiapkan pasangan primary-standby untuk demo failover.
# Primary di port 54365, standby di port 54366 (database toko).
# Skrip promosi.sh dan divergensi.sh melanjutkan pasangan ini.
set -eu
PGBIN=/home/z/my-project/tools/dl/pgdeb/usr/lib/postgresql/17/bin
BASE=/home/z/my-project/tmp/rp_failover
PRI=$BASE/pgdata_pusat # primary, port 54365
STD=$BASE/pgdata_cabang # standby, port 54366
SOCK=$BASE/sock
# Buang sisa percobaan sebelumnya bila ada.
"$PGBIN/pg_ctl" -D "$PRI" -m immediate stop >/dev/null 2>&1 || true
"$PGBIN/pg_ctl" -D "$STD" -m immediate stop >/dev/null 2>&1 || true
rm -rf "$BASE"
mkdir -p "$SOCK"
# Polling: tunggu sampai sebuah query mengembalikan nilai yang diharapkan.
tunggu() { # port database sql nilai_yang_diharapkan
local port="$1" db="$2" sql="$3" harapan="$4" i hasil
for i in $(seq 1 150); do
hasil=$(psql -X -q -t -A -h 127.0.0.1 -p "$port" -U postgres -d "$db" \
-c "$sql" 2>/dev/null) || true
if [ "$hasil" = "$harapan" ]; then return 0; fi
sleep 0.2
done
echo "TIMEOUT: menunggu '$sql' menjadi $harapan (port $port)" >&2
return 1
}
echo "== Langkah 1: initdb dan jalankan primary di port 54365 =="
"$PGBIN/initdb" -D "$PRI" --no-locale -E UTF8 -U postgres >/dev/null 2>&1
"$PGBIN/pg_ctl" -D "$PRI" \
-o "-p 54365 -k $SOCK -c listen_addresses=127.0.0.1" \
-l "$BASE/primary.log" start >/dev/null 2>&1
tunggu 54365 postgres "SELECT 1;" "1"
echo "primary siap"
echo "== Langkah 2: data awal di primary =="
psql -X -h 127.0.0.1 -p 54365 -U postgres -d postgres \
-c "CREATE DATABASE toko;"
psql -X -h 127.0.0.1 -p 54365 -U postgres -d toko \
-c "CREATE TABLE produk (id int PRIMARY KEY, nama text, harga numeric);" \
-c "INSERT INTO produk VALUES (1, 'Kopi Tubruk', 15000), (2, 'Teh Celup', 8000);"
echo "== Langkah 3: pg_basebackup -R, lalu jalankan standby di port 54366 =="
PGDATABASE=postgres pg_basebackup -h 127.0.0.1 -p 54365 -U postgres \
-D "$STD" -R -X stream -c fast >/dev/null 2>&1
"$PGBIN/pg_ctl" -D "$STD" \
-o "-p 54366 -k $SOCK -c listen_addresses=127.0.0.1 -c cluster_name=standby_cabang" \
-l "$BASE/standby.log" start >/dev/null 2>&1
tunggu 54366 toko "SELECT count(*) FROM produk;" "2"
echo "standby menyala dan konsisten"
echo "== Langkah 4: satu tulisan terakhir sebelum bencana =="
psql -X -h 127.0.0.1 -p 54365 -U postgres -d toko \
-c "INSERT INTO produk VALUES (3, 'Gula Pasir', 12000);"
tunggu 54366 toko "SELECT count(*) FROM produk;" "3"
psql -X -h 127.0.0.1 -p 54366 -U postgres -d toko \
-c "SELECT pg_is_in_recovery() AS sedang_recovery;"