Chapter Konsep Failover ditutup dengan pengakuan yang jujur: pg_ctl promote hanyalah satu dari lima langkah failover, dan lima langkah itu tidak mungkin dikerjakan manusia dalam hitungan detik pada jam 3 pagi. High availability (HA) adalah penghapusan manusia dari jalur kritis itu: perangkat lunak yang mendeteksi kegagalan, memutuskan lewat konsensus, memberi pagar (fencing), mempromosikan standby, dan mengalihkan koneksi aplikasi — semuanya otomatis. Chapter penutup section ini menyusun arsitektur itu dari fondasinya: replikasi sinkron agar failover tidak kehilangan transaksi, lalu Patroni sebagai lapisan otomasi di atas penyimpanan konsensus, ditutup peta alat dan perbedaan HA dengan disaster recovery.

Mengapa Promosi Manual Tidak Cukup #

Failover manual yang kita jalankan di chapter sebelumnya melibatkan urutan yang tampak sederhana tetapi masing-masing langkahnya punya jebakan: mendeteksi bahwa primary memang mati (bukan sekadar lambat), memfencing bekas primary agar tidak bisa menulis lagi, mempromosikan standby, mengarahkan ulang koneksi aplikasi, dan memulihkan bekas primary sebagai standby baru. Manusia bisa melakukan semuanya — dalam lima sampai tiga puluh menit, dengan risiko salah tekan pada kondisi lelah. Selama itu pula aplikasi Anda berjalan tanpa kemampuan menulis. Ukuran lamanya pemulihan ini disebut RTO (recovery time objective), dan HA menuntutnya dalam hitungan detik.

Tabel berikut memetakan kelima tanggung jawab itu beserta pembawa kerjanya di arsitektur manual versus otomatis:

Tanggung jawabManual (pg_ctl + tangan)Otomatis (arsitektur HA)
Deteksi kegagalanManusia memeriksa alarmAgent memantau heartbeat tiap detik
Keputusan failoverPenilaian manusiaKonsensus mayoritas node (quorum)
Fencing bekas primaryMematikan server manualDitandai tidak sah menulis sebelum di-rewind
Promosi standbypg_ctl promotepg_ctl promote yang dipicu agent
Routing aplikasiEdit connection string / restartHealth check mengalihkan endpoint

Perhatikan ironi pada baris promosi: perintahnya sama persis dengan yang sudah Anda kuasai. Yang dibeli lapisan otomasi bukan perintah baru, melainkan kecepatan dan keselamatan pengambilan keputusan di sekelilingnya.

Fondasi Data: Replikasi Sinkron dengan Quorum #

Otomasi tanpa fondasi data yang tepat hanya mempercepat kehilangan data. Replikasi asinkron yang dipakai sepanjang section ini berpotensi kehilangan transaksi terakhir saat failover — WAL yang belum sempat terkirim lenyap bersama primary. Bila janji layanan Anda menuntut zero data loss (RPO = 0), fondasinya adalah synchronous replication: commit di primary menunggu sampai setidaknya satu standby mengonfirmasi telah men-flush WAL-nya ke disk, baru COMMIT dikembalikan ke klien.

PostgreSQL mengendalikannya lewat synchronous_standby_names. Bentuk ANY 1 (standby_cabang) berarti quorum commit: commit menunggu konfirmasi flush dari satu standby mana pun dalam daftar — dengan tiga standby dan ANY 2 (...), dua konfirmasi diwajibkan sekaligus tetap toleran terhadap satu standby yang gagal. Skrip berikut menghidupkan mode itu pada pasangan server demo (primary 54371, standby 54372) dan memperlihatkan peralihan statusnya di pg_stat_replication:

#!/usr/bin/env bash
# Replikasi sinkron dengan quorum: synchronous_standby_names 'ANY 1 (...)'.
# Primary port 54371, standby port 54372, database toko.
# Skrip ini mandiri: membersihkan dirinya sendiri di akhir.
set -eu

PGBIN=/home/z/my-project/tools/dl/pgdeb/usr/lib/postgresql/17/bin
BASE=/home/z/my-project/tmp/rp_ha
PRI=$BASE/pgdata_pusat     # primary, port 54371
STD=$BASE/pgdata_cabang    # standby,  port 54372
SOCK=$BASE/sock

