# 物理流复制

LLMS 索引： [llms.txt](/llms.txt)

---

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

理解 HA，至少要能回答：

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

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

## 20.2.1 WAL 发送、接收、重放与 LSN {#item-20-2-1}

### 物理复制传的是 WAL

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

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

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

```text
received
written/flushed
replayed
```

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

### LSN 是 WAL 地址

Log Sequence Number 写作：

```text
0/170002B0
```

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

常用函数：

```sql
-- 只在非 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`](/labs/ch20/ha-facts.sql) 先判断 `pg_is_in_recovery()`，
再选择 primary 或 standby 合适的函数。

### primary 观察 walsender

```sql
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;
```

字段的方向：

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

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

```text
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

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

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

```text
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 不是一个数字

至少区分：

```text
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 落后。

正确做法是组合：

```text
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 或锁冲突。系统必须在：

```text
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 只能说明采样点：

```text
the replica caught up at observation time
```

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

## 20.2.2 timeline、恢复目标与历史分叉 {#item-20-2-2}

### promotion 会创建新 timeline

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

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

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

```text
timeline 5                     X ------ Y
```

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

### system identifier 与 timeline

两个身份不要混：

```text
system identifier
  initdb 时产生，标识一个 PostgreSQL database cluster lineage

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

本章正式证据：

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

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

原生证据：

```sql
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 通常使用：

```text
recovery_target_timeline = latest
```

这是 PostgreSQL 默认值，使其能跟随 promotion 后的最新历史。官方
[warm standby 文档](https://www.postgresql.org/docs/18/warm-standby.html)
明确建议 HA 多 standby 使用 latest。

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

### 从 WAL 文件名得到 primary 当前 timeline

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

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

本章把它保存为：

```text
current_wal_timeline_id
```

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

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

### checkpoint timeline 不是 standby 当前 replay timeline

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

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

最终正式运行结束后：

```text
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 更新到相同数值。

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

```text
checkpoint_timeline_id
```

验收只要求：

\[
checkpoint\_timeline\_id \le current\ phase\ timeline
\]

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

这条经验说明：

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

### 如何验证 standby 跟随正确历史

组合证据：

```text
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
```

若需要进一步诊断，可检查：

```text
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 保留 {#item-20-2-3}

### standby 必须拿到连续 WAL

standby 能继续 recovery 的前提：

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

WAL 来源可以组合：

```text
streaming from primary
local pg_wal
WAL archive via restore_command
```

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

```text
reinitialize from a new base backup
```

### 三种主要保留机制

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

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

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

### 复制槽的保证与反噬

physical replication slot 告诉 primary：

```text
在 consumer 确认之前，不要移除它仍需要的 WAL
```

原生查询：

```sql
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：

```text
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 永远不回来：

```text
retained WAL grows
pg_wal filesystem fills
primary can stop accepting writes
```

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

### slot 监控

至少观测：

```text
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 是另一条恢复路径

启用：

```text
archive_mode=on
```

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

```text
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

```text
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 最低检查：

```text
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`。“释放空间”的错误动作可能直接删除唯一
可恢复路径。

## 本节实验查询

在合适的节点使用：

```sql
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 查询封装这些证据：

- [`ha-facts.sql`](/labs/ch20/ha-facts.sql)
- [`capture.py`](/labs/ch20/capture.py)

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

## 小结

```text
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
```

## 权威参考

- [PostgreSQL 18：Streaming Replication、Monitoring、Slots 与 Timelines](https://www.postgresql.org/docs/18/warm-standby.html)
- [PostgreSQL 18：System Administration Functions](https://www.postgresql.org/docs/18/functions-admin.html)
- [PostgreSQL 18：`pg_replication_slots`](https://www.postgresql.org/docs/18/view-pg-replication-slots.html)
- [本章 PostgreSQL evidence query](/labs/ch20/ha-facts.sql)

---

[上一节：从失败模型设计高可用](../01/) · [返回本章目录](../) · [下一节：同步策略与提交语义](../03/) ·
[查看全书目录](/toc/) · [查看索引中心](/indexes/)
