Bayangkan standby Toko Maju Jaya mati pada Jumat malam karena maintenance jaringan yang molor. Primary terus melayani transaksi sabtu ramai, checkpoint berjalan, dan PostgreSQL dengan wajar mendaur ulang WAL lama yang tidak lagi dibutuhkan siapa pun. Senin pagi standby dinyalakan kembali — dan menolak menyala: WAL yang dibutuhnya sudah terdaur, satu-satunya jalan adalah pg_basebackup ulang dari nol pada database yang kini jauh lebih besar. Replication slot mencegah skenario ini: primary tidak berkeputusan sendiri membuang WAL yang masih dijanjikan kepada penerima tertentu. Chapter ini membuktikan mekanisme itu dengan satu pasangan server nyata, lalu membahas sisi gelapnya — slot yang ditinggalkan bisa mengisi disk sampai primary berhenti.

Masalah yang Dipecahkan Slot #

Tanpa slot, keputusan mendaur WAL dibuat buta terhadap nasib standby: PostgreSQL hanya melihat “apakah WAL ini lebih tua dari kebutuhan lokal saya”. Slot menambah satu pertanyaan: “apakah ada penerima yang belum mengaku menerima WAL ini?”. Saat sebuah slot aktif, primary mencatat titik restart_lsn — posisi WAL terakhir yang dikonfirmasi diterima penerima. Checkpoint lalu dilarang mendaur segmen WAL yang berada setelah titik itu milik slot tertua. Selama slot itu milik standby Anda, WAL menumpuk dengan sabar menunggu standby kembali, seberapapun lama jedanya — persis yang dibuktikan demo di bawah.

Cara pemakaian slot di sisi standby hanya satu baris konfigurasi: primary_slot_name di postgresql.auto.conf (atau postgresql.conf) berisi nama slot. Praktisnya, pg_basebackup bisa membuatkan semuanya sekaligus lewat kombinasi opsi -C -S nama_slot -R: base backup diambil, slot dibuat, dan primary_slot_name ditulis otomatis ke file konfigurasi standby.

Alternatif Tanpa Slot: wal_keep_size #

Sebelum slot ada (PostgreSQL 9.6 ke bawah), satu-satunya pengaman adalah wal_keep_size — menyisakan WAL sebanyak N MB terakhir “apa pun yang terjadi”. Mari kita baca nilai kedua parameter pengaman WAL di server ini:

sql batas-wal.sql
-- Berapa WAL yang dipertahankan TANPA slot (window tetap, dalam MB).
-- 0 berarti PostgreSQL tidak menyisakan WAL khusus untuk standby.
SHOW wal_keep_size;

-- Batas atas WAL yang boleh ditahan SEMUA slot (dalam MB).
-- -1 berarti tanpa batas: slot yang lupa dihapus bisa mengisi disk.
SHOW max_slot_wal_keep_size;

Output yang Dihasilkan:

text
 wal_keep_size 
---------------
 0
(1 row)

 max_slot_wal_keep_size 
------------------------
 -1
(1 row)

Nilai wal_keep_size = 0 menegaskan pendekatan window tetap tidak dipakai server ini. Memangnya kenapa window tetap lemah? Karena Anda menebak: 1 GB bisa terlalu besar untuk gangguan 5 menit, dan terlalu kecil untuk maintenance 2 hari. Slot mengganti tebakan dengan fakta — WAL disisakan tepat sampai titik yang dikonfirmasi penerima, tidak lebih dan tidak kurang.

Siklus Hidup Slot, Dibuktikan #

Skrip berikut memainkan satu putaran penuh: slot lahir bersama base backup, standby memakainya, standby dimatikan sementara primary terus menulis, standby menyala kembali dan terhindar dari pg_basebackup ulang, lalu slot dihapus bersih. Perhatikan kolom active dan wal_status di pg_replication_slots sepanjang perjalanan:

bash siklus-slot.sh
#!/usr/bin/env bash
# Siklus hidup replication slot: dibuat oleh pg_basebackup -C, menjaga WAL
# selama standby mati, lalu dihapus saat tidak lagi dibutuhkan.
# Server mini: primary port 54345, standby port 54346, 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_slot
PRI=$BASE/pgdata_pusat     # primary, port 54345
STD=$BASE/pgdata_cabang    # standby,  port 54346
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 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 dan jalankan primary di port 54345 =="
"$PGBIN/initdb" -D "$PRI" --no-locale -E UTF8 -U postgres >/dev/null 2>&1
"$PGBIN/pg_ctl" -D "$PRI" \
  -o "-p 54345 -k $SOCK -c listen_addresses=127.0.0.1" \
  -l "$BASE/primary.log" start >/dev/null 2>&1
tunggu 54345 postgres "SELECT 1;" "1"
psql -X -h 127.0.0.1 -p 54345 -U postgres -d postgres \
  -c "CREATE DATABASE toko;"
psql -X -h 127.0.0.1 -p 54345 -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 2: base backup dengan slot (-C -S slot_cabang -R) =="
PGDATABASE=postgres pg_basebackup -h 127.0.0.1 -p 54345 -U postgres \
  -D "$STD" -R -X stream -c fast -C -S slot_cabang >/dev/null 2>&1