bersihkan() {
  "$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"
}
trap bersihkan EXIT

# Buang sisa percobaan sebelumnya bila ada.
bersihkan
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: siapkan pasangan primary (54371) dan standby (54372) =="
"$PGBIN/initdb" -D "$PRI" --no-locale -E UTF8 -U postgres >/dev/null 2>&1
"$PGBIN/pg_ctl" -D "$PRI" \
  -o "-p 54371 -k $SOCK -c listen_addresses=127.0.0.1" \
  -l "$BASE/primary.log" start >/dev/null 2>&1
tunggu 54371 postgres "SELECT 1;" "1"
psql -X -h 127.0.0.1 -p 54371 -U postgres -d postgres \
  -c "CREATE DATABASE toko;"
psql -X -h 127.0.0.1 -p 54371 -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);"
PGDATABASE=postgres pg_basebackup -h 127.0.0.1 -p 54371 -U postgres \
  -D "$STD" -R -X stream -c fast >/dev/null 2>&1
"$PGBIN/pg_ctl" -D "$STD" \
  -o "-p 54372 -k $SOCK -c listen_addresses=127.0.0.1 -c cluster_name=standby_cabang" \
  -l "$BASE/standby.log" start >/dev/null 2>&1
tunggu 54372 toko "SELECT count(*) FROM produk;" "2"
echo "pasangan siap dalam mode bawaan (asinkron)"

echo "== Langkah 2: keadaan awal — belum ada standby sinkron =="
psql -X -h 127.0.0.1 -p 54371 -U postgres -d toko \
  -c "SHOW synchronous_standby_names;" \
  -c "SELECT application_name, state, sync_state FROM pg_stat_replication;"

echo "== Langkah 3: nyalakan mode sinkron quorum =="
psql -X -h 127.0.0.1 -p 54371 -U postgres -d toko \
  -c "ALTER SYSTEM SET synchronous_standby_names = 'ANY 1 (standby_cabang)';" \
  -c "SELECT pg_reload_conf();"
tunggu 54371 toko \
  "SELECT sync_state FROM pg_stat_replication WHERE application_name = 'standby_cabang';" \
  "quorum"
echo "standby kini berstatus quorum"

echo "== Langkah 4: keadaan sesudah — commit menunggu flush standby =="
psql -X -h 127.0.0.1 -p 54371 -U postgres -d toko \
  -c "SHOW synchronous_standby_names;" \
  -c "SHOW synchronous_commit;" \
  -c "SELECT application_name, state, sync_state FROM pg_stat_replication;"

echo "== Langkah 5: tulisan tetap mengalir, kini dengan jaminan quorum =="
psql -X -h 127.0.0.1 -p 54371 -U postgres -d toko \
  -c "INSERT INTO produk VALUES (3, 'Gula Pasir', 12000);"
tunggu 54372 toko "SELECT count(*) FROM produk;" "3"
psql -X -h 127.0.0.1 -p 54372 -U postgres -d toko \
  -c "SELECT id, nama, harga FROM produk ORDER BY id;"

Output yang Dihasilkan:

text
== Langkah 1: siapkan pasangan primary (54371) dan standby (54372) ==
CREATE DATABASE
CREATE TABLE
INSERT 0 2
pasangan siap dalam mode bawaan (asinkron)
== Langkah 2: keadaan awal — belum ada standby sinkron ==
 synchronous_standby_names 
---------------------------
 
(1 row)

 application_name |   state   | sync_state 
------------------+-----------+------------
 standby_cabang   | streaming | async
(1 row)

== Langkah 3: nyalakan mode sinkron quorum ==
ALTER SYSTEM
 pg_reload_conf 
----------------
 t
(1 row)

standby kini berstatus quorum
== Langkah 4: keadaan sesudah — commit menunggu flush standby ==
 synchronous_standby_names 
---------------------------
 ANY 1 (standby_cabang)
(1 row)

 synchronous_commit 
--------------------
 on
(1 row)

 application_name |   state   | sync_state 
------------------+-----------+------------
 standby_cabang   | streaming | quorum
