Replication 與 High Availability
介紹 PostgreSQL 的 Streaming Replication、Logical Replication、以及 Patroni / PgBouncer 組成的生產級 HA 架構
一套 WAL 驅動兩種複製
PostgreSQL 的複製建立在 WAL 之上,分兩種:
| 類型 | 傳輸內容 | Standby 可寫? | 跨版本? | 選擇性複製? |
|---|---|---|---|---|
| Streaming Replication | WAL(物理) | ✗ | ✗ | ✗(全庫) |
| Logical Replication | 邏輯變更(row-level) | ✓ | ✓ | ✓(per-table) |
Streaming Replication 是主流的 HA 方案;Logical Replication 用於跨版本升級、部分表同步、資料分發等場景。
Streaming Replication
架構

上圖展示 Streaming Replication 的完整架構。Primary 端的 WAL Sender 程序透過 TCP 串流把 WAL 物理日誌即時傳送給 Standby;Standby 端的 WAL Receiver 接收 WAL 並寫入本地磁碟,Startup Process 持續重播 WAL 使 Standby 保持最新狀態。開啟 hot_standby = on 後 Standby 可同時接受唯讀查詢(但 WAL 重播可能 cancel 正在執行的查詢)。
圖的下方列出四種同步模式:Async(預設,只等本地 fsync)、Remote Write(等 Standby 收到 WAL)、Remote Flush(等 Standby fsync)、Remote Apply(等 Standby 重播完,讀 Standby 保證一致)。
和 MySQL 的主從複製概念類似,但傳輸的是 WAL(物理日誌)而非 Binlog(邏輯日誌)。
設定
Primary 端:
# postgresql.conf
wal_level = replica # 必須 >= replica
max_wal_senders = 10 # 最大 WAL sender 數
wal_keep_size = 1GB # 保留的 WAL 最小量(防 standby 來不及拉)
# pg_hba.conf — 允許 replication 連線
# host replication replicator 10.0.0.0/24 scram-sha-256
Standby 端:
# postgresql.conf
primary_conninfo = 'host=primary port=5432 user=replicator password=xxx'
hot_standby = on # 允許 Standby 處理唯讀查詢
同步模式
| 模式 | synchronous_commit 設定 | Primary COMMIT 等什麼 | 資料安全 |
|---|---|---|---|
| Async(預設) | on | 只等本地 WAL fsync | Standby 可能落後 |
| Remote Write | remote_write | 等 Standby 收到 WAL | 網路斷不丟 |
| Remote Flush | on + synchronous_standby_names | 等 Standby fsync WAL | 幾乎零丟失 |
| Remote Apply | remote_apply | 等 Standby 重播完 WAL | 讀 Standby 一致 |
# 指定同步 standby(支援 quorum)
synchronous_standby_names = 'FIRST 1 (standby1, standby2)'
# 或 quorum:ANY 1 (standby1, standby2)
Hot Standby
Standby 在重播 WAL 的同時可以接受唯讀查詢(hot_standby = on)。這是讀寫分離的基礎。
但要注意 replication conflict:Standby 上的查詢可能因為 WAL 重播需要清理它正在讀的 tuple 而被 cancel。用 max_standby_streaming_delay(預設 30 秒)控制容忍度——超過就 cancel query 讓 WAL 重播繼續。
與 MySQL 主從複製的差異
| 面向 | PostgreSQL Streaming | MySQL Replication |
|---|---|---|
| 傳輸內容 | WAL(物理 page diff) | Binlog(邏輯 SQL/row) |
| 一致性 | byte-level 一致 | 邏輯一致 |
| Standby 可寫 | 不可(物理副本) | 不可(但可設為 read-only) |
| 並行重播 | PG 15+ 有改善 | 8.0 WRITESET 做得較好 |
| 跨版本 | 不行 | 大版本間有時可以 |
Logical Replication
PostgreSQL 10 引入。把 WAL 中的物理變更解碼成邏輯的行變更(INSERT/UPDATE/DELETE),再傳給 subscriber。
架構

