跳至内容

20.2 物理流复制

PostgreSQL 物理复制不是“把表同步到另一台机器”,而是让另一套数据目录 持续接收并重放同一个 database cluster 的 WAL 历史。

理解 HA,至少要能回答:

WAL 生成到哪里?
发送到哪里?
备库写到哪里?
flush 到哪里?
replay 到哪里?
当前属于哪条 timeline?
旧 WAL 由谁保留?

这些问题都有 PostgreSQL 原生证据,不必靠角色标签猜。

20.2.1 WAL 发送、接收、重放与 LSN

物理复制传的是 WAL

primary 修改 data page 之前,相关变化先以 WAL record 进入 WAL。physical standby 的基本流水线:

primary backend
  -> WAL insert
      -> WAL buffer / local flush
          -> walsender
              -> network
                  -> walreceiver
                      -> standby WAL write/flush
                          -> recovery process replay
                              -> hot standby query visibility

因此“复制到达”至少有三层:

received
written/flushed
replayed

同步提交等待哪一层,由 synchronous_commit 决定;读请求能否看到,由 replay 位置决定。

LSN 是 WAL 地址

Log Sequence Number 写作:

0/170002B0

可理解为单调推进的 WAL byte position。它不是 wall-clock time,也不是 transaction ID。

常用函数:

-- 只在非 recovery 节点调用
SELECT pg_current_wal_lsn();

-- standby 上最后收到/重放的位置
SELECT pg_last_wal_receive_lsn(),
       pg_last_wal_replay_lsn(),
       pg_last_xact_replay_timestamp();

-- byte difference
SELECT pg_wal_lsn_diff('0/170002B0', '0/17000000');

不要在 standby 无条件调用 pg_current_wal_lsn()。本章 ha-facts.sql 先判断 pg_is_in_recovery(), 再选择 primary 或 standby 合适的函数。

primary 观察 walsender

SELECT application_name,
       client_addr,
       state,
       sync_state,
       sent_lsn,
       write_lsn,
       flush_lsn,
       replay_lsn,
       pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn)
         AS replay_gap_bytes
FROM pg_stat_replication
ORDER BY application_name;

字段的方向:

sent_lsn    primary 已发送
write_lsn   standby 已写到 OS
flush_lsn   standby 已报告 durable flush
replay_lsn  standby 已重放

本章每个稳定 phase 都要求 primary 看到:

exactly two rows
application_name = current two replicas
state = streaming
sync_state = async
client_addr = declared member address
replay_gap_bytes <= 1 MiB

state=streaming 是必要条件,不是业务 freshness 的充分条件。

standby 观察 walreceiver

SELECT status,
       sender_host,
       sender_port,
       written_lsn,
       flushed_lsn,
       latest_end_lsn
FROM pg_stat_wal_receiver;

每个 standby 应看到一个 receiver,且 upstream 地址是当前 primary。在正式 切换中:

timeline 5: pg-test-2/3 receiver -> 10.10.10.11
timeline 6: pg-test-1/3 receiver -> 10.10.10.12
timeline 7: pg-test-2/3 receiver -> 10.10.10.11

这比只看 Patroni 的 Role=Replica 多证明了一层:PostgreSQL 自己确实在 接收当前 upstream。

lag 不是一个数字

至少区分:

transport lag
  generated - received

flush lag
  generated - durable on standby

replay lag
  generated - applied

visibility lag
  commit on primary - visible to standby query

business freshness
  source event time - projection/report watermark

用 byte gap 的优势是明确;局限是业务时间随 WAL generation rate 变化。 用 now() - pg_last_xact_replay_timestamp() 也有陷阱:没有新事务时, timestamp 看起来越来越“旧”,并不代表 standby 落后。

正确做法是组合:

sender/receiver state
LSN byte gaps
replay timestamp with workload context
WAL generation rate
application watermark where needed

replay 与查询冲突

hot standby 一边 replay,一边允许只读查询。长查询可能与 recovery 需要 清理的 tuple、DDL 或锁冲突。系统必须在:

cancel standby query
delay WAL replay
retain more dead rows on primary via feedback

之间取舍。HA replica 若同时承担分析负载,可能让 replay lag、bloat 与 failover eligibility 互相影响。

本章把 pg-test-3 标为 offline-query placement intent,但不会把标签当成 已证明的 workload isolation。

观测快照不是连续保证

pg_stat_replication 是当前状态。采到 gap=0 只能说明采样点:

the replica caught up at observation time

它不证明上一秒、下一秒或 primary 永久损失时也为零。本章 planned switchover 会选择健康 candidate;这正是它不能代替 catastrophic failover RPO 测试的原因。