(1 row)

== Langkah 5: tulisan tetap mengalir, kini dengan jaminan quorum ==
INSERT 0 1
 id |    nama     | harga 
----+-------------+-------
  1 | Kopi Tubruk | 15000
  2 | Teh Celup   |  8000
  3 | Gula Pasir  | 12000
(3 rows)

Tiga bukti dari output itu:

  1. Langkah 2 — status awal: synchronous_standby_names kosong dan sync_state = async — inilah mode bawaan yang dipakai semua demo section ini; primary tidak menunggu siapa-siapa saat commit.
  2. Langkah 3 dan 4 — peralihan tanpa restart: ALTER SYSTEM SET plus pg_reload_conf() mengubah status koneksi yang sama menjadi sync_state = quorum dalam hitungan detik. Parameter ini berlaku tanpa memulai ulang server, sehingga bisa dinaikkan/diturunkan menurut jam layanan.
  3. Langkah 5 — durabilitas berganda: sejak status quorum, INSERT hanya mendapat COMMIT setelah WAL-nya ter-flush di primary dan dikonfirmasi ter-flush di standby. Failover sesaat setelah commit itu dijamin tidak kehilangan barisnya — inilah RPO nol dalam praktik.

Patroni: Otomasi di Atas Konsensus #

Patroni adalah daemon yang berjalan di setiap node PostgreSQL dan menjadwalkan seluruh siklus hidup cluster lewat sebuah distributed configuration store (DCS) — etcd, Consul, ZooKeeper, atau API Kubernetes. Ide intinya satu: kepemilikan peran “leader” (primary) diambil alih dari konvensi file konfigurasi menjadi lock di DCS — objek dengan TTL yang harus terus diperbarui oleh primary yang sehat. Seluruh keputusan failover mengalir dari properti sederhana itu.

Failover otomatis Patroni berjalan kurang lebih begini:

  1. Setiap node Patroni memperbarui statusnya (kesehatan PostgreSQL, posisi replikasi) ke DCS tiap beberapa detik, sekaligus memperbarui TTL lock bila node itu leader.
  2. Primary macet total — lock-nya tidak diperbarui dan TTL-nya kedaluwarsa di DCS.
  3. Patroni di node standby mendeteksi kedaluwarsa, dan yang berhasil mengambil lock lebih dulu (lewat DCS, yang menjamin satu pemenang) menjadi calon leader baru.
  4. Calon itu menjalankan pg_ctl promote — perintah yang sudah Anda kuasai — atas standby-nya.
  5. Bekas primary yang bangun menemukan dirinya kehilangan lock; Patroni menurunkannya menjadi standby (memakai pg_rewind bila perlu) alih-alih membiarkannya menulis — fencing secara peran, tanpa perlu manusia memadamkan mesin.
  6. HAProxy di depan cluster memeriksa endpoint REST Patroni tiap node (/primary, /replica, /health); begitu kepemilikan berpindah, endpoint ikut berpindah dan trafik menulis dialihkan dalam satu-dua detik.
  7. Aplikasi tidak pernah tahu alamat primary yang sebenarnya — aplikasi hanya mengenal alamat HAProxy yang stabil.

Diagram itu memetakan pembagian kerja yang tegas: HAProxy hanya melihat hasil keputusan (endpoint sehat atau tidak), Patroni hanya melihat DCS dan server lokalnya, dan PostgreSQL hanya melihat Patroni yang memegang kendalinya. Tidak ada satu titik yang bisa berbohong dua kali — kecuali DCS itu sendiri, dan karena itu etcd selalu ditanam sebagai klaster tiga node, bukan satu.

Patroni tidak terpasang di lingkungan uji course ini, sehingga tidak ada blok output yang mengklaim hasil eksekusinya. Sebagai gambaran bentuknya, berikut potongan konfigurasi patroni.yml yang lazim untuk pasangan seperti demo kita — sketsa dokumentasi, bukan file yang dijalankan:

yaml
# Sketsa konfigurasi Patroni (tidak dieksekusi di lingkungan course).
namespace: toko_maju_jaya
scope: klaster_prod
restapi:
  listen: 0.0.0.0:8008
