Chapter sebelumnya membahas apa dan mengapa; chapter ini membuat tangan Anda benar-benar kotor. Kita akan menjalankan dua server PostgreSQL utuh di satu mesin: satu primary yang menerima tulisan, dan satu standby yang terus me-replay WAL yang dikirim primary — bukan simulasi, melainkan streaming replication sungguhan yang sama dengan yang dipakai di produksi, hanya dialamatkan ke port lokal. Seluruh perjalanan — dari initdb dua cluster, pg_basebackup, sampai baris INSERT di primary yang terbaca di standby — berlangsung dalam waktu kurang dari lima detik, dan setiap skrip bisa Anda jalankan ulang kapan pun.

Arsitektur Demo: Dua Server di Satu Mesin #

Server utama course ini berjalan pada socket /home/z/my-project/pgrun port 54329 dan tidak akan disentuh sama sekali. Sebagai gantinya, setiap skrip chapter ini membuat dua cluster disposable di /home/z/my-project/tmp/rp_stream:

PeranPortDirektori dataKeterangan
Primary54341rp_stream/pgdata_pusatMenerima semua tulisan
Standby54342rp_stream/pgdata_cabangRead-only, me-replay WAL dari primary

Nomor port tidak istimewa — yang penting keduanya bebas dan berbeda. Skrip pertama berikut menyiapkan semuanya dari nol; ia juga sengaja berhenti tanpa mematikan kedua server, karena tiga skrip sesudahnya melanjutkan pasangan yang sama:

bash siapkan-demo.sh
#!/usr/bin/env bash
# Menyiapkan pasangan primary-standby untuk demo streaming replication.
# Dua server mini ini terpisah dari server utama course (socket 54329):
# primary demo berjalan di port 54341, standby di port 54342.
# Skrip berikutnya (aliran-data.sh) memakai pasangan yang ditinggalkan skrip ini.
set -eu

PGBIN=/home/z/my-project/tools/dl/pgdeb/usr/lib/postgresql/17/bin
BASE=/home/z/my-project/tmp/rp_stream
PRI=$BASE/pgdata_pusat     # primary, port 54341
STD=$BASE/pgdata_cabang    # standby,  port 54342
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 80); 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 cluster primary baru =="
"$PGBIN/initdb" -D "$PRI" --no-locale -E UTF8 -U postgres >/dev/null
echo "cluster primary siap"

echo "== Langkah 2: jalankan primary di port 54341 =="
"$PGBIN/pg_ctl" -D "$PRI" \
  -o "-p 54341 -k $SOCK -c listen_addresses=127.0.0.1 -c wal_level=replica -c max_wal_senders=10" \
  -l "$BASE/primary.log" start >/dev/null 2>&1
tunggu 54341 postgres "SELECT 1;" "1"
echo "primary menerima koneksi"

echo "== Langkah 3: buat database dan data awal di primary =="
psql -X -h 127.0.0.1 -p 54341 -U postgres -d postgres \
  -c "CREATE DATABASE toko;"
psql -X -h 127.0.0.1 -p 54341 -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 4: pg_basebackup -R menyalin primary menjadi standby =="
PGDATABASE=postgres pg_basebackup -h 127.0.0.1 -p 54341 -U postgres \
  -D "$STD" -R -X stream -c fast >/dev/null 2>&1
echo "base backup selesai"

echo "== Langkah 5: file yang ditulis otomatis oleh -R =="
cat "$STD/postgresql.auto.conf"
[ -f "$STD/standby.signal" ] && echo "standby.signal ada"

echo "== Langkah 6: jalankan standby di port 54342 =="
"$PGBIN/pg_ctl" -D "$STD" \
  -o "-p 54342 -k $SOCK -c listen_addresses=127.0.0.1 -c cluster_name=standby_cabang" \
  -l "$BASE/standby.log" start >/dev/null 2>&1
tunggu 54342 toko "SELECT count(*) FROM produk;" "2"
echo "standby menyala dan sudah konsisten"

echo "== Langkah 7: data awal terbaca dari standby =="
psql -X -h 127.0.0.1 -p 54342 -U postgres -d toko \
  -c "SELECT id, nama, harga FROM produk ORDER BY id;"

Output yang Dihasilkan:

text
== Langkah 1: initdb cluster primary baru ==
cluster primary siap
== Langkah 2: jalankan primary di port 54341 ==
primary menerima koneksi
== Langkah 3: buat database dan data awal di primary ==
CREATE DATABASE
CREATE TABLE
INSERT 0 2
== Langkah 4: pg_basebackup -R menyalin primary menjadi standby ==
base backup selesai
== Langkah 5: file yang ditulis otomatis oleh -R ==
# Do not edit this file manually!
# It will be overwritten by the ALTER SYSTEM command.
primary_conninfo = 'user=postgres passfile=''/home/z/.pgpass'' channel_binding=prefer host=127.0.0.1 port=54341 sslmode=prefer sslnegotiation=postgres sslcompression=0 sslcertmode=allow sslsni=1 ssl_min_protocol_version=TLSv1.2 gssencmode=prefer krbsrvname=postgres gssdelegation=0 target_session_attrs=any load_balance_hosts=disable dbname=postgres'
standby.signal ada
== Langkah 6: jalankan standby di port 54342 ==
standby menyala dan sudah konsisten
== Langkah 7: data awal terbaca dari standby ==
 id |    nama     | harga 
----+-------------+-------
  1 | Kopi Tubruk | 15000
  2 | Teh Celup   |  8000
(2 rows)

Mari kita bedah langkah-langkah kuncinya:

  1. Blok bersih-bersih dan tunggu(): skrip dimulai dengan menghentikan sisa server dari percobaan sebelumnya lalu menghapus direktorinya — inilah yang membuat skrip aman dijalankan berulang kali. Function tunggu() melakukan polling: query diulang tiap 0,2 detik sampai hasilnya sama dengan harapan. Polling semacam ini wajib dalam dunia replikasi karena “sudah tiba di standby” adalah peristiwa asinkron.
  2. initdb + pg_ctl start dengan -o: cluster primary dibuat lalu dijalankan dengan tiga opsi yang relevan untuk replikasi — wal_level=replica agar WAL membawa data replay, max_wal_senders=10 agar walsender bisa hidup, dan listen_addresses=127.0.0.1 agar primary mau menerima koneksi walreceiver lewat TCP.
  3. pg_basebackup -R: satu perintah ini melakukan tiga hal — menyalin seluruh file data primary (base backup, dibahas di chapter Base Backup ), mengambil WAL yang dibutuhkan lewat opsi -X stream, dan karena -R, menuliskan file standby.signal plus baris primary_conninfo ke postgresql.auto.conf milik salinan. Output Langkah 5 memperlihatkan hasilnya: baris primary_conninfo berisi host, port, dan user yang dipakai walreceiver untuk pulang ke primary, sementara keberadaan standby.signal adalah penanda “server ini standby” di PostgreSQL 12 ke atas.
  4. cluster_name=standby_cabang saat standby menyala: nama cluster ini ikut terkirim sebagai application_name koneksi walreceiver — nanti terlihat di pg_stat_replication dan memudahkan Anda mengenali standby mana yang tersambung.
  5. tunggu 54342 ... "2": standby baru dinyatakan siap setelah dua baris data awal benar-benar bisa dibaca. Dari titik ini, pasangan server hidup dan saling tersambung.

Menyaksikan Data Mengalir #

Base backup hanya menyalin keadaan awal. Kekuatan sesungguhnya streaming replication adalah perubahan baru terkirim begitu tercipta. Skrip berikut menulis satu baris di primary, membaca ulang dari standby, lalu mengintip sisi primary untuk melihat koneksi walsender-nya:

bash aliran-data.sh
#!/usr/bin/env bash
# Membuktikan data baru mengalir dari primary ke standby secara streaming.
# Skrip ini melanjutkan pasangan server dari siapkan-demo.sh:
# primary di port 54341, standby di port 54342, database toko.
set -eu

# 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 80); 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 "== Tulis satu baris baru di primary =="
psql -X -h 127.0.0.1 -p 54341 -U postgres -d toko \
  -c "INSERT INTO produk VALUES (3, 'Gula Pasir', 12000);"

echo "== Baca dari standby: baris baru sudah tiba =="
tunggu 54342 toko "SELECT count(*) FROM produk;" "3"
psql -X -h 127.0.0.1 -p 54342 -U postgres -d toko \
  -c "SELECT id, nama, harga FROM produk ORDER BY id;"

echo "== Koneksi walsender yang terlihat dari primary =="
tunggu 54341 postgres \
  "SELECT state FROM pg_stat_replication WHERE application_name = 'standby_cabang';" \
  "streaming"
psql -X -h 127.0.0.1 -p 54341 -U postgres -d postgres \
  -c "SELECT application_name, client_addr, state, sync_state FROM pg_stat_replication;"