psql -X -h 127.0.0.1 -p 54345 -U postgres -d postgres \
  -c "SELECT slot_name, slot_type, active, wal_status FROM pg_replication_slots;"
grep primary_slot_name "$STD/postgresql.auto.conf"

echo "== Langkah 3: jalankan standby dan pastikan data mengalir =="
"$PGBIN/pg_ctl" -D "$STD" \
  -o "-p 54346 -k $SOCK -c listen_addresses=127.0.0.1 -c cluster_name=standby_cabang" \
  -l "$BASE/standby.log" start >/dev/null 2>&1
tunggu 54346 toko "SELECT count(*) FROM produk;" "2"
psql -X -h 127.0.0.1 -p 54345 -U postgres -d toko \
  -c "INSERT INTO produk VALUES (3, 'Gula Pasir', 12000);"
tunggu 54346 toko "SELECT count(*) FROM produk;" "3"
psql -X -h 127.0.0.1 -p 54345 -U postgres -d postgres \
  -c "SELECT slot_name, slot_type, active, wal_status FROM pg_replication_slots;"

echo "== Langkah 4: matikan standby, lalu terus menulis di primary =="
"$PGBIN/pg_ctl" -D "$STD" -m fast stop >/dev/null 2>&1
psql -X -h 127.0.0.1 -p 54345 -U postgres -d toko \
  -c "INSERT INTO produk VALUES (4, 'Susu Kental', 9000), (5, 'Keju Slice', 21000);"
psql -X -h 127.0.0.1 -p 54345 -U postgres -d postgres \
  -c "SELECT slot_name, slot_type, active, wal_status FROM pg_replication_slots;"

echo "== Langkah 5: nyalakan standby lagi — WAL yang dipertahankan slot menyelamatkannya =="
"$PGBIN/pg_ctl" -D "$STD" \
  -o "-p 54346 -k $SOCK -c listen_addresses=127.0.0.1 -c cluster_name=standby_cabang" \
  -l "$BASE/standby.log" start >/dev/null 2>&1
tunggu 54346 toko "SELECT count(*) FROM produk;" "5"
psql -X -h 127.0.0.1 -p 54346 -U postgres -d toko \
  -c "SELECT id, nama, harga FROM produk ORDER BY id;"

echo "== Langkah 6: slot tidak dipakai lagi — hapus agar WAL bebas terdaur =="
"$PGBIN/pg_ctl" -D "$STD" -m fast stop >/dev/null 2>&1
psql -X -h 127.0.0.1 -p 54345 -U postgres -d postgres \
  -c "SELECT pg_drop_replication_slot('slot_cabang');" \
  -c "SELECT count(*) AS sisa_slot FROM pg_replication_slots;"

Output yang Dihasilkan:

text
== Langkah 1: initdb dan jalankan primary di port 54345 ==
CREATE DATABASE
CREATE TABLE
INSERT 0 2
== Langkah 2: base backup dengan slot (-C -S slot_cabang -R) ==
  slot_name  | slot_type | active | wal_status 
-------------+-----------+--------+------------
 slot_cabang | physical  | f      | reserved
(1 row)

primary_slot_name = 'slot_cabang'
== Langkah 3: jalankan standby dan pastikan data mengalir ==
INSERT 0 1
  slot_name  | slot_type | active | wal_status 
-------------+-----------+--------+------------
 slot_cabang | physical  | t      | reserved
(1 row)

== Langkah 4: matikan standby, lalu terus menulis di primary ==
INSERT 0 2
  slot_name  | slot_type | active | wal_status 
-------------+-----------+--------+------------
 slot_cabang | physical  | f      | reserved
(1 row)

== Langkah 5: nyalakan standby lagi — WAL yang dipertahankan slot menyelamatkannya ==
 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: slot tidak dipakai lagi — hapus agar WAL bebas terdaur ==
 pg_drop_replication_slot 
--------------------------
 
(1 row)

 sisa_slot 
-----------
         0
(1 row)

Mari kita lacak putarannya langkah demi langkah:

  1. Langkah 2 — slot lahir: pg_basebackup -C -S slot_cabang membuat slot physical bernama slot_cabang di primary, dan -R menuliskan primary_slot_name = 'slot_cabang' ke konfigurasi standby — baris grep membuktikannya. Posisi active = f wajar karena standby memang belum menyala.
  2. Langkah 3 — slot bekerja: begitu standby menyala dan mengejar, active berubah t. Kolom wal_status = reserved berarti WAL yang ditahan slot masih dalam batas wajar.
  3. Langkah 4 — jeda yang dulu berakibat fatal: standby dimatikan, primary menulis dua baris lagi. Di dunia tanpa slot, inilah momen WAL berisiko terdaur; dengan slot, wal_status tetap reserved dan WAL dua baris itu dipertahankan primary.
  4. Langkah 5 — pembuktian: standby menyala dan langsung melompat ke lima baris tanpa pg_basebackup ulang. Semua WAL yang tercipta selama jeda ditemukan utuh karena slot menjaganya.
  5. Langkah 6 — kebersihan: standby dihentikan lebih dulu karena slot yang sedang dipakai (active = t) menolak dihapus — server akan menjawab ERROR: replication slot "slot_cabang" is active for PID .... Setelah standby berhenti, pg_drop_replication_slot() menghapus slot dan sisa_slot kembali nol.

