Chapter sebelumnya membangun replikasi dengan satu resep: salin seluruh cluster, kirim WAL mentah. Logical replication memilih sudut pandang yang berbeda — publisher hanya mengumumkan “saya punya perubahan pada tabel-tabel ini”, dan server lain boleh berlangganan pengumuman itu lalu mengeksekusinya sebagai INSERT/UPDATE/DELETE biasa. Hasilnya adalah replikasi yang selektif per tabel, lintas versi major, dan — ini yang paling mengejutkan bagi yang baru pindah dari physical — server penerima tetap server normal yang bisa ditulis. Chapter ini membangun dua cluster lokal sungguhan, menyaksikan initial copy menyalin data awal, mengalirkan perubahan DML, lalu menguji batasnya: DDL dan sequence yang memang tidak pernah ikut dikirim.

Model Publikasi dan Langganan #

Logical replication memakai istilah yang akrab di sistem pesan: publisher dan subscriber. Publisher adalah server sumber yang menjalankan wal_level = logical (level yang dibahas chapter Physical vs Logical Replication ); di dalamnya Anda membuat publication — daftar tabel yang perubahannya mau dikirim. Subscriber adalah server tujuan; di dalamnya Anda membuat subscription yang menyimpan connection string ke publisher plus nama publication yang dilanggan. Begitu subscription aktif, PostgreSQL mengerjakan dua hal otomatis: menyalin isi tabel saat ini (initial copy), lalu mengalirkan setiap perubahan baru.

KomponenBerada diPeran
CREATE PUBLICATIONPublisherMenandai tabel mana yang perubahannya dikirim
CREATE SUBSCRIPTIONSubscriberKoneksi ke publisher + daftar publication yang dilanggan
Logical replication slotPublisherDibuat otomatis oleh subscription; menahan WAL seperti chapter Replication Slot
Apply workerSubscriberProses background yang mengeksekusi perubahan yang datang

Aliran datanya satu arah: publisher ke subscriber. Sebuah publication boleh dilanggan banyak subscriber, dan satu subscriber boleh melanggan publication dari banyak publisher — pola bintang untuk distribusi data maupun konsolidasi.

Menyiapkan Publisher #

Publisher membutuhkan wal_level = logical, sehingga server utama course (yang berjalan di level replica) tidak bisa dipakai begitu saja — dan kita memang tidak akan menyentuhnya. Seperti chapter Streaming Replication , kita membuat dua cluster disposable sendiri, kali ini di /home/z/my-project/tmp/rp_logical:

PeranPortDatabaseKeterangan
Publisher (kantor pusat)54351pusatwal_level = logical, sumber data
Subscriber (cabang)54352cabangServer normal, sepenuhnya bisa ditulis

Skrip pertama menyiapkan kedua cluster dan menutup dengan CREATE PUBLICATION — pasangan server ini sengaja dibiarkan menyala karena tiga skrip berikutnya melanjutkannya:

bash publikasi.sh
#!/usr/bin/env bash
# Menyiapkan publisher dan subscriber untuk demo logical replication.
# Publisher (kantor pusat) di port 54351 dengan wal_level=logical,
# subscriber (cabang) di port 54352 sebagai server normal yang bisa ditulis.
# Skrip berikutnya (langganan.sh) melanjutkan pasangan server ini.
set -eu

PGBIN=/home/z/my-project/tools/dl/pgdeb/usr/lib/postgresql/17/bin
BASE=/home/z/my-project/tmp/rp_logical
PUB=$BASE/pgdata_pusat    # publisher, port 54351
SUB=$BASE/pgdata_cabang   # subscriber, port 54352
SOCK=$BASE/sock

# Buang sisa percobaan sebelumnya bila ada.
"$PGBIN/pg_ctl" -D "$PUB" -m immediate stop >/dev/null 2>&1 || true
"$PGBIN/pg_ctl" -D "$SUB" -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 dua cluster =="
"$PGBIN/initdb" -D "$PUB" --no-locale -E UTF8 -U postgres >/dev/null 2>&1
"$PGBIN/initdb" -D "$SUB" --no-locale -E UTF8 -U postgres >/dev/null 2>&1