20.2.2 timeline、恢复目标与历史分叉

promotion 会创建新 timeline

standby promotion 的本质不是“改角色字段”,而是从共同 WAL 历史的某个点 开始一条新分支。

timeline 5  ------ A ------ promotion
                              \
timeline 6                     B ------ C

如果旧 primary 在分支点后也继续写:

timeline 5                     X ------ Y

B/CX/Y 可能都是各自内部合法的事务,但无法通过普通流复制自动合并。 这就是为什么 authority 与 fencing 比“选主速度”更重要。

system identifier 与 timeline

两个身份不要混:

system identifier
  initdb 时产生,标识一个 PostgreSQL database cluster lineage

timeline ID
  标识该 lineage 中一次 WAL history 分支

本章正式证据:

one unchanged system identifier
timeline 5 -> 6 -> 7

如果成员 system identifier 不同,它不是这个 physical cluster 的合法 replica;如果 system identifier 相同但 timeline 关系不对,则必须检查 history 与分叉。

原生证据:

SELECT system_identifier
FROM pg_control_system();

SELECT timeline_id
FROM pg_control_checkpoint();

第二条查询有一个重要边界,后面单独解释。

timeline history

promotion 会产生 timeline history 文件,描述新 timeline 从哪条父 timeline 的哪个 WAL 位置分叉。recovery 要选择正确 history。

HA standby 通常使用:

recovery_target_timeline = latest

这是 PostgreSQL 默认值,使其能跟随 promotion 后的最新历史。官方 warm standby 文档 明确建议 HA 多 standby 使用 latest。

“latest”不是允许随便选择历史。它仍依赖可达的 archive/stream、history 文件和一个被授权的 upstream。

从 WAL 文件名得到 primary 当前 timeline

WAL 文件名的前 8 个十六进制字符编码 timeline。primary 可以:

SELECT split.timeline_id
FROM pg_split_walfile_name(
       pg_walfile_name(pg_current_wal_lsn())
     ) AS split;

本章把它保存为:

current_wal_timeline_id

并要求它与 Patroni 当前 timeline 一致。

不要在 recovery 中调用 pg_walfile_name(pg_current_wal_lsn());这些 current-WAL 函数不是 standby 当前 replay timeline 的通用接口。

checkpoint timeline 不是 standby 当前 replay timeline

第一次真实演练暴露了一个非常有价值的测量陷阱:

Patroni: pg-test-3 streaming on timeline 3
pg_control_checkpoint().timeline_id on pg-test-3: 1

最终正式运行结束后:

Patroni current timeline: 7
pg-test-3 checkpoint_timeline_id: 3
receiver: streaming from current primary

没有发生“备库卡在旧 timeline”。pg_control_checkpoint() 返回 control file 中最近 checkpoint 的信息;一个 standby 可能已经重放后续 timeline, 但还没有用新的 checkpoint metadata 更新到相同数值。

因此证据模型使用精确名称:

checkpoint_timeline_id

验收只要求:

[ checkpoint_timeline_id \le current\ phase\ timeline ]

而不是错误地要求 standby checkpoint timeline 必须时时等于 Patroni timeline。

这条经验说明:

观测函数名、状态生命周期和适用节点不清楚时,“更多 SQL”也会产生错误 告警。

如何验证 standby 跟随正确历史

组合证据:

Patroni member timeline and state
standby pg_is_in_recovery() = true
pg_stat_wal_receiver.status = streaming
sender_host = current primary
receive/replay LSN advances
primary walsender sees that application
system identifier unchanged

若需要进一步诊断,可检查:

timeline history files
PostgreSQL recovery logs
archive availability
Patroni logs and DCS history

不要用一个 checkpoint 字段替代整个 lineage proof。

promotion 是不可逆状态迁移

promotion 后,新 primary 会产生新 timeline。要把它“变回原样”,不是把 配置里的 role 改回 replica:

  • 若未产生分叉写入且工具能安全处理,仍需验证;
  • 常见路径是 pg_rewind 对齐;
  • rewind 前提不满足或失败时,从新 base backup 重建;
  • 任何时候都必须先确认唯一的 source of truth。

本章的第二次 switchover 又创建 timeline 7;这是恢复“教学角色基线”, 不是把 WAL 历史倒回 timeline 5。

20.2.3 复制槽、归档与 WAL 保留

standby 必须拿到连续 WAL

standby 能继续 recovery 的前提:

从自己的起点到当前目标之间,没有缺失所需 WAL

WAL 来源可以组合:

streaming from primary
local pg_wal
WAL archive via restore_command

如果旧 WAL 已在 primary 被 recycle,archive 也没有,而 standby 仍需要:

reinitialize from a new base backup

