PostgreSQL Logical Replication + Debezium CDC:replication slot × failure × recovery 對照
這篇涵蓋 logical replication 與 Debezium CDC 共用的 replication slot 生命週期,以及 consumer lag、zombie slot、schema change 斷流、initial COPY、replay storm 這些 production failure mode 跟 recovery 的對照。
Replication slot × Failure × Recovery 對照
Logical replication 跟 Debezium CDC 的 production 議題集中在 replication slot — 它是 PostgreSQL 內保證 WAL 不被回收的 anchor point;slot 設不對、整個 CDC pipeline 失效。各 failure mode 對 slot 的影響跟 recovery 路徑:
| Failure mode | 對 slot 影響 | Primary 端徵兆 | Recovery 路徑 |
|---|---|---|---|
| Consumer 卡住 / lag | slot LSN 不前進、WAL 留著 | pg_wal 目錄持續長大、disk 撐爆 | 修 consumer / 加 throttle / 必要時 drop slot |
| Consumer crash 無 restart | slot 留在 active state | 跟 lag 同、不會自動清 | 手動 SELECT pg_drop_replication_slot('name') |
| Schema change(ADD COLUMN) | 多數 plugin 自動處理、無感 | 通常無感 | - |
| Schema change(DROP / RENAME COLUMN) | 多數 plugin 直接斷 | Consumer log 報錯、slot active 卻不前進 | 重建 publication / 重 init load |
| Initial COPY | slot 建立時跑 snapshot、long-running tx | 大表 COPY 期間鎖跟 WAL 都受影響 | 用 CREATE_REPLICATION_SLOT ... NOEXPORT_SNAPSHOT 分階段 |
| Promotion (failover) | physical slot 跟 logical slot 處理不同 | logical slot 在 PG 16- 不跨 failover | PG 17+ 用 failover 選項把 logical slot 同步到 standby、或 consumer 重 init load |
| Replay storm(offset 重置) | slot 不變、consumer 重讀 | Kafka 端流量爆、application 看 duplicate | Idempotent consumer 設計、或 transactional outbox |
Consumer 卡住、zombie slot、DROP / RENAME COLUMN 斷流、initial COPY 與 replay storm 的配置與 recovery 步驟在〈Production 故障演練〉逐一展開;Promotion 的處理在〈跟 Patroni HA 整合〉。
Logical replication 基礎:publication + subscription + slot
1-- Primary:建 publication
2CREATE PUBLICATION app_changes FOR TABLE orders, events;
3
4-- Subscriber:建 subscription(自動建 replication slot)
5CREATE SUBSCRIPTION app_sub
6 CONNECTION 'host=primary user=replicator dbname=app'
7 PUBLICATION app_changes
8 WITH (slot_name = 'app_sub_slot', copy_data = true);關鍵物件:
- publication(primary 端):宣告 哪些表 + 哪些操作(INSERT/UPDATE/DELETE/TRUNCATE) 對外暴露
- subscription(subscriber 端、若是 PG-to-PG):訂閱 + 自動建 slot + 自動 initial COPY
- replication slot:primary 端、保證 consumer 還沒消費的 WAL 不被回收
copy_data = true 觸發 initial COPY(snapshot)+ 後續 streaming;copy_data = false 只 streaming、適合 already-in-sync 場景。
Debezium CDC:用 logical replication slot 但繞過 subscription
Debezium 是 直接讀 replication slot 的外部 consumer,而不是 PostgreSQL subscriber:
1# Debezium PostgreSQL connector
2connector.class=io.debezium.connector.postgresql.PostgresConnector
3database.hostname=primary
4database.dbname=app
5plugin.name=pgoutput # 內建、PG 10+ 推薦
6slot.name=debezium_app
7publication.name=app_changes
8publication.autocreate.mode=filtered # debezium 自動建 publication
9table.include.list=public.orders,public.events
10snapshot.mode=initial # 起始 snapshot 後 streaming差異:
- Debezium 用
pgoutput(PG 10+ 內建)或wal2json(外掛 plugin)解 WAL、轉成結構化事件送 Kafka - 不像 PG-to-PG subscription、Debezium 沒 subscription object、是 外部 consumer 自管 replication slot
- Failure mode 上 consumer 端是 Debezium 自己、所以 lag 來源是 Debezium 處理速度 / Kafka 寫入速度
Production 故障演練
Consumer lag、slot LSN 不前進、primary disk 爆
徵兆:primary pg_wal 目錄持續長大、df -h 看磁碟 90%+;pg_replication_slots 看 confirmed_flush_lsn 卡在某 LSN、pg_wal_lsn_diff(pg_current_wal_lsn(), confirmed_flush_lsn) 數十 GB。
根因:consumer(Debezium / subscriber)處理慢於 primary 寫入;replication slot 保證 WAL 不回收、但 consumer 沒消費 → WAL 堆積。
修法:
- 監測:Prometheus alert
pg_replication_slot_lag_bytes > 5GB觸發前 catch - 修 consumer:throttle primary 寫入 OR scale Debezium / subscriber 處理能力
- 緊急:
SELECT pg_drop_replication_slot('debezium_app')釋放 WAL — 但 consumer 必須重 init load(資料缺一塊) - 架構:用 max_slot_wal_keep_size(PG 13+)設 slot 能保留 WAL 上限、超出自動 invalidate slot、保護 primary disk
Consumer crash 後 slot 變 zombie
徵兆:Debezium pod OOM crash、新 pod 起來時報 slot is active for PID X、無法 attach;primary 端 pg_replication_slots.active = true、active_pid 指向已經死掉的 process。
根因:PostgreSQL 把 slot 標 active 是基於 當下有 connection;consumer crash 但 connection 沒被 server 端發現(network 沒 RST)、slot 留在 active state。
修法:
1-- 手動清 zombie slot
2SELECT pg_terminate_backend(active_pid) FROM pg_replication_slots
3 WHERE slot_name = 'debezium_app' AND active;
4
5-- 或直接 drop(會丟資料、consumer 要重 init)
6SELECT pg_drop_replication_slot('debezium_app');預防:
- PostgreSQL
tcp_keepalives_idle / interval / count設較短(300 / 60 / 6)、network drop 較快被發現 - Consumer 端用 graceful shutdown +
pg_terminate_backend(active_pid)在 startup 前主動清 stale connection
Schema change(DROP / RENAME COLUMN)斷流
徵兆:Debezium consumer 突然停 produce 訊息、log 報 column XYZ does not exist;primary 端 slot 還 active、但 confirmed_flush_lsn 不前進。
根因:logical decoding 解 WAL 時用的是 那筆變更發生當時的 catalog(historic snapshot),DROP COLUMN 之前的 row event 照樣帶著舊欄位解出來,primary 端的解碼本身不會失敗。斷在 consumer 端:consumer 手上的 table schema 跟事件帶來的欄位對不上。PG-to-PG subscription 可以直接重現:
1-- primary(database postgres)
2CREATE TABLE src (id int PRIMARY KEY, keep text);
3CREATE PUBLICATION pub_src FOR TABLE src;
4-- subscriber(database sub)建同樣的 src 表、訂閱 pub_src
5
6-- primary 改欄名後再寫一筆
7ALTER TABLE src RENAME COLUMN keep TO kept;
8INSERT INTO src VALUES (2, 'b');
9
10-- subscriber 的 apply worker log(反覆重試、每次同一個錯):
11-- ERROR: logical replication target relation "public.src" is missing replicated column: "kept"
12-- subscriber 的 src 停在改名前的資料,primary 上這個 slot 的 confirmed_flush_lsn 不再前進修法:
- 預防:schema change 走 expand-contract pattern
- Phase 1: ADD COLUMN new_col(不影響 logical replication)
- Phase 2: application 雙寫 old + new
- Phase 3: 等 consumer catch up old column 訊息
- Phase 4: DROP COLUMN old_col(此時無 in-flight WAL 帶 old_col)
- 緊急:DROP existing slot、重建 publication 跟 slot、consumer 重 init load
- 長期:Debezium PostgreSQL connector 從 logical decoding plug-in 送來的 event 取得 table schema、沒有 schema history topic,截至 2026-09 的官方文件
snapshot.mode也沒有schema_only_recovery這個值;重建 slot 之後不想重灌既有資料時設snapshot.mode=no_data,connector 只從新 slot 的位置開始 streaming,slot 重建之前的變更不會補送
Initial COPY 大表鎖太久
徵兆:對 1TB 表跑 CREATE SUBSCRIPTION ... WITH (copy_data=true) 後、application 對該表 query / write 阻塞 30+ 分鐘;application timeout 大量。
根因:initial COPY 預設跑在 single transaction、整個 snapshot LSN 鎖住、長 transaction 跟 vacuum 衝突;同時對 subscriber 端鎖表寫入。
修法:
- 分階段 init:
1-- Primary:建 publication 不 copy
2CREATE PUBLICATION app_changes FOR TABLE big_table;
3
4-- Subscriber:建 subscription 不 copy
5CREATE SUBSCRIPTION app_sub
6 CONNECTION '...'
7 PUBLICATION app_changes
8 WITH (copy_data = false);
9
10-- 手動跑 partition-by-partition COPY(若是 partition table)
11-- 或用 pg_dump / pg_basebackup 拿 snapshot- Parallel init:
max_sync_workers_per_subscription(PG 10 起就有、預設 2)調到 4,平行 COPY 多個表 - Debezium replacement:用 incremental snapshot(Debezium 1.6+)、background trickle copy、不鎖長 transaction
Replay storm 後 consumer offset reset
徵兆:Debezium 修 bug / 重 deploy 後、snapshot.mode=initial 觸發整個資料重灌;Kafka topic 流量爆 10x、下游 application 看到大量 duplicate event。
根因:Debezium offset store(Kafka topic 或 file)被誤刪 / corruption;重啟時不知道從哪 LSN 開始、預設 fall back 到 initial snapshot。
修法:
- 預防:Debezium offset store 跟 Kafka cluster backup 一起做、不要單獨依賴 Kafka topic
- 架構:consumer side 設計 idempotent — 用 event 自帶的 (source LSN + transaction ID) 當 dedupe key
- transactional outbox pattern:CDC 只 capture outbox 表、application 主動寫 outbox + business data 在同 transaction;duplicate 由 application 自己 dedupe
容量規劃
| 維度 | 估算 | 警戒 |
|---|---|---|
| Replication slot lag | pg_wal_lsn_diff(pg_current_wal_lsn(), confirmed_flush_lsn) | > 1GB lag 訊號 consumer 跟不上 |
Primary pg_wal size | retention × peak WAL rate | 預留 disk 容量 = max_slot_wal_keep_size + 30% buffer |
| Debezium throughput | ~5-10K row/s 單 connector、多表平行可拉 | 跟 primary write rate 對比 |
| Initial COPY time | 100GB ~ 10-30 分鐘(看 network + subscriber IO) | TB 級必須分階段 |
| Slot 數量 | 每 slot 佔 primary 一份 WAL 保留 buffer | 5+ slot 同時跑 disk 壓力倍增 |
| max_replication_slots | 預設 10、production 跑 CDC + standby 各佔 slot 要拉到 20-50 | 達上限會拒新 slot 建立 |
實務 default:
- Debezium production:1 connector per source schema、不要 1 connector 跨 50 個表
- Slot retention:
max_slot_wal_keep_size = 100GB、超出 invalidate slot 保護 primary - Monitor cadence:1 分鐘 sample lag + 5 分鐘 alert threshold
整合 / 下一步
跟 Patroni HA 整合
logical slot 在 PG 16- 不跨 failover、是長期痛點:
- PG 16-:failover 後 logical consumer 必須重 init(slot 在新 leader 上不存在)
- PG 17+:
failoverparameter 讓 logical slot 在 standby 同步、failover 後 consumer 直接接 - Patroni 16+ 支援 logical slot persistence 配置、配合用
跟 Kafka outbox pattern
production-grade CDC 不直接 read business table、是 read outbox table:
1-- Application transaction
2BEGIN;
3 INSERT INTO orders (...) VALUES (...);
4 INSERT INTO outbox (event_type, payload, created_at) VALUES ('order_created', '...', now());
5COMMIT;Debezium 只 capture outbox table、event payload 已是 application-shaped JSON、不用解 row event。好處:
- Schema change 不影響 CDC(outbox table schema 穩定)
- 跨表 transaction 對應到單 event(outbox 是業務語意層)
- Replay 可靠 — outbox 是 append-only、可重讀
跟 partitioning 整合
partitioned table 的 logical replication:
- PG 13+
publish_via_partition_root = true— publication 從 parent 角度看、不是 per-partition - Subscriber 端可 partition 不同 strategy(甚至不 partition)
- Schema change 對 partition table 更複雜、走 expand-contract 嚴格
下一步議題
- Logical replication conflict:subscriber 端寫衝突的處理(PG 17+ 加 conflict resolution)
- bi-directional replication(pg_active):多 region active-active、衝突解決設計
- Decoder plugin 對比:pgoutput / wal2json / decoderbufs 效能跟易用性
相關連結
- 上游 vendor 頁:PostgreSQL
- 上游 chapter:Schema Migration Rollout Evidence — schema change × CDC 對應
- PostgreSQL 的其他主題:Patroni HA / PITR + WAL Archiving / Replication Slot Management(slot lifecycle / orphan / failover sync)/ Replication Topology(streaming + LSN 基礎)
- Methodology:Vendor 深度技術文章的寫作方法論
#backend #database #postgresql #logical-replication #debezium #cdc #deep-article