複寫與高可用(Replication):從 Streaming 到 Logical Replication | PostgreSQL

2026/07/06
複寫與高可用(Replication):從 Streaming 到 Logical Replication | PostgreSQL

PostgreSQL複寫(Replication)是高可用架構的核心技術。透過將 Primary 節點的資料變更持續同步到 Standby 節點,系統在 Primary 故障時能快速切換。本文將完整解析 Physical ReplicationLogical Replication 兩大機制,從設定到 Failover 全流程。

Physical vs Logical Replication

PostgreSQL 提供兩種截然不同的複寫機制,各有適用場景:

Physical Replication(物理複寫):
  Primary → WAL stream (binary) → Standby
  • 複寫整個叢集(byte-for-byte)
  • Standby 只讀
  • DDL 自動同步
  • 延遲極低(毫秒級)

Logical Replication(邏輯複寫):
  Publisher → Decoded change stream (rows) → Subscriber
  • 可選擇特定 table / 操作
  • Subscriber 可讀寫
  • DDL 不自動複寫
  • 支援跨版本、跨平台
面向PhysicalLogical
複寫單位WAL 記錄(bytes)行(row)變更
複寫範圍整個叢集可選特定 table
Standby 可寫否(只讀)
DDL 複寫自動不支援
跨版本不支援支援
資料過濾不支援Row Filter(PG15+)
Failover簡單(pg_promote)複雜
典型用途HA / DR / 讀取擴展版本升級 / 跨平台整合

WAL 在複寫中的角色

WAL(Write-Ahead Log)是所有複寫機制的基礎。Physical Replication 直接傳送 WAL 記錄,Logical Replication 則先透過 Logical Decoding 解碼為行級變更再傳送:

Primary:
  Transaction COMMIT
      │
      ▼
  生成 WAL 記錄
      │
      ▼
  WAL Sender process
      │ Physical → 直接傳送 WAL 記錄
      │ Logical  → 先解碼,再傳送行級變更
      ▼
  Network → Standby WAL Receiver

LSN(Log Sequence Number)追蹤進度:
  sent_lsn    → 已傳送到的位置
  write_lsn   → Standby 已寫入的位置
  flush_lsn   → Standby 已 fsync 的位置
  replay_lsn  → Standby 已重放的位置

Timeline 概念

Timeline 是 PostgreSQL 避免資料分歧(split-brain)的機制,類似版本控制的分支:

Timeline 1:
  ──●──●──●──●──●── (Primary 正常運作)
                 │ Failover(Primary 宕機)
                 ▼
Timeline 2:
                 ──●──●──●──●── (Standby 晉升為新 Primary)

每個 WAL segment 檔名包含 Timeline ID:
  000000010000000100000001  → Timeline 1
  000000020000000100000005  → Timeline 2(Failover 後)

Streaming Replication 設定

Streaming Replication 是 Physical Replication 的主要實現方式,延遲通常在毫秒級別。

Primary 端設定

# postgresql.conf(Primary)
wal_level = replica          -- 至少 replica(Logical 需要 logical)
max_wal_senders = 10         -- 每個 Standby 佔用一個
wal_keep_size = 1024         -- 保留的 WAL 量(MB),PG13+
hot_standby = on             -- 允許 Standby 接受唯讀查詢
listen_addresses = '*'
# pg_hba.conf(Primary)
# TYPE  DATABASE    USER         ADDRESS          METHOD
host    replication replicator   192.168.1.0/24   scram-sha-256
-- 建立複寫專用使用者
CREATE ROLE replicator
  LOGIN
  REPLICATION
  PASSWORD 'strong_password_here';

用 pg_basebackup 建立 Standby