三种主要保留机制

机制依据优势风险/局限
wal_keep_size至少保留一段量简单不是按 consumer 精确
physical slot按 consumer restart LSN精确追踪需要consumer 卡住可撑满磁盘
WAL archive外部历史catch-up/PITR 共用需要独立完整性与恢复验证

它们不是互斥。一个成熟系统可能同时:

slot protects online replica
archive protects longer recovery history
wal_keep_size absorbs transient behavior

复制槽的保证与反噬

physical replication slot 告诉 primary:

在 consumer 确认之前,不要移除它仍需要的 WAL

原生查询:

SELECT slot_name,
       slot_type,
       active,
       restart_lsn,
       wal_status,
       safe_wal_size
FROM pg_replication_slots
ORDER BY slot_name;

本章每个 stable primary 都要求两个 active physical slot:

pg-test-1 primary -> pg_test_2, pg_test_3
pg-test-2 primary -> pg_test_1, pg_test_3

slot 名使用下划线,因为 PostgreSQL slot 名只允许特定小写字符集合。

但 slot 的保护方式是不删 WAL。如果 replica 离线很久、网络断开或 consumer 永远不回来:

retained WAL grows
pg_wal filesystem fills
primary can stop accepting writes

官方文档明确警告这一风险,并提供 max_slot_wal_keep_size 作为边界之一。 边界达到后,slot 可能失去可继续恢复所需的 WAL,运维必须在“磁盘安全”和 “无需重建 replica”之间明确取舍。

slot 监控

至少观测:

active
restart_lsn
current_lsn - restart_lsn
wal_status
safe_wal_size
inactive_since where available
pg_wal filesystem used/free
WAL generation rate
consumer identity and owner

告警不能只在 disk 95% 才触发。需要提前估算:

[ time\ to\ full

\frac{free\ bytes}{WAL\ generation\ bytes/s} ]

并考虑 burst、checkpoint、backup 与其他 slot。

archive 是另一条恢复路径

启用:

archive_mode=on

只说明 PostgreSQL 会尝试归档。还要检查:

archive_command/library result
last success and last failure
repository independence
retention
timeline history files
restore_command
end-to-end restore

pg_stat_archiver.failed_count 是累计量;看到历史失败不能直接判定当前坏, 也不能因为最近一次成功就忽略趋势。第 21 章会从 archive 到 restore 闭环。

slot 不是 archive,archive 不是 backup proof

slot
  primary-side retention promise for a replication consumer

archive
  copied WAL history

base backup + archive + tested restore
  才可能形成可用 recovery chain

slot 会随 primary 故障域一起消失;archive 若也在同一主机/账户,就可能 共同消失。

planned switchover 前检查

candidate eligibility 最低检查:

member state = streaming
replay gap inside declared bound
system identifier same
timeline eligible
archive/slot not in dangerous state
candidate not tagged nofailover
no paused HA control
service/drain/maintenance authority ready

本章 executable 将 replay gap 上限固定为 1 MiB,并检查两个 sender、 两个 active slot 与 receiver upstream。这个 byte bound 只用于健康计划 切换 preflight,不等于生产 RPO。

诊断顺序

发现 replica lag:

  1. primary 是否继续生成大量 WAL;
  2. walsender sent/write/flush/replay 哪一段拉开;
  3. standby receiver 是否 streaming、upstream 是否正确;
  4. network throughput/error;
  5. standby disk write 与 recovery apply;
  6. replay conflict/long query;
  7. slot retention 与 pg_wal headroom;
  8. archive 是否能补缺;
  9. 是否已越过必须 rebuild 的 stop line。

不要先 drop slot 或删 pg_wal。“释放空间”的错误动作可能直接删除唯一 可恢复路径。

本节实验查询

在合适的节点使用:

SELECT pg_is_in_recovery();
SELECT * FROM pg_stat_replication;
SELECT * FROM pg_stat_wal_receiver;
SELECT * FROM pg_replication_slots;
SELECT * FROM pg_stat_archiver;
SELECT * FROM pg_control_system();
SELECT * FROM pg_control_checkpoint();

公开实验用一个 allowlisted JSON 查询封装这些证据:

不要把复制 credential、primary_conninfo password 或完整 Patroni config 复制进 evidence。

小结

physical replication follows WAL
receive != flush != replay
LSN byte gap != business time
system identifier != timeline
checkpoint timeline != current standby replay timeline
slots protect consumers by retaining WAL
retained WAL can exhaust primary storage
archive claims require restore proof

权威参考


上一节:从失败模型设计高可用 · 返回本章目录 · 下一节:同步策略与提交语义 · 查看全书目录 · 查看索引中心

最后更新于