上圖展示 Logical Replication 的資料流:Publisher 端的 Logical Decoding 模組將 WAL 中的物理變更解碼成邏輯的行變更(INSERT/UPDATE/DELETE),透過網路傳送給 Subscriber;Subscriber 端的 Logical Replication Worker 接收行變更並 apply 到本地表。與 Streaming Replication 不同,Subscriber 的表是可讀可寫的獨立表。
圖的下方整理了適用場景(跨版本升級、部分表同步、多源匯聚、跨雲同步)和限制(不複製 DDL、不複製 sequence、初始同步慢、衝突需手動處理)。
設定
-- Publisher 端
-- postgresql.conf: wal_level = logical
CREATE PUBLICATION my_pub FOR TABLE orders, users;
-- 或 FOR ALL TABLES
-- Subscriber 端
CREATE SUBSCRIPTION my_sub
CONNECTION 'host=publisher port=5432 dbname=mydb user=replicator'
PUBLICATION my_pub;
適用場景
- 跨大版本升級:PG 14 → PG 16,用 logical replication 做 zero-downtime migration
- 部分表同步:只複製需要的表到分析用的副本
- 多個來源匯聚:多個 publisher 的資料匯到一個 subscriber
- 跨雲同步:不同雲的 PostgreSQL 之間
限制
- 不複製 DDL(需要手動同步 schema)
- 不複製 sequence 值
- 大交易的初始同步(initial table sync)可能很慢
- 衝突需要手動處理(subscriber 上也可以寫,衝突時 subscription 會暫停)
生產級 HA 架構
Patroni + etcd + PgBouncer

這是目前 PostgreSQL 社群最推薦的 HA 方案。上圖展示完整架構:
- 最上層:Application 連接到 PgBouncer(連線池),PgBouncer 負責連線池管理(200 客戶端 → 20 backends)和讀寫分離(寫走 Leader、讀走 Replica)
- 中間層:三個 PostgreSQL 節點,每個都跑一個 Patroni Agent。Patroni 管理 PostgreSQL 的生命週期、監控健康狀態、處理 Streaming Replication 設定。Leader 負責讀寫,Replica 透過 WAL 保持同步並提供唯讀查詢
- 底層:etcd Cluster 做分散式共識。Patroni Leader 持續 renew etcd 中的 leader key(TTL 機制),一旦停止 renew,其他 Patroni 就會發起 leader election
圖的底部展示 Failover 流程:leader key TTL 到期 → Replica 贏得選舉 → pg_promote() 升為 Primary → 其他 Replica 重新指向 → PgBouncer callback 更新連線,全程約 5-15 秒。
Patroni
Python 寫的 HA agent,跑在每個 PostgreSQL 節點上:
- 透過 etcd/ZooKeeper/Consul 做 leader election
- 監控 PostgreSQL 健康狀態
- Leader 掛了自動 promote replica 為新 leader
- 處理 Streaming Replication 的設定
- 提供 REST API 查詢 cluster 狀態
# patroni.yml 範例(簡化)
scope: my-cluster
name: node1
restapi:
listen: 0.0.0.0:8008
etcd:
hosts: etcd1:2379,etcd2:2379,etcd3:2379
bootstrap:
dcs:
synchronous_mode: true
postgresql:
parameters:
max_connections: 200
shared_buffers: 4GB
postgresql:
listen: 0.0.0.0:5432
data_dir: /var/lib/postgresql/data
replication:
username: replicator
password: xxx
PgBouncer
負責:
- 連線池:200 個應用連線 → 20 個 PostgreSQL backend
- 讀寫分離:寫走 leader、讀走 replica
- 自動切換:配合 Patroni 的 callback,failover 時更新連線指向
Failover 流程
- etcd 的 leader key TTL 到期(Patroni leader 停止 renew)
- 某個 replica 的 Patroni 贏得 leader election
- 該 replica 執行
pg_promote()升為 primary - 其他 replica 重新指向新 primary
- PgBouncer 透過 callback 更新連線
- 應用層短暫中斷(通常 5-15 秒)後恢復
與 MySQL 高可用方案的對比
| 方案 | 適用 | failover 時間 | 資料安全 |
|---|---|---|---|
| PG: Patroni + etcd | 主流 HA | 5-15 秒 | 同步模式下零丟失 |
| PG: pg_auto_failover | 簡單場景 | 秒級 | 同步模式下零丟失 |
| MySQL: InnoDB Cluster | 主流 HA | 秒級 | Paxos 多數決 |
| MySQL: MHA/Orchestrator | 傳統方案 | 秒到分鐘 | 半同步下趨近零 |
監控複製狀態
-- Primary 端:查看所有 standby 的狀態
SELECT client_addr, state, sent_lsn, write_lsn, flush_lsn, replay_lsn,
(sent_lsn - replay_lsn) AS replay_lag_bytes
FROM pg_stat_replication;
-- Standby 端:查看自己的延遲
SELECT now() - pg_last_xact_replay_timestamp() AS replay_lag;
-- Logical Replication:查看 subscription 狀態
SELECT subname, pid, received_lsn, latest_end_lsn
FROM pg_stat_subscription;