etcd3:
  hosts: etcd1.example.com:2379,etcd2.example.com:2379,etcd3.example.com:2379
bootstrap:
  dcs:
    postgresql:
      parameters:
        wal_level: replica
        max_wal_senders: 10
      # Mode sinkron dinamis: 1 konfirmasi dari standby mana pun.
      synchronous_mode: true
      synchronous_mode_strict: false
  initdb:
    - encoding: UTF8
postgresql:
  data_dir: /var/lib/postgresql/17/main
  pg_hba:
    - host replication replicator 10.0.0.0/24 scram-sha-256
  authentication:
    superuser:
      username: postgres
      password: YOUR_PASSWORD
    replication:
      username: replicator
      password: YOUR_PASSWORD

Dua baris layak diperhatikan: synchronous_mode: true adalah janji yang kita buktikan sendiri di skrip sebelumnya — Patroni mengelola synchronous_standby_names secara dinamis, menaikkan mode sinkron saat standby cukup dan mundur saat quorum hilang (persis penawaran masalah di WARNING di atas). pg_hba di dalam konfigurasi menegaskan pula bahwa Patroni yang menulis aturan akses replikasi — satu sumber kebenaran untuk seluruh cluster.

Peta Alat Otomasi #

Patroni bukan satu-satunya jawaban; tiga keluarga alat mendominasi lanskap HA PostgreSQL:

AlatPendekatanKekuatan khas
PatroniAgent per node + DCS (etcd/Consul/Kubernetes)Standar de-facto industri; failover teruji, sinkron dinamis, REST API untuk routing
repmgrToolkit CLI + daemon monitorPaling lama; ringan dan sederhana untuk tim yang baru pindah dari manual
pg_auto_failoverMonitor pusat + satu node guardianArsitektur “satu pengawas” yang mudah dipahami; pengembangan aktif berlanjut di ekosistem Microsoft

Untuk lingkungan Kubernetes, jawabannya biasanya operator seperti CloudNativePG: bukan sekadar alat failover, melainkan pengelola seluruh siklus hidup — provision instance, basebackup standby, rotasi sertifikat, failover, hingga upgrade minor — dinyatakan sebagai resource Kubernetes. Pilihan antara keempatnya lebih sedikit soal fitur dan lebih banyak soal bentuk tim dan platform tempat Anda berdiri.

HA versus DR: Dua Masalah Berbeda #

Arsitektur HA yang kita susun membetulkan satu bangunan. Disaster recovery (DR) menjawab pertanyaan yang lebih kasar: bagaimana bila bangunan itu sendiri hilang? Keduanya kerap memakai teknologi yang sama — streaming replication — tetapi rancangannya berbeda karena jarak dan domain kegagalannya berbeda:

LapisanContoh domain kegagalanPerlindungan khas
HA dalam satu siteServer mati, disk rusakFailover otomatis detik; replikasi sinkron, RPO 0
DR antar site satu kotaRuang server kebakaran, listrik gedung padamStandby jauh, asinkron; RTO menit-jam
DR antar regionBencana alam regionalSalinan lintas region; prosedur pemulihan terdokumentasi dan teruji

Perhatikan keputusan yang tampak aneh di mata pemula: mengapa lapisan DR memakai replikasi asinkron, bukan sinkron? Karena jarak. Sinkron menuntut standby merespons sebelum commit kembali — pada jarak 40 km itu puluhan milidetik tiap commit (menggandakan latensi tulisan), pada jarak antar benua itu ratusan milidetik yang membuat aplikasi tak terpakai. Praktik lazimnya berlapis: replikasi sinkron ke standby satu gedung demi RPO nol, plus replikasi asinkron ke site DR demi bertahan dari kebakaran — dua janji berbeda dengan dua mekanisme berbeda. Rancangan serupa juga berlaku untuk failure domain yang lebih kecil: standby sebaiknya tidak berbagi rak daya, switch, atau pendingin dengan primary-nya, sebab domain kegagalan yang sama menggagalkan redundancy yang mahal.