Output yang Dihasilkan:

text
== Tulis satu baris baru di primary ==
INSERT 0 1
== Baca dari standby: baris baru sudah tiba ==
 id |    nama     | harga 
----+-------------+-------
  1 | Kopi Tubruk | 15000
  2 | Teh Celup   |  8000
  3 | Gula Pasir  | 12000
(3 rows)

== Koneksi walsender yang terlihat dari primary ==
 application_name | client_addr |   state   | sync_state 
------------------+-------------+-----------+------------
 standby_cabang   | 127.0.0.1   | streaming | async
(1 row)

Tiga bukti dalam output itu. Pertama, INSERT 0 1 di primary diikuti tabel berisi tiga baris di standby — Gula Pasir menyeberang tanpa perintah salin-menyalin apa pun dari Anda. Kedua, baris pg_stat_replication di primary memperlihatkan persis apa yang diketahui primary tentang standby-nya: application_name berasal dari cluster_name yang kita pasang, client_addr adalah alamat walreceiver, state = streaming berarti pengiriman WAL berlangsung kontinu, dan sync_state = async menegaskan mode bawaan — primary tidak menunggu standby untuk mengembalikan COMMIT. Perhatikan juga bahwa skrip tetap memakai tunggu sebelum membaca dari standby: pada mesin ini perpindahan terjadi dalam hitungan milidetik, tetapi “milidetik” tetap bukan “nol” — jeda inilah topik chapter Replication Lag .

Standby Hanya Bisa Membaca #

Standby menjalankan recovery terus-menerus, sehingga server menolak semua perintah yang mengubah data — bukan karena aturan administrasi, melainkan karena menulis akan merusak konsistensi salinan. Skrip berikut membuktikannya dengan mencoba INSERT langsung ke standby:

#!/usr/bin/env bash
# Standby adalah server read-only: INSERT di sana harus ditolak server.
# Skrip ini sengaja berakhir dengan exit code 3 (galat SQL dari psql),
# memakai pasangan server dari siapkan-demo.sh (standby di port 54342).
exec psql -X -v ON_ERROR_STOP=1 -h 127.0.0.1 -p 54342 -U postgres -d toko <<'SQL'
INSERT INTO produk VALUES (4, 'Susu Kental', 9000);
SQL

Output yang Dihasilkan:

text
ERROR:  cannot execute INSERT in a read-only transaction

Pesan cannot execute INSERT in a read-only transaction adalah jawaban server yang deterministik. Satu-satunya cara standby menerima tulisan adalah berhenti menjadi standby — lewat promosi yang dibahas di chapter Konsep Failover .

Mematikan Demo #

Keempat skrip terakhir menghentikan kedua server secara sopan (stanby lebih dulu, lalu primary) dan menghapus direktori data demo, sehingga mesin kembali bersih dan server utama course tidak pernah terpengaruh:

bash hentikan-demo.sh
#!/usr/bin/env bash
# Menghentikan kedua server demo streaming replication 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_stream

"$PGBIN/pg_ctl" -D "$BASE/pgdata_cabang" -m fast stop >/dev/null 2>&1 || true
echo "standby berhenti"
"$PGBIN/pg_ctl" -D "$BASE/pgdata_pusat" -m fast stop >/dev/null 2>&1 || true
echo "primary berhenti"
rm -rf "$BASE"
echo "direktori data demo dihapus"

Output yang Dihasilkan:

text
standby berhenti
primary berhenti
direktori data demo dihapus

Dengan pasangan primary-standby yang sudah pernah Anda bangun dan saksikan hidup, pertanyaan berikutnya tinggal memilih model: replikasi fisik seperti barusan, atau replikasi logis per tabel — perbandingan itu adalah pembuka chapter Physical vs Logical Replication .

Referensi #

Berkas Sumber

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

aliran-data.sh

Unduh berkas
bash
#!/usr/bin/env bash
# Membuktikan data baru mengalir dari primary ke standby secara streaming.
# Skrip ini melanjutkan pasangan server dari siapkan-demo.sh:
# primary di port 54341, standby di port 54342, database toko.
set -eu

# 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 80); 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 "== Tulis satu baris baru di primary =="
psql -X -h 127.0.0.1 -p 54341 -U postgres -d toko \
  -c "INSERT INTO produk VALUES (3, 'Gula Pasir', 12000);"