echo "== Langkah 2: jalankan publisher dengan wal_level=logical =="
"$PGBIN/pg_ctl" -D "$PUB" \
  -o "-p 54351 -k $SOCK -c listen_addresses=127.0.0.1 -c wal_level=logical" \
  -l "$BASE/publisher.log" start >/dev/null 2>&1
tunggu 54351 postgres "SHOW wal_level;" "logical"

echo "== Langkah 3: jalankan subscriber (server normal) =="
"$PGBIN/pg_ctl" -D "$SUB" \
  -o "-p 54352 -k $SOCK -c listen_addresses=127.0.0.1" \
  -l "$BASE/subscriber.log" start >/dev/null 2>&1
tunggu 54352 postgres "SELECT 1;" "1"

echo "== Langkah 4: database dan tabel di kedua sisi =="
psql -X -h 127.0.0.1 -p 54351 -U postgres -d postgres \
  -c "CREATE DATABASE pusat;"
psql -X -h 127.0.0.1 -p 54352 -U postgres -d postgres \
  -c "CREATE DATABASE cabang;"
psql -X -h 127.0.0.1 -p 54351 -U postgres -d pusat \
  -c "CREATE TABLE produk (id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, nama text NOT NULL, harga numeric);" \
  -c "INSERT INTO produk (nama, harga) VALUES ('Kopi Tubruk', 15000), ('Teh Celup', 8000), ('Gula Pasir', 12000);"
echo "tabel di subscriber sengaja belum dibuat"

echo "== Langkah 5: publikasikan tabel produk =="
psql -X -h 127.0.0.1 -p 54351 -U postgres -d pusat \
  -c "CREATE PUBLICATION pub_produk FOR TABLE produk;"
psql -X -h 127.0.0.1 -p 54351 -U postgres -d pusat \
  -c "SELECT pubname, puballtables FROM pg_publication;" \
  -c "SELECT pubname, schemaname, tablename FROM pg_publication_tables ORDER BY tablename;"

Output yang Dihasilkan:

text
== Langkah 1: initdb dua cluster ==
== Langkah 2: jalankan publisher dengan wal_level=logical ==
== Langkah 3: jalankan subscriber (server normal) ==
== Langkah 4: database dan tabel di kedua sisi ==
CREATE DATABASE
CREATE DATABASE
CREATE TABLE
INSERT 0 3
tabel di subscriber sengaja belum dibuat
== Langkah 5: publikasikan tabel produk ==
CREATE PUBLICATION
  pubname   | puballtables 
------------+--------------
 pub_produk | f
(1 row)

  pubname   | schemaname | tablename 
------------+------------+-----------
 pub_produk | public     | produk
(1 row)

Mari kita bedah bagian-bagian kuncinya:

  1. -c wal_level=logical hanya di publisher: subscriber tidak butuh level khusus — server normal dengan wal_level bawaan saja sudah cukup menerima dan mengeksekusi perubahan. Ini cermin asimetri peran: publisher harus menerjemahkan WAL menjadi perubahan logis, subscriber tinggal mengeksekusinya.
  2. Langkah 4 — tabel sengaja dibuat hanya di publisher: baris tabel di subscriber sengaja belum dibuat menandai pilihan sadar. Skema tidak pernah dikirim oleh logical replication, jadi tabel di subscriber harus dibuat manual — dan itu tugas skrip kedua.
  3. Langkah 5 — dua view katalog: pg_publication memperlihatkan publication pub_produk dengan puballtables = f (bukan FOR ALL TABLES, jadi benar-benar selektif), dan pg_publication_tables menegaskan isinya satu tabel: public.produk.

Berlangganan: Initial Copy Lalu Streaming #

Subscription membutuhkan tabel tujuan yang sudah ada dengan kolom yang cocok. Skrip berikut membuat tabel produk di cabang, lalu menjalankan CREATE SUBSCRIPTION — perhatikan bahwa perintah ini sekaligus menciptakan slot replikasi di publisher (lihat NOTICE di output), melakukan initial copy, dan menyalakan apply worker:

bash langganan.sh
#!/usr/bin/env bash
# Membuat subscription di sisi cabang: initial copy lalu streaming perubahan.
# Skrip ini melanjutkan pasangan server dari publikasi.sh:
# publisher di port 54351 (database pusat), subscriber di port 54352.
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 "== Langkah 1: tabel harus sudah ada di subscriber sebelum berlangganan =="
psql -X -h 127.0.0.1 -p 54352 -U postgres -d cabang \
  -c "CREATE TABLE produk (id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, nama text NOT NULL, harga numeric);"

echo "== Langkah 2: buat subscription ke publication pub_produk =="
psql -X -h 127.0.0.1 -p 54352 -U postgres -d cabang 2>&1 \
  -c "CREATE SUBSCRIPTION sub_produk CONNECTION 'host=127.0.0.1 port=54351 user=postgres dbname=pusat' PUBLICATION pub_produk;"

echo "== Langkah 3: initial copy menyalin isi tabel ke cabang =="
tunggu 54352 cabang "SELECT count(*) FROM produk;" "3"
psql -X -h 127.0.0.1 -p 54352 -U postgres -d cabang \
  -c "SELECT id, nama, harga FROM produk ORDER BY id;"

echo "== Langkah 4: katalog subscription di cabang =="
psql -X -h 127.0.0.1 -p 54352 -U postgres -d cabang \
  -c "SELECT subname, subenabled, subslotname, subpublications FROM pg_subscription;"

echo "== Langkah 5: koneksi yang terlihat dari publisher =="
tunggu 54351 pusat "SELECT count(*) FROM pg_stat_replication;" "1"
psql -X -h 127.0.0.1 -p 54351 -U postgres -d pusat \
  -c "SELECT application_name, state, sync_state FROM pg_stat_replication;"

echo "== Langkah 6: subscriber tetap server biasa — bisa ditulis =="
echo "tabel catatan_penerimaan tidak ikut dipublikasikan, murni milik cabang:"
psql -X -h 127.0.0.1 -p 54352 -U postgres -d cabang \
  -c "CREATE TABLE catatan_penerimaan (id int PRIMARY KEY, catatan text);" \
  -c "INSERT INTO catatan_penerimaan VALUES (1, 'initial copy produk selesai');" \
  -c "SELECT id, catatan FROM catatan_penerimaan ORDER BY id;"

Output yang Dihasilkan:

text
== Langkah 1: tabel harus sudah ada di subscriber sebelum berlangganan ==
CREATE TABLE
== Langkah 2: buat subscription ke publication pub_produk ==
NOTICE:  created replication slot "sub_produk" on publisher
CREATE SUBSCRIPTION
== Langkah 3: initial copy menyalin isi tabel ke cabang ==
 id |    nama     | harga 
----+-------------+-------
  1 | Kopi Tubruk | 15000
  2 | Teh Celup   |  8000
  3 | Gula Pasir  | 12000
(3 rows)

== Langkah 4: katalog subscription di cabang ==
  subname   | subenabled | subslotname | subpublications 
------------+------------+-------------+-----------------
 sub_produk | t          | sub_produk  | {pub_produk}
(1 row)

== Langkah 5: koneksi yang terlihat dari publisher ==
 application_name |   state   | sync_state 
------------------+-----------+------------
 sub_produk       | streaming | async
(1 row)

== Langkah 6: subscriber tetap server biasa — bisa ditulis ==
tabel catatan_penerimaan tidak ikut dipublikasikan, murni milik cabang:
CREATE TABLE
INSERT 0 1
 id |           catatan           
----+-----------------------------
  1 | initial copy produk selesai
(1 row)

Empat bukti berjejer di output itu:

  1. NOTICE: created replication slot "sub_produk" on publisher: subscription otomatis membuat slot logical bernama sama dengan subscription-nya. Slot inilah yang menahan WAL di publisher — teori chapter Replication Slot bekerja persis sama di sini.
  2. Langkah 3 — tiga baris muncul di cabang tanpa INSERT manual: begitu subscription aktif, apply worker melakukan initial copy memakai COPY internal. Data awal tidak lewat WAL — hanya data setelahnya yang mengalir sebagai perubahan logis.
  3. Langkah 4 dan 5 — dua sisi satu koneksi: dari cabang, pg_subscription menunjukkan subscription aktif (subenabled = t) dengan slot sub_produk dan publication pub_produk. Dari pusat, pg_stat_replication menampilkan koneksi masuk dengan application_name = sub_produk, state = streaming, sync_state = async.
  4. Langkah 6 — cabang menerima INSERT lokal tanpa protes: tabel catatan_penerimaan tidak ada di publication, jadi baris tersebut murni urusan cabang. Bandingkan dengan standby physical di chapter Streaming Replication yang menolak semua INSERT — subscriber logical adalah server tulis penuh.