# 在 Standby 伺服器上執行
# 清空 Standby 資料目錄
rm -rf /var/lib/postgresql/data/*

# 執行 pg_basebackup
pg_basebackup \
  --host=192.168.1.10 \
  --port=5432 \
  --username=replicator \
  --pgdata=/var/lib/postgresql/data \
  --wal-method=stream \
  --write-recovery-conf \
  --checkpoint=fast \
  --progress \
  --verbose

--write-recovery-conf 會自動建立 standby.signal 並寫入 postgresql.auto.conf。若需手動設定:

# 建立 standby.signal(PG12+)
touch /var/lib/postgresql/data/standby.signal
# postgresql.conf(Standby)
primary_conninfo = 'host=192.168.1.10 port=5432 user=replicator password=strong_password_here application_name=standby1'
primary_slot_name = 'standby1_slot'
recovery_target_timeline = 'latest'

啟動並驗證

# 啟動 Standby
pg_ctl start -D /var/lib/postgresql/data

# 確認正在接收 WAL
tail -f /var/lib/postgresql/data/log/postgresql-*.log
# 應看到:LOG: started streaming WAL from primary at 0/4000000 on timeline 1

在 Primary 查看複寫狀態:

-- 查看所有 Standby 狀態
SELECT
    application_name,
    client_addr,
    state,
    pg_size_pretty(pg_wal_lsn_diff(sent_lsn, replay_lsn)) AS replication_lag,
    replay_lag,
    sync_state
FROM pg_stat_replication;

在 Standby 確認:

-- 確認 Standby 身份
SELECT pg_is_in_recovery();     -- 應回傳 true

-- 查看接收狀態
SELECT
    pg_last_wal_receive_lsn()  AS received_lsn,
    pg_last_wal_replay_lsn()   AS replayed_lsn,
    EXTRACT(EPOCH FROM (now() - pg_last_xact_replay_timestamp()))
        AS lag_seconds;

同步 vs 非同步複寫

非同步(預設)

Primary 的 COMMIT 在 WAL 寫入本地磁碟後立即返回,不等待 Standby 確認。適用於效能優先、跨機房(高 RTT)場景。

同步複寫

Primary 的 COMMIT 必須等到指定 Standby 確認後才返回:

# postgresql.conf(Primary)

# 等待名為 standby1 的確認
synchronous_standby_names = 'standby1'

# FIRST 語法:等待前 N 個最快確認的
synchronous_standby_names = 'FIRST 1 (standby1, standby2, standby3)'

# ANY 語法:等待任意 N 個確認(更高容錯)
synchronous_standby_names = 'ANY 2 (standby1, standby2, standby3)'

同步確認級別可在 session 或 transaction 層級調整:

-- 最強:等待 Standby 已 apply
SET synchronous_commit = remote_apply;

-- 等待 Standby 已寫入 OS 緩衝(但未 fsync)
SET synchronous_commit = remote_write;

-- 只等 Primary 本地 flush,忽略 Standby
SET synchronous_commit = local;

-- 針對單一 transaction
BEGIN;
SET LOCAL synchronous_commit = remote_apply;
UPDATE orders SET status = 'confirmed' WHERE id = 42;
COMMIT;
synchronous_commit零資料遺失效能代價
off最高(無額外等待)
local
on(預設)中(1x RTT)
remote_write接近是
remote_apply低(需 Standby apply)

Replication Slots

Replication Slot 確保 WAL 不被過早清除——只要 Slot 存在,Primary 就不會刪除 consumer 尚未消費的 WAL。

-- 建立 Physical Replication Slot
SELECT pg_create_physical_replication_slot('standby1_slot');

-- 建立 Logical Replication Slot
SELECT pg_create_logical_replication_slot('my_logical_slot', 'pgoutput');

-- 查看所有 Slot 狀態
SELECT
    slot_name,
    slot_type,
    active,
    pg_size_pretty(
        pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)
    ) AS retained_wal
FROM pg_replication_slots;

-- 刪除 Slot
SELECT pg_drop_replication_slot('standby1_slot');

防止 WAL 暴增

# postgresql.conf(PG13+)
# 限制 Slot 可保留的最大 WAL
# 超過限制,Slot 會被 invalidate
max_slot_wal_keep_size = 10GB

監控 WAL 積壓:

SELECT
    slot_name,
    active,
    pg_size_pretty(
        pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)
    ) AS wal_lag,
    CASE
        WHEN pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) > 5*1024^3
        THEN 'WARNING: >5GB'
        ELSE 'OK'
    END AS status
FROM pg_replication_slots
ORDER BY pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) DESC NULLS LAST;

Cascading Replication

允許 Standby 再向下游提供 WAL 串流,減輕 Primary 負擔:

Primary(主機房)
    │ WAL stream
    ▼
Standby A(主機房,同步)
    │ WAL stream(A 作為中繼)
    ▼
Standby B(DR 機房,非同步)
    ├──► Standby C(報表用)
    └──► Standby D(備份用)

設定方式與一般 Standby 相同,只是 primary_conninfo 指向上游 Standby 而非 Primary。


Logical Replication

Logical Replication 在行(row)層級傳送變更,適用於跨版本升級、部分表格複寫、雙向複寫等場景。

建立 Publication

-- 在 Publisher 端

-- 複寫特定 table
CREATE PUBLICATION pub_orders FOR TABLE orders, order_items;

-- 複寫整個資料庫的所有 table
CREATE PUBLICATION pub_all FOR ALL TABLES;

-- 只複寫 INSERT 操作
CREATE PUBLICATION pub_insert_only
FOR TABLE audit_log
WITH (publish = 'insert');

-- PG15+:複寫特定欄位
CREATE PUBLICATION pub_partial
FOR TABLE customers (id, email, updated_at);

-- PG15+:Row Filter(只複寫符合條件的資料)
CREATE PUBLICATION pub_active_users
FOR TABLE users WHERE (status = 'active');

-- 查看所有 Publication
SELECT * FROM pg_publication;
SELECT * FROM pg_publication_tables;

建立 Subscription

-- 在 Subscriber 端(需事先建立相同結構的 table)
-- DDL 不自動複寫,可用 pg_dump --schema-only 匯出 schema

-- 建立 Subscription
CREATE SUBSCRIPTION sub_orders
CONNECTION 'host=publisher-ip port=5432 dbname=mydb user=replicator password=xxx'
PUBLICATION pub_orders;

-- 不做初始資料同步(資料已透過其他方式同步)
CREATE SUBSCRIPTION sub_orders
CONNECTION 'host=publisher-ip ...'
PUBLICATION pub_orders
WITH (copy_data = false);

-- 查看 Subscription 狀態
SELECT subname, subenabled, subslotname, subpublications
FROM pg_subscription;

-- 查看 per-table 同步狀態
SELECT * FROM pg_subscription_rel;
-- srsubstate: i=初始化, d=資料複製中, s=已同步, r=正常複寫

-- 管理 Subscription
ALTER SUBSCRIPTION sub_orders DISABLE;    -- 暫停
ALTER SUBSCRIPTION sub_orders ENABLE;     -- 恢復
ALTER SUBSCRIPTION sub_orders REFRESH PUBLICATION;  -- 更新 table 清單
DROP SUBSCRIPTION sub_orders;             -- 刪除

重要限制

  1. DDL 不複寫:ALTER TABLE、CREATE INDEX 等需在兩端手動執行
  2. Sequence 不同步:Failover 後需手動調整 setval('seq', MAX(id) + 1000)
  3. Large Object 不支援:改用 bytea 欄位
  4. 需要 REPLICA IDENTITY:UPDATE/DELETE 需要主鍵或唯一鍵
-- 若 table 無主鍵,需設定 REPLICA IDENTITY
ALTER TABLE mytable REPLICA IDENTITY FULL;  -- 使用所有欄位(效能影響大)
-- 或使用唯一索引
ALTER TABLE mytable REPLICA IDENTITY USING INDEX mytable_unique_idx;

Failover / Switchover

Failover:Primary 意外宕機,強制晉升 Standby(可能有少量資料遺失)。 Switchover:計劃性切換,確認同步後再切換(零資料遺失)。

pg_promote()(PG12+)

-- 在 Standby 上執行
SELECT pg_promote(wait => true, wait_seconds => 60);
-- 回傳 true 表示晉升成功

-- 確認晉升完成
SELECT pg_is_in_recovery();   -- 應回傳 false

計劃性 Switchover 步驟

# 1. 確認 Standby 已同步(lag 接近 0)
psql -c "SELECT application_name, replay_lag FROM pg_stat_replication;"

# 2. 強制 checkpoint
psql -c "CHECKPOINT;"

# 3. 確認 Standby LSN 與 Primary 一致
# Primary:
psql -c "SELECT pg_current_wal_lsn();"
# Standby:
psql -h standby-ip -c "SELECT pg_last_wal_replay_lsn();"

# 4. 優雅關閉 Primary
pg_ctl stop -D /var/lib/postgresql/data -m fast

# 5. 晉升 Standby
psql -h standby-ip -c "SELECT pg_promote();"

# 6. 更新應用程式連線字串

# 7. 將舊 Primary 設為新 Standby(使用 pg_rewind)
pg_rewind \
  --target-pgdata=/var/lib/postgresql/data \
  --source-server='host=new-primary-ip port=5432 user=replicator' \
  --progress
touch /var/lib/postgresql/data/standby.signal
pg_ctl start -D /var/lib/postgresql/data

緊急 Failover

# 1. 確認 Primary 確實無法服務(避免 split-brain)
# 2. 晉升最接近 Primary 的 Standby
pg_ctl promote -D /var/lib/postgresql/standby_data
# 3. 更新 DNS / Load Balancer
# 4. 通知其他 Standby 切換到新 Primary
psql -h other-standby -c "ALTER SYSTEM SET primary_conninfo = 'host=new-primary-ip ...';"
psql -h other-standby -c "SELECT pg_reload_conf();"
# 5. 嘗試用 pg_rewind 恢復舊 Primary

Logical Decoding 與 CDC

Logical Decoding 是 Logical Replication 的底層機制,允許外部程式讀取 WAL 的解碼輸出,實現 CDC(Change Data Capture):

WAL records (binary)
    │
    ▼
Output Plugin(解碼器)
    ├── pgoutput(內建)
    ├── wal2json(JSON 輸出)
    └── decoderbufs(Protocol Buffers)
    │
    ▼
Change stream → Consumer
    ├── Logical Replication Subscriber
    ├── Debezium → Kafka
    └── 自訂應用程式

使用 wal2json

-- 建立 Logical Slot
SELECT pg_create_logical_replication_slot('cdc_slot', 'wal2json');

-- 讀取變更(不消費:peek)
SELECT * FROM pg_logical_slot_peek_changes('cdc_slot', NULL, NULL,
    'pretty-print', '1',
    'include-timestamp', '1'
);

-- 讀取並消費(consume)
SELECT * FROM pg_logical_slot_get_changes('cdc_slot', NULL, NULL,
    'pretty-print', '1'
);

-- 輸出範例(JSON):
-- {"change":[
--   {"kind":"insert","schema":"public","table":"orders",
--    "columnnames":["id","customer","amount"],
--    "columnvalues":[101,"Alice",599.99]}
-- ]}

常見陷阱

1. Replication Slot WAL 積壓(最危險)

Standby 斷線後 Slot 持續累積 WAL,可能撐爆 Primary 磁碟:

# 設定 max_slot_wal_keep_size 防止暴增
max_slot_wal_keep_size = 10GB
# 搭配監控告警:WAL 積壓 > 可用空間 50% 時告警

2. 同步 Standby 全部離線導致 Primary 掛起

所有同步 Standby 斷線時,Primary 的所有 COMMIT 都會無限等待:

# 使用 ANY 語法提高容錯
synchronous_standby_names = 'ANY 1 (s1, s2, s3)'
# 準備緊急回退:SET synchronous_standby_names = ''

3. Timeline 不匹配

Failover 後舊 Primary 重啟可能產生衝突:

# Standby 設定自動追蹤最新 Timeline
recovery_target_timeline = 'latest'
# 舊 Primary 使用 pg_rewind 回到正確歷史

4. hot_standby_feedback 未啟用

Standby 上的長查詢被 Primary 的 VACUUM 取消:

# 開啟回饋機制
hot_standby_feedback = on
max_standby_streaming_delay = 30s

5. Logical Replication 的 Sequence 陷阱

Failover 到 Subscriber 後,sequence 值可能衝突:

-- 晉升 Subscriber 前,手動推進 sequence
SELECT setval('orders_id_seq', (SELECT MAX(id) FROM orders) + 1000);

版本演進

版本複寫相關新功能
PG 9.0Streaming Replication、Hot Standby
PG 9.1Synchronous Replication
PG 9.2Cascading Replication
PG 9.4Replication Slots、Logical Decoding
PG 10Logical Replication(Publication/Subscription)
PG 12pg_promote()、standby.signal 取代 recovery.conf
PG 13wal_keep_size 取代 wal_keep_segments;max_slot_wal_keep_size
PG 15Row Filter / Column Filter for Publication
PG 16Standby 可做 pg_basebackup
PG 17Logical Replication Failover Slots;pg_createsubscriber

總結

複寫是 PostgreSQL 高可用架構的根基。關鍵要點:

  1. Physical Replication 適合 HA/DR,設定簡單、延遲極低
  2. Logical Replication 適合跨版本升級、部分表格同步、CDC 整合
  3. 同步複寫確保零資料遺失,但有效能代價——用 ANY 語法提高容錯
  4. Replication Slot 防止 WAL 被清除,但必須搭配 max_slot_wal_keep_size 監控
  5. Failover 使用 pg_promote()Switchoverpg_rewind 讓舊 Primary 回歸
  6. 善用 Cascading Replication 減輕 Primary 負擔

下一篇,我們將深入探討 高可用架構方案——掌握 Patroni 自動 Failover、HAProxy 負載均衡與 etcd 共識機制,打造真正的零停機 PostgreSQL 叢集。

BenZ Software Developer

熱愛技術的軟體開發者,在這裡分享程式開發經驗與學習筆記。

本週主打

AI 自動化入門包

你每天手動在做的那些煩事,其實 AI 可以自己跑。這份給你 10 個照著做就會的自動化工作流 + 50 個複製即用的提示詞,不用會寫程式。

看看這個產品 →