Dari satu server di chapter pertama section ini, Anda kini memegang peta utuh: data diduplikasi lewat WAL (fisik maupun logis), dijaga oleh slot, diukur lag-nya, dimanfaatkan untuk baca, dipromosikan saat bencana, dan diotomasi menjadi arsitektur yang menyembuhkan diri. Ketahanan hanyalah satu sisi skala; sisi lainnya menunggu di section berikutnya — ketika data yang sehat itu tumbuh terlalu besar untuk satu tabel, dan PostgreSQL menjawabnya dengan Partitioning dan Scalability .

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

replikasi-sinkron.sh

Unduh berkas
bash
#!/usr/bin/env bash
# Replikasi sinkron dengan quorum: synchronous_standby_names 'ANY 1 (...)'.
# Primary port 54371, standby port 54372, database toko.
# Skrip ini mandiri: membersihkan dirinya sendiri di akhir.
set -eu

PGBIN=/home/z/my-project/tools/dl/pgdeb/usr/lib/postgresql/17/bin
BASE=/home/z/my-project/tmp/rp_ha
PRI=$BASE/pgdata_pusat     # primary, port 54371
STD=$BASE/pgdata_cabang    # standby,  port 54372
SOCK=$BASE/sock

bersihkan() {
  "$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"
}
trap bersihkan EXIT

# Buang sisa percobaan sebelumnya bila ada.
bersihkan
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: siapkan pasangan primary (54371) dan standby (54372) =="
"$PGBIN/initdb" -D "$PRI" --no-locale -E UTF8 -U postgres >/dev/null 2>&1
"$PGBIN/pg_ctl" -D "$PRI" \
  -o "-p 54371 -k $SOCK -c listen_addresses=127.0.0.1" \
  -l "$BASE/primary.log" start >/dev/null 2>&1
tunggu 54371 postgres "SELECT 1;" "1"
psql -X -h 127.0.0.1 -p 54371 -U postgres -d postgres \
  -c "CREATE DATABASE toko;"
psql -X -h 127.0.0.1 -p 54371 -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);"
PGDATABASE=postgres pg_basebackup -h 127.0.0.1 -p 54371 -U postgres \
  -D "$STD" -R -X stream -c fast >/dev/null 2>&1
"$PGBIN/pg_ctl" -D "$STD" \
  -o "-p 54372 -k $SOCK -c listen_addresses=127.0.0.1 -c cluster_name=standby_cabang" \
  -l "$BASE/standby.log" start >/dev/null 2>&1
tunggu 54372 toko "SELECT count(*) FROM produk;" "2"
echo "pasangan siap dalam mode bawaan (asinkron)"

echo "== Langkah 2: keadaan awal — belum ada standby sinkron =="
psql -X -h 127.0.0.1 -p 54371 -U postgres -d toko \
  -c "SHOW synchronous_standby_names;" \
  -c "SELECT application_name, state, sync_state FROM pg_stat_replication;"

echo "== Langkah 3: nyalakan mode sinkron quorum =="
psql -X -h 127.0.0.1 -p 54371 -U postgres -d toko \
  -c "ALTER SYSTEM SET synchronous_standby_names = 'ANY 1 (standby_cabang)';" \
  -c "SELECT pg_reload_conf();"
tunggu 54371 toko \
  "SELECT sync_state FROM pg_stat_replication WHERE application_name = 'standby_cabang';" \
  "quorum"
echo "standby kini berstatus quorum"

echo "== Langkah 4: keadaan sesudah — commit menunggu flush standby =="
psql -X -h 127.0.0.1 -p 54371 -U postgres -d toko \
  -c "SHOW synchronous_standby_names;" \
  -c "SHOW synchronous_commit;" \
  -c "SELECT application_name, state, sync_state FROM pg_stat_replication;"

echo "== Langkah 5: tulisan tetap mengalir, kini dengan jaminan quorum =="
psql -X -h 127.0.0.1 -p 54371 -U postgres -d toko \
  -c "INSERT INTO produk VALUES (3, 'Gula Pasir', 12000);"
tunggu 54372 toko "SELECT count(*) FROM produk;" "3"
psql -X -h 127.0.0.1 -p 54372 -U postgres -d toko \
  -c "SELECT id, nama, harga FROM produk ORDER BY id;"