Perubahan DML Mengalir Setiap Detik #

Initial copy hanyalah pembuka. Dari titik ini setiap INSERT, UPDATE, dan DELETE yang di-commit di pusat dikirim ke cabang. Skrip berikut menjalankan ketiganya berturut-turut dan memverifikasi hasilnya di sisi cabang:

#!/usr/bin/env bash
# INSERT, UPDATE, dan DELETE di pusat mengalir ke cabang lewat subscription.
# Skrip ini melanjutkan pasangan server dari publikasi.sh + langganan.sh:
# publisher di port 54351, subscriber di port 54352.
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 "== INSERT di pusat =="
psql -X -h 127.0.0.1 -p 54351 -U postgres -d pusat \
  -c "INSERT INTO produk (nama, harga) VALUES ('Susu Kental', 9000);"
tunggu 54352 cabang "SELECT count(*) FROM produk;" "4"
echo "tiba di cabang"

echo "== UPDATE di pusat =="
psql -X -h 127.0.0.1 -p 54351 -U postgres -d pusat \
  -c "UPDATE produk SET harga = 8500 WHERE nama = 'Teh Celup';"
tunggu 54352 cabang "SELECT harga FROM produk WHERE nama = 'Teh Celup';" "8500"
echo "terlihat di cabang"

echo "== DELETE di pusat =="
psql -X -h 127.0.0.1 -p 54351 -U postgres -d pusat \
  -c "DELETE FROM produk WHERE nama = 'Kopi Tubruk';"
tunggu 54352 cabang "SELECT count(*) FROM produk;" "3"
echo "hilang di cabang"

echo "== Keadaan akhir tabel yang sama di cabang =="
psql -X -h 127.0.0.1 -p 54352 -U postgres -d cabang \
  -c "SELECT id, nama, harga FROM produk ORDER BY id;"

Output yang Dihasilkan:

text
== INSERT di pusat ==
INSERT 0 1
tiba di cabang
== UPDATE di pusat ==
UPDATE 1
terlihat di cabang
== DELETE di pusat ==
DELETE 1
hilang di cabang
== Keadaan akhir tabel yang sama di cabang ==
 id |    nama     | harga 
----+-------------+-------
  2 | Teh Celup   |  8500
  3 | Gula Pasir  | 12000
  4 | Susu Kental |  9000
(3 rows)

Perhatikan pola poll-then-verify: setiap perubahan diikuti tunggu yang mendeteksi efeknya di cabang. Tabel akhir cabang memuat persis hasil ketiga operasi — Susu Kental hadir dengan id 4, harga Teh Celup sudah 8500, dan Kopi Tubruk lenyap. Replikasi level baris artinya benar-benar level baris: kolom yang tidak disentuh UPDATE tetap utuh, dan id yang lahir dari sequence pusat ikut terkirim sebagai nilai biasa.

Batasan: DDL dan Sequence Tidak Ikut #

Inilah gotcha terbesar logical replication. Publication mengumumkan perubahan data, bukan definisi tabel. Ringkasan resminya:

ObjekIkut direplikasikan?Konsekuensi
INSERT / UPDATE / DELETE / TRUNCATEYaInti aliran data
DDL (ALTER TABLE, CREATE INDEX, …)TidakSkema dikelola manual di kedua sisi
Sequence / nilai auto-incrementTidaknextval() di subscriber tidak mengikuti publisher
Large object (pg_largeobject)TidakTidak bisa dikirim lewat publication

Skrip terakhir membuktikan dua baris pertama tabel itu secara langsung — dan sekaligus memperlihatkan apa yang terjadi ketika skema dua sisi berbeda: replikasi macet dalam diam, lalu pulih begitu DDL disamakan:

#!/usr/bin/env bash
# Batasan logical replication yang paling sering menggigit:
# DDL dan sequence tidak ikut direplikasikan.
# Skrip terakhir chapter ini sekaligus menghentikan kedua server demo.
set -eu

PGBIN=/home/z/my-project/tools/dl/pgdeb/usr/lib/postgresql/17/bin
BASE=/home/z/my-project/tmp/rp_logical

# 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 "== Batasan 1: DDL tidak direplikasikan =="
psql -X -h 127.0.0.1 -p 54351 -U postgres -d pusat \
  -c "ALTER TABLE produk ADD COLUMN stok int DEFAULT 0;"
echo "jumlah kolom tabel produk:"
psql -X -h 127.0.0.1 -p 54351 -U postgres -d pusat \
  -c "SELECT count(*) AS kolom_pusat FROM information_schema.columns WHERE table_name = 'produk';"
psql -X -h 127.0.0.1 -p 54352 -U postgres -d cabang \
  -c "SELECT count(*) AS kolom_cabang FROM information_schema.columns WHERE table_name = 'produk';"

echo "== Skema yang berbeda membuat replikasi macet =="
psql -X -h 127.0.0.1 -p 54351 -U postgres -d pusat \
  -c "INSERT INTO produk (nama, harga, stok) VALUES ('Kopi Luwak', 250000, 3);"
echo "pantau 4 detik: baris ke-4 tidak pernah sampai cabang"
for i in $(seq 1 20); do
  n=$(psql -X -q -t -A -h 127.0.0.1 -p 54352 -U postgres -d cabang \
      -c "SELECT count(*) FROM produk;" 2>/dev/null)
  if [ "$n" -gt 3 ]; then
    echo "UNEXPECTED: baris sampai cabang, skrip tidak valid" >&2
    exit 1
  fi
  sleep 0.2
done
psql -X -h 127.0.0.1 -p 54352 -U postgres -d cabang \
  -c "SELECT count(*) AS baris_cabang FROM produk;"

echo "== Perbaikan: terapkan DDL yang sama manual di cabang =="
psql -X -h 127.0.0.1 -p 54352 -U postgres -d cabang \
  -c "ALTER TABLE produk ADD COLUMN stok int DEFAULT 0;"
tunggu 54352 cabang "SELECT count(*) FROM produk;" "4"
psql -X -h 127.0.0.1 -p 54352 -U postgres -d cabang \
  -c "SELECT id, nama, harga, stok FROM produk ORDER BY id;"

echo "== Batasan 2: sequence tidak direplikasikan =="
psql -X -h 127.0.0.1 -p 54351 -U postgres -d pusat \
  -c "SELECT nextval(pg_get_serial_sequence('produk', 'id')) AS id_berikut_pusat;"
psql -X -h 127.0.0.1 -p 54352 -U postgres -d cabang \
  -c "SELECT nextval(pg_get_serial_sequence('produk', 'id')) AS id_berikut_cabang;"

echo "== Bereskan demo =="
"$PGBIN/pg_ctl" -D "$BASE/pgdata_cabang" -m fast stop >/dev/null 2>&1
echo "subscriber berhenti"
"$PGBIN/pg_ctl" -D "$BASE/pgdata_pusat" -m fast stop >/dev/null 2>&1
echo "publisher berhenti"
rm -rf "$BASE"
echo "direktori data demo dihapus"

Output yang Dihasilkan:

text
== Batasan 1: DDL tidak direplikasikan ==
ALTER TABLE
jumlah kolom tabel produk:
 kolom_pusat 
-------------
           4
(1 row)

 kolom_cabang 
--------------
            3
(1 row)

== Skema yang berbeda membuat replikasi macet ==
INSERT 0 1
pantau 4 detik: baris ke-4 tidak pernah sampai cabang
 baris_cabang 
--------------
            3
(1 row)

== Perbaikan: terapkan DDL yang sama manual di cabang ==
ALTER TABLE
 id |    nama     | harga  | stok 
----+-------------+--------+------
  2 | Teh Celup   |   8500 |    0
  3 | Gula Pasir  |  12000 |    0
  4 | Susu Kental |   9000 |    0
  5 | Kopi Luwak  | 250000 |    3
(4 rows)

== Batasan 2: sequence tidak direplikasikan ==
 id_berikut_pusat 
------------------
                6
(1 row)

 id_berikut_cabang 
-------------------
                 1
(1 row)

== Bereskan demo ==
subscriber berhenti
publisher berhenti
direktori data demo dihapus

Mari kita baca bukti-bukti pentingnya:

  1. Kolom stok hanya lahir di pusat: kolom_pusat = 4 sementara kolom_cabang = 3 — ALTER TABLE di publisher tidak menyentuh subscriber sama sekali.
  2. Baris Kopi Luwak tertahan selama 4 detik: INSERT baru memuat kolom stok, sedangkan tabel cabang tidak punya kolom itu. Apply worker gagal mengeksekusi perubahan dan terus mencoba ulang — inilah replikasi macet: tidak ada error yang muncul di sesi pusat (commit di sana sukses!), hanya keheningan di cabang.
  3. Setelah DDL disamakan, baris telat itu tiba: begitu kolom stok dibuat di cabang, percobaan ulang apply worker berhasil dan Kopi Luwak muncul dengan id 5, stok 3. Perubahan yang tertunda tidak hilang — hanya menunggu.
  4. id_berikut_pusat = 6 vs id_berikut_cabang = 1: initial copy menyalin baris, bukan counter. Sequence di cabang belum pernah dimajukan siapa pun, jadi nextval() di sana mulai dari awal. Bila cabang kelak perlu menghasilkan id sendiri, selaraskan dulu dengan setval() — atau rancang rentang id berbeda per lokasi agar tidak pernah bertabrakan.

Logical replication menempatkannya sebagai alat distribusi dan migrasi — bukan kandidat failover, karena subscriber tidak memuat state cluster. Namun baik physical maupun logical sama-sama bekerja asinkron: ada jeda antara commit di satu sisi dan penerimaan di sisi lain. Jeda itulah yang harus diukur dan dikendalikan — topik chapter Replication Lag .

Referensi #

Berkas Sumber

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

aliran-perubahan.sh

Unduh berkas
bash
#!/usr/bin/env bash
# INSERT, UPDATE, dan DELETE di pusat mengalir ke cabang lewat subscription.
# Skrip ini melanjutkan pasangan server dari publikasi.sh + langganan.sh:
# publisher di port 54351, subscriber di port 54352.
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 "== INSERT di pusat =="
psql -X -h 127.0.0.1 -p 54351 -U postgres -d pusat \
  -c "INSERT INTO produk (nama, harga) VALUES ('Susu Kental', 9000);"
tunggu 54352 cabang "SELECT count(*) FROM produk;" "4"
echo "tiba di cabang"

echo "== UPDATE di pusat =="
psql -X -h 127.0.0.1 -p 54351 -U postgres -d pusat \
  -c "UPDATE produk SET harga = 8500 WHERE nama = 'Teh Celup';"
tunggu 54352 cabang "SELECT harga FROM produk WHERE nama = 'Teh Celup';" "8500"
echo "terlihat di cabang"

echo "== DELETE di pusat =="
psql -X -h 127.0.0.1 -p 54351 -U postgres -d pusat \
  -c "DELETE FROM produk WHERE nama = 'Kopi Tubruk';"
tunggu 54352 cabang "SELECT count(*) FROM produk;" "3"
echo "hilang di cabang"

echo "== Keadaan akhir tabel yang sama di cabang =="
psql -X -h 127.0.0.1 -p 54352 -U postgres -d cabang \
  -c "SELECT id, nama, harga FROM produk ORDER BY id;"

batasan-logical.sh

Unduh berkas
bash
#!/usr/bin/env bash
# Batasan logical replication yang paling sering menggigit:
# DDL dan sequence tidak ikut direplikasikan.
# Skrip terakhir chapter ini sekaligus menghentikan kedua server demo.
set -eu

PGBIN=/home/z/my-project/tools/dl/pgdeb/usr/lib/postgresql/17/bin
BASE=/home/z/my-project/tmp/rp_logical

# 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 "== Batasan 1: DDL tidak direplikasikan =="
psql -X -h 127.0.0.1 -p 54351 -U postgres -d pusat \
  -c "ALTER TABLE produk ADD COLUMN stok int DEFAULT 0;"