echo "== Baca dari standby: baris baru sudah tiba =="
tunggu 54342 toko "SELECT count(*) FROM produk;" "3"
psql -X -h 127.0.0.1 -p 54342 -U postgres -d toko \
  -c "SELECT id, nama, harga FROM produk ORDER BY id;"

echo "== Koneksi walsender yang terlihat dari primary =="
tunggu 54341 postgres \
  "SELECT state FROM pg_stat_replication WHERE application_name = 'standby_cabang';" \
  "streaming"
psql -X -h 127.0.0.1 -p 54341 -U postgres -d postgres \
  -c "SELECT application_name, client_addr, state, sync_state FROM pg_stat_replication;"

hentikan-demo.sh

Unduh berkas
bash
#!/usr/bin/env bash
# Menghentikan kedua server demo streaming replication 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_stream

"$PGBIN/pg_ctl" -D "$BASE/pgdata_cabang" -m fast stop >/dev/null 2>&1 || true
echo "standby berhenti"
"$PGBIN/pg_ctl" -D "$BASE/pgdata_pusat" -m fast stop >/dev/null 2>&1 || true
echo "primary berhenti"
rm -rf "$BASE"
echo "direktori data demo dihapus"

siapkan-demo.sh

Unduh berkas
bash
#!/usr/bin/env bash
# Menyiapkan pasangan primary-standby untuk demo streaming replication.
# Dua server mini ini terpisah dari server utama course (socket 54329):
# primary demo berjalan di port 54341, standby di port 54342.
# Skrip berikutnya (aliran-data.sh) memakai pasangan yang ditinggalkan skrip ini.
set -eu

PGBIN=/home/z/my-project/tools/dl/pgdeb/usr/lib/postgresql/17/bin
BASE=/home/z/my-project/tmp/rp_stream
PRI=$BASE/pgdata_pusat     # primary, port 54341
STD=$BASE/pgdata_cabang    # standby,  port 54342
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 80); 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 cluster primary baru =="
"$PGBIN/initdb" -D "$PRI" --no-locale -E UTF8 -U postgres >/dev/null
echo "cluster primary siap"

echo "== Langkah 2: jalankan primary di port 54341 =="
"$PGBIN/pg_ctl" -D "$PRI" \
  -o "-p 54341 -k $SOCK -c listen_addresses=127.0.0.1 -c wal_level=replica -c max_wal_senders=10" \
  -l "$BASE/primary.log" start >/dev/null 2>&1
tunggu 54341 postgres "SELECT 1;" "1"
echo "primary menerima koneksi"

echo "== Langkah 3: buat database dan data awal di primary =="
psql -X -h 127.0.0.1 -p 54341 -U postgres -d postgres \
  -c "CREATE DATABASE toko;"
psql -X -h 127.0.0.1 -p 54341 -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 4: pg_basebackup -R menyalin primary menjadi standby =="
PGDATABASE=postgres pg_basebackup -h 127.0.0.1 -p 54341 -U postgres \
  -D "$STD" -R -X stream -c fast >/dev/null 2>&1
echo "base backup selesai"

echo "== Langkah 5: file yang ditulis otomatis oleh -R =="
cat "$STD/postgresql.auto.conf"
[ -f "$STD/standby.signal" ] && echo "standby.signal ada"

echo "== Langkah 6: jalankan standby di port 54342 =="
"$PGBIN/pg_ctl" -D "$STD" \
  -o "-p 54342 -k $SOCK -c listen_addresses=127.0.0.1 -c cluster_name=standby_cabang" \
  -l "$BASE/standby.log" start >/dev/null 2>&1
tunggu 54342 toko "SELECT count(*) FROM produk;" "2"
echo "standby menyala dan sudah konsisten"

echo "== Langkah 7: data awal terbaca dari standby =="
psql -X -h 127.0.0.1 -p 54342 -U postgres -d toko \
  -c "SELECT id, nama, harga FROM produk ORDER BY id;"

tulis-di-standby.sh

Unduh berkas
bash
#!/usr/bin/env bash
# Standby adalah server read-only: INSERT di sana harus ditolak server.
# Skrip ini sengaja berakhir dengan exit code 3 (galat SQL dari psql),
# memakai pasangan server dari siapkan-demo.sh (standby di port 54342).
exec psql -X -v ON_ERROR_STOP=1 -h 127.0.0.1 -p 54342 -U postgres -d toko <<'SQL'
INSERT INTO produk VALUES (4, 'Susu Kental', 9000);
SQL