Slot adalah teman setia physical replication; menariknya, logical replication juga memakainya sebagai fondasi — setiap subscription otomatis membuat satu slot logical di publisher-nya. Dari pinning WAL kita bergerak ke model publikasi-langganan di chapter Logical Replication .

Referensi #

Berkas Sumber

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

batas-wal.sql

Unduh berkas
sql
-- Berapa WAL yang dipertahankan TANPA slot (window tetap, dalam MB).
-- 0 berarti PostgreSQL tidak menyisakan WAL khusus untuk standby.
SHOW wal_keep_size;

-- Batas atas WAL yang boleh ditahan SEMUA slot (dalam MB).
-- -1 berarti tanpa batas: slot yang lupa dihapus bisa mengisi disk.
SHOW max_slot_wal_keep_size;

siklus-slot.sh

Unduh berkas
bash
#!/usr/bin/env bash
# Siklus hidup replication slot: dibuat oleh pg_basebackup -C, menjaga WAL
# selama standby mati, lalu dihapus saat tidak lagi dibutuhkan.
# Server mini: primary port 54345, standby port 54346, 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_slot
PRI=$BASE/pgdata_pusat     # primary, port 54345
STD=$BASE/pgdata_cabang    # standby,  port 54346
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 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 dan jalankan primary di port 54345 =="
"$PGBIN/initdb" -D "$PRI" --no-locale -E UTF8 -U postgres >/dev/null 2>&1
"$PGBIN/pg_ctl" -D "$PRI" \
  -o "-p 54345 -k $SOCK -c listen_addresses=127.0.0.1" \
  -l "$BASE/primary.log" start >/dev/null 2>&1
tunggu 54345 postgres "SELECT 1;" "1"
psql -X -h 127.0.0.1 -p 54345 -U postgres -d postgres \
  -c "CREATE DATABASE toko;"
psql -X -h 127.0.0.1 -p 54345 -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 2: base backup dengan slot (-C -S slot_cabang -R) =="
PGDATABASE=postgres pg_basebackup -h 127.0.0.1 -p 54345 -U postgres \
  -D "$STD" -R -X stream -c fast -C -S slot_cabang >/dev/null 2>&1
psql -X -h 127.0.0.1 -p 54345 -U postgres -d postgres \
  -c "SELECT slot_name, slot_type, active, wal_status FROM pg_replication_slots;"
grep primary_slot_name "$STD/postgresql.auto.conf"

echo "== Langkah 3: jalankan standby dan pastikan data mengalir =="
"$PGBIN/pg_ctl" -D "$STD" \
  -o "-p 54346 -k $SOCK -c listen_addresses=127.0.0.1 -c cluster_name=standby_cabang" \
  -l "$BASE/standby.log" start >/dev/null 2>&1
tunggu 54346 toko "SELECT count(*) FROM produk;" "2"
psql -X -h 127.0.0.1 -p 54345 -U postgres -d toko \
  -c "INSERT INTO produk VALUES (3, 'Gula Pasir', 12000);"
tunggu 54346 toko "SELECT count(*) FROM produk;" "3"
psql -X -h 127.0.0.1 -p 54345 -U postgres -d postgres \
  -c "SELECT slot_name, slot_type, active, wal_status FROM pg_replication_slots;"

echo "== Langkah 4: matikan standby, lalu terus menulis di primary =="
"$PGBIN/pg_ctl" -D "$STD" -m fast stop >/dev/null 2>&1
psql -X -h 127.0.0.1 -p 54345 -U postgres -d toko \
  -c "INSERT INTO produk VALUES (4, 'Susu Kental', 9000), (5, 'Keju Slice', 21000);"
psql -X -h 127.0.0.1 -p 54345 -U postgres -d postgres \
  -c "SELECT slot_name, slot_type, active, wal_status FROM pg_replication_slots;"

echo "== Langkah 5: nyalakan standby lagi — WAL yang dipertahankan slot menyelamatkannya =="
"$PGBIN/pg_ctl" -D "$STD" \
  -o "-p 54346 -k $SOCK -c listen_addresses=127.0.0.1 -c cluster_name=standby_cabang" \
  -l "$BASE/standby.log" start >/dev/null 2>&1
tunggu 54346 toko "SELECT count(*) FROM produk;" "5"
psql -X -h 127.0.0.1 -p 54346 -U postgres -d toko \
  -c "SELECT id, nama, harga FROM produk ORDER BY id;"

echo "== Langkah 6: slot tidak dipakai lagi — hapus agar WAL bebas terdaur =="
"$PGBIN/pg_ctl" -D "$STD" -m fast stop >/dev/null 2>&1
psql -X -h 127.0.0.1 -p 54345 -U postgres -d postgres \
  -c "SELECT pg_drop_replication_slot('slot_cabang');" \
  -c "SELECT count(*) AS sisa_slot FROM pg_replication_slots;"