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.
| Komponen | Berada di | Peran |
|---|---|---|
CREATE PUBLICATION | Publisher | Menandai tabel mana yang perubahannya dikirim |
CREATE SUBSCRIPTION | Subscriber | Koneksi ke publisher + daftar publication yang dilanggan |
| Logical replication slot | Publisher | Dibuat otomatis oleh subscription; menahan WAL seperti chapter Replication Slot |
| Apply worker | Subscriber | Proses 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:
| Peran | Port | Database | Keterangan |
|---|---|---|---|
| Publisher (kantor pusat) | 54351 | pusat | wal_level = logical, sumber data |
| Subscriber (cabang) | 54352 | cabang | Server 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:
#!/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:
== 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:
-c wal_level=logicalhanya di publisher: subscriber tidak butuh level khusus — server normal denganwal_levelbawaan saja sudah cukup menerima dan mengeksekusi perubahan. Ini cermin asimetri peran: publisher harus menerjemahkan WAL menjadi perubahan logis, subscriber tinggal mengeksekusinya.- Langkah 4 — tabel sengaja dibuat hanya di publisher: baris
tabel di subscriber sengaja belum dibuatmenandai pilihan sadar. Skema tidak pernah dikirim oleh logical replication, jadi tabel di subscriber harus dibuat manual — dan itu tugas skrip kedua. - Langkah 5 — dua view katalog:
pg_publicationmemperlihatkan publicationpub_produkdenganpuballtables = f(bukanFOR ALL TABLES, jadi benar-benar selektif), danpg_publication_tablesmenegaskan 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:
#!/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:
== 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:
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.- Langkah 3 — tiga baris muncul di cabang tanpa
INSERTmanual: begitu subscription aktif, apply worker melakukan initial copy memakaiCOPYinternal. Data awal tidak lewat WAL — hanya data setelahnya yang mengalir sebagai perubahan logis. - Langkah 4 dan 5 — dua sisi satu koneksi: dari cabang,
pg_subscriptionmenunjukkan subscription aktif (subenabled = t) dengan slotsub_produkdan publicationpub_produk. Dari pusat,pg_stat_replicationmenampilkan koneksi masuk denganapplication_name = sub_produk,state = streaming,sync_state = async. - Langkah 6 — cabang menerima
INSERTlokal tanpa protes: tabelcatatan_penerimaantidak ada di publication, jadi baris tersebut murni urusan cabang. Bandingkan dengan standby physical di chapter Streaming Replication yang menolak semuaINSERT— 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:
== 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:
| Objek | Ikut direplikasikan? | Konsekuensi |
|---|---|---|
INSERT / UPDATE / DELETE / TRUNCATE | Ya | Inti aliran data |
DDL (ALTER TABLE, CREATE INDEX, …) | Tidak | Skema dikelola manual di kedua sisi |
| Sequence / nilai auto-increment | Tidak | nextval() di subscriber tidak mengikuti publisher |
Large object (pg_largeobject) | Tidak | Tidak 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:
== 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 dihapusMari kita baca bukti-bukti pentingnya:
- Kolom
stokhanya lahir di pusat:kolom_pusat = 4sementarakolom_cabang = 3—ALTER TABLEdi publisher tidak menyentuh subscriber sama sekali. - Baris
Kopi Luwaktertahan selama 4 detik:INSERTbaru memuat kolomstok, 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. - Setelah DDL disamakan, baris telat itu tiba: begitu kolom
stokdibuat di cabang, percobaan ulang apply worker berhasil danKopi Luwakmuncul denganid 5,stok 3. Perubahan yang tertunda tidak hilang — hanya menunggu. id_berikut_pusat = 6vsid_berikut_cabang = 1: initial copy menyalin baris, bukan counter. Sequence di cabang belum pernah dimajukan siapa pun, jadinextval()di sana mulai dari awal. Bila cabang kelak perlu menghasilkanidsendiri, selaraskan dulu dengansetval()— atau rancang rentangidberbeda 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#!/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#!/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#!/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#!/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;"