echo "jumlah kolom tabel produk:"
psql -X -h 127.0.0.1 -p 54351 -U postgres -d pusat \
  -c "SELECT count(*) AS kolom_pusat FROM information_schema.columns WHERE table_name = 'produk';"
psql -X -h 127.0.0.1 -p 54352 -U postgres -d cabang \
  -c "SELECT count(*) AS kolom_cabang FROM information_schema.columns WHERE table_name = 'produk';"

echo "== Skema yang berbeda membuat replikasi macet =="
psql -X -h 127.0.0.1 -p 54351 -U postgres -d pusat \
  -c "INSERT INTO produk (nama, harga, stok) VALUES ('Kopi Luwak', 250000, 3);"
echo "pantau 4 detik: baris ke-4 tidak pernah sampai cabang"
for i in $(seq 1 20); do
  n=$(psql -X -q -t -A -h 127.0.0.1 -p 54352 -U postgres -d cabang \
      -c "SELECT count(*) FROM produk;" 2>/dev/null)
  if [ "$n" -gt 3 ]; then
    echo "UNEXPECTED: baris sampai cabang, skrip tidak valid" >&2
    exit 1
  fi
  sleep 0.2
done
psql -X -h 127.0.0.1 -p 54352 -U postgres -d cabang \
  -c "SELECT count(*) AS baris_cabang FROM produk;"

echo "== Perbaikan: terapkan DDL yang sama manual di cabang =="
psql -X -h 127.0.0.1 -p 54352 -U postgres -d cabang \
  -c "ALTER TABLE produk ADD COLUMN stok int DEFAULT 0;"
tunggu 54352 cabang "SELECT count(*) FROM produk;" "4"
psql -X -h 127.0.0.1 -p 54352 -U postgres -d cabang \
  -c "SELECT id, nama, harga, stok FROM produk ORDER BY id;"

echo "== Batasan 2: sequence tidak direplikasikan =="
psql -X -h 127.0.0.1 -p 54351 -U postgres -d pusat \
  -c "SELECT nextval(pg_get_serial_sequence('produk', 'id')) AS id_berikut_pusat;"
psql -X -h 127.0.0.1 -p 54352 -U postgres -d cabang \
  -c "SELECT nextval(pg_get_serial_sequence('produk', 'id')) AS id_berikut_cabang;"

echo "== Bereskan demo =="
"$PGBIN/pg_ctl" -D "$BASE/pgdata_cabang" -m fast stop >/dev/null 2>&1
echo "subscriber berhenti"
"$PGBIN/pg_ctl" -D "$BASE/pgdata_pusat" -m fast stop >/dev/null 2>&1
echo "publisher berhenti"
rm -rf "$BASE"
echo "direktori data demo dihapus"

langganan.sh

Unduh berkas
bash
#!/usr/bin/env bash
# Membuat subscription di sisi cabang: initial copy lalu streaming perubahan.
# Skrip ini melanjutkan pasangan server dari publikasi.sh:
# publisher di port 54351 (database pusat), subscriber di port 54352.
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 "== Langkah 1: tabel harus sudah ada di subscriber sebelum berlangganan =="
psql -X -h 127.0.0.1 -p 54352 -U postgres -d cabang \
  -c "CREATE TABLE produk (id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, nama text NOT NULL, harga numeric);"

echo "== Langkah 2: buat subscription ke publication pub_produk =="
psql -X -h 127.0.0.1 -p 54352 -U postgres -d cabang 2>&1 \
  -c "CREATE SUBSCRIPTION sub_produk CONNECTION 'host=127.0.0.1 port=54351 user=postgres dbname=pusat' PUBLICATION pub_produk;"

echo "== Langkah 3: initial copy menyalin isi tabel ke cabang =="
tunggu 54352 cabang "SELECT count(*) FROM produk;" "3"
psql -X -h 127.0.0.1 -p 54352 -U postgres -d cabang \
  -c "SELECT id, nama, harga FROM produk ORDER BY id;"

echo "== Langkah 4: katalog subscription di cabang =="
psql -X -h 127.0.0.1 -p 54352 -U postgres -d cabang \
  -c "SELECT subname, subenabled, subslotname, subpublications FROM pg_subscription;"

echo "== Langkah 5: koneksi yang terlihat dari publisher =="
tunggu 54351 pusat "SELECT count(*) FROM pg_stat_replication;" "1"
psql -X -h 127.0.0.1 -p 54351 -U postgres -d pusat \
  -c "SELECT application_name, state, sync_state FROM pg_stat_replication;"

echo "== Langkah 6: subscriber tetap server biasa — bisa ditulis =="
echo "tabel catatan_penerimaan tidak ikut dipublikasikan, murni milik cabang:"
psql -X -h 127.0.0.1 -p 54352 -U postgres -d cabang \
  -c "CREATE TABLE catatan_penerimaan (id int PRIMARY KEY, catatan text);" \
  -c "INSERT INTO catatan_penerimaan VALUES (1, 'initial copy produk selesai');" \
  -c "SELECT id, catatan FROM catatan_penerimaan ORDER BY id;"

publikasi.sh

Unduh berkas
bash
#!/usr/bin/env bash
# Menyiapkan publisher dan subscriber untuk demo logical replication.
# Publisher (kantor pusat) di port 54351 dengan wal_level=logical,
# subscriber (cabang) di port 54352 sebagai server normal yang bisa ditulis.
# Skrip berikutnya (langganan.sh) melanjutkan pasangan server ini.
set -eu

PGBIN=/home/z/my-project/tools/dl/pgdeb/usr/lib/postgresql/17/bin
BASE=/home/z/my-project/tmp/rp_logical
PUB=$BASE/pgdata_pusat    # publisher, port 54351
SUB=$BASE/pgdata_cabang   # subscriber, port 54352
SOCK=$BASE/sock

# Buang sisa percobaan sebelumnya bila ada.
"$PGBIN/pg_ctl" -D "$PUB" -m immediate stop >/dev/null 2>&1 || true
"$PGBIN/pg_ctl" -D "$SUB" -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 dua cluster =="
"$PGBIN/initdb" -D "$PUB" --no-locale -E UTF8 -U postgres >/dev/null 2>&1
"$PGBIN/initdb" -D "$SUB" --no-locale -E UTF8 -U postgres >/dev/null 2>&1

echo "== Langkah 2: jalankan publisher dengan wal_level=logical =="
"$PGBIN/pg_ctl" -D "$PUB" \
  -o "-p 54351 -k $SOCK -c listen_addresses=127.0.0.1 -c wal_level=logical" \
  -l "$BASE/publisher.log" start >/dev/null 2>&1
tunggu 54351 postgres "SHOW wal_level;" "logical"

echo "== Langkah 3: jalankan subscriber (server normal) =="
"$PGBIN/pg_ctl" -D "$SUB" \
  -o "-p 54352 -k $SOCK -c listen_addresses=127.0.0.1" \
  -l "$BASE/subscriber.log" start >/dev/null 2>&1
tunggu 54352 postgres "SELECT 1;" "1"

echo "== Langkah 4: database dan tabel di kedua sisi =="
psql -X -h 127.0.0.1 -p 54351 -U postgres -d postgres \
  -c "CREATE DATABASE pusat;"
psql -X -h 127.0.0.1 -p 54352 -U postgres -d postgres \
  -c "CREATE DATABASE cabang;"
psql -X -h 127.0.0.1 -p 54351 -U postgres -d pusat \
  -c "CREATE TABLE produk (id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, nama text NOT NULL, harga numeric);" \
  -c "INSERT INTO produk (nama, harga) VALUES ('Kopi Tubruk', 15000), ('Teh Celup', 8000), ('Gula Pasir', 12000);"
echo "tabel di subscriber sengaja belum dibuat"

echo "== Langkah 5: publikasikan tabel produk =="
psql -X -h 127.0.0.1 -p 54351 -U postgres -d pusat \
  -c "CREATE PUBLICATION pub_produk FOR TABLE produk;"
psql -X -h 127.0.0.1 -p 54351 -U postgres -d pusat \
  -c "SELECT pubname, puballtables FROM pg_publication;" \
  -c "SELECT pubname, schemaname, tablename FROM pg_publication_tables ORDER BY tablename;"