# 物理备份与 WAL 连续性

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

---

物理 PITR 可以写成一条依赖式：

\[
Recoverable(t)=
BaseBackup(b)
\land ContinuousWAL(b_{start},t)
\land ReachableTimeline(t)
\land RuntimeCompatibility
\]

其中任何一项缺失，都不是“少恢复一点”，而可能是整条恢复链不可执行。

## 21.2.1 基础备份、检查点与一致性起点 {#item-21-2-1}

### 为什么运行中的文件可以备份

在线复制数据目录时，文件并不处在同一个瞬间：

```text
relation A copied at 10:00:01
relation B page modified at 10:00:02
relation B copied at 10:00:03
catalog copied at 10:00:04
```

单看文件副本，它可能不一致。PostgreSQL 的保证来自：

1. 备份在一个已知 checkpoint 边界开始；
2. 保存恢复所需的 backup metadata；
3. 保存备份期间及之后所需的 WAL；
4. 恢复时从 redo 起点重放到一致状态。

所以物理备份不是“恰好一致的一篮子文件”，而是：

```text
recoverable file image + exact WAL obligations
```

### checkpoint 在做什么

checkpoint 把一个恢复起点写入控制信息，并推动 dirty buffer 写盘。它不是
“把所有事务历史压成备份”，也不是备份完成点。

在线备份通常在 checkpoint 起点建立 redo requirement：

```text
checkpoint redo LSN
  <= backup start LSN
      <= copied data interval
          <= backup stop LSN
              <= selected recovery target
```

恢复必须拥有从要求的 redo/WAL 起点到目标的连续记录。

### `pg_backup_start` / `pg_backup_stop`

低层协议的核心顺序：

```sql
SELECT pg_backup_start(label => 'reviewed-label', fast => false);
-- keep this session alive while the file copy runs
SELECT * FROM pg_backup_stop(wait_for_archive => true);
```

关键语义：

- 调用 `pg_backup_start` 的连接必须保持；
- `fast=true` 请求立即 checkpoint，可能增加 I/O；
- 文件复制期间数据库可以继续写；
- `pg_backup_stop` 产生重要的 `backup_label` / `tablespace_map` 内容；
- primary 上默认等待所需 WAL 归档；
- 自制工具必须逐步验证，不能只在最后看 exit code。

正式系统通常使用 `pg_basebackup` 或 pgBackRest，而不是重新实现低层协议。
理解协议是为了判断工具证据。

### `backup_label` 不是备注

`backup_label` 告诉恢复过程：

```text
which backup session
start WAL file
start LSN
checkpoint location
start time
```

它和 tablespace mapping 是恢复输入，不是方便人看的注释。随意修改、漏拷或
把另一份备份的 label 混入，都会破坏血缘。

PostgreSQL 12 以后在线备份 label 通常作为 `pg_backup_stop` 输出交给备份工具，
而不是永久留在运行中 primary 的数据目录。不要照搬旧版“拷走
`backup_label` 文件”的手册。

### `pg_basebackup`

`pg_basebackup` 通过复制协议取得运行中 cluster 的物理基础备份。常见能力：

```text
plain or tar output
server/client compression
backup manifest
WAL streaming or fetch
rate limiting
tablespace mapping
standby signal/config generation
checkpoint mode
progress reporting
```

它需要具备 `REPLICATION` 权限或 superuser，并满足 `pg_hba.conf` 与
`max_wal_senders`。它只备份整个 cluster，不能只备份一个 database。

示意：

```bash
pg_basebackup \
  --host=source \
  --pgdata=/safe/new/path \
  --format=plain \
  --wal-method=stream \
  --checkpoint=fast \
  --progress \
  --verbose
```

这不是本章 formal executor；正式实验使用 Pigsty 已交付的 pgBackRest。

### `--wal-method`

概念上：

```text
none
  backup output 不带 WAL；必须另有完整 archive

fetch
  在备份末尾从 source pg_wal 取所需 WAL；
  必须确保 WAL 未被回收

stream
  另开 replication connection 同步流式接收；
  需要额外 wal sender
```

即使 backup 包含让自身达到一致点的 WAL，若要恢复到更晚时间，仍需要后续
连续 archive。

### backup manifest 与校验

manifest 记录文件、大小、checksum、WAL range 等，可用
`pg_verifybackup` 验证一份 `pg_basebackup`：

```bash
pg_verifybackup /path/to/basebackup
```

它能证明文件与 manifest 一致、所需 WAL 结构满足其检查，却仍不能证明：

- repository credential 在事故时可用；
- PostgreSQL 能在目标环境启动；
- target 之后/之前的业务边界正确；
- extension/OS/配置依赖齐全；
- 服务能够切换。

校验是 restore proof 的一层，不是替代。

### data checksum 的边界

PostgreSQL data checksum 可以在读页时发现某些 page corruption。备份工具也
可对仓库对象做 checksum。两者都重要，但：

```text
checksum matches
  means bytes match expected checksum

checksum matches
  does not mean business values are semantically correct
```

错误事务生成的页面 checksum 完全正确。

### tablespace 与外部路径

物理备份必须覆盖 tablespace。恢复环境要检查：

- `pg_tblspc` symlink；
- target path 是否存在、为空、owner 正确；
- 不同主机路径是否需要 remap；
- mount 是否真的是预期设备；
- tablespace 与数据目录是否落在同一快照 consistency group。

忽略 tablespace 常导致“主数据目录恢复成功，启动时才发现一半对象缺失”。

### 从 standby 备份

从 replica 取备份可降低 primary I/O，但带来不同限制：

- 备份期间 standby 不能被 promote；
- backup 与 primary timeline/WAL archive 仍要协调；
- restartpoint 不等于 primary checkpoint；
- 低活动时某些增量条件可能不成立；
- replication lag 影响备份包含的历史；
- 工具必须知道如何从正确节点取得并验证 WAL。

“从副本备份”是容量/可用性选择，不自动增加备份独立性。

### PostgreSQL 18 原生增量

PostgreSQL 18 支持：

```bash
pg_basebackup --incremental=/path/to/prior/backup_manifest ...
```

服务端依据 `pg_wal/summaries` 中的 WAL summary 判断改变的 block。恢复前用
`pg_combinebackup` 把 full 与后续 incremental 合成为可启动的 synthetic
full。

依赖链：

```text
prior full
  -> prior/current manifests
      -> every required intermediate incremental
          -> WAL summaries at backup time
              -> pg_combinebackup
                  -> normal WAL recovery
```

PostgreSQL 不会替你管理哪些旧备份仍被新 incremental 依赖。过期策略删除
一个祖先，就可能让后代全部不可恢复。

### 版本兼容

物理备份通常用于同一 PostgreSQL 大版本。恢复环境还要匹配：

```text
CPU architecture and page format expectations
PostgreSQL major
extension shared libraries
collation/locale providers
tablespace layout
configuration parameters needed during recovery
```

本章开发实验故意把 `max_connections` 从 source 的 `500` 降到 `20`，
PostgreSQL 18 拒绝恢复：

```text
recovery aborted because of insufficient parameter settings
max_connections = 20 is lower than on the primary, where it was 500
```

这不是性能调优问题，而是 recovery safety check。正式 runner 携带 source 的：

```text
max_connections
max_worker_processes
max_wal_senders
max_prepared_transactions
max_locks_per_transaction
```

不能因为“验证实例很小”就任意降低 WAL 所要求的上限。

## 21.2.2 归档、timeline history 与恢复链 {#item-21-2-2}

### WAL 是有序的物理历史

WAL record 在数据页落盘前耐久化，支持 crash recovery、streaming
replication 与 archive recovery。LSN 是逻辑日志位置，例如：

```text
0/200002D0
```

通常每个 segment 16 MiB，但 segment size 可在 `initdb` 时选择。WAL 文件名
编码 timeline、log 与 segment，不应靠截字符串之外的自造规则做跨配置运算；
使用 PostgreSQL 函数：

```sql
SELECT pg_current_wal_lsn();
SELECT pg_walfile_name(pg_current_wal_lsn());
SELECT pg_wal_lsn_diff(pg_current_wal_lsn(), '0/20000000');
SELECT pg_switch_wal();
```

### 连续性要求

一份 base backup 要恢复到目标 \(t\)，必须有：

\[
[LSN_{required\ start}, LSN_t]
\]

上的每一段 WAL，以及中途 timeline 切换需要的 history。

缺一段不会得到“少几秒数据”；恢复通常停在 gap：

```text
base -> WAL A -> WAL B -> [missing C] -> WAL D -> target
```

即使 D 在仓库里，也不能跨过 C。

### `archive_mode`、`archive_command` 与 `archive_library`

归档要求：

```text
wal_level >= replica
archive_mode = on
archive_command or archive_library configured
```

shell archive command 中：

```text
%p  source path relative to data directory
%f  WAL filename
```

最重要的返回值合同：

```text
exit 0
  PostgreSQL believes WAL is durably archived and may recycle local file

nonzero
  PostgreSQL retries
```

错误地返回 0 是数据丢失风险；持续返回非零则形成 backlog 与磁盘风险。

### 归档必须幂等，但不能覆盖不同内容

crash 后 PostgreSQL 可能再次提交同一个 WAL 文件。正确 archive sink 应：

```text
target absent
  atomically write and durably persist

target exists with identical content
  return success

target exists with different content
  return failure and alert
```

绝不能无条件覆盖。两个不同 system identifier 的 cluster 若误用同一 archive
namespace，可能产生相同 WAL 文件名却内容不同。

stanza、repository path、cluster identity 和权限隔离是防碰撞设计的一部分。

### timeline 为什么存在

一次恢复或 promote 会从旧历史分叉：

```text
timeline 7: A -> B -> C -> D
                         \
timeline 8:              C' -> E -> F
```

新 timeline 保留父 timeline 与分叉 LSN 的 history。这样旧历史不会被
“回到过去再向前写”覆盖。

需要区分：

```text
system identifier
  initdb 生成的物理 cluster 血缘标识

timeline
  同一物理血缘中的历史分支

checkpoint timeline
  最近 checkpoint 记录的 timeline

current WAL timeline
  当前写入 WAL filename 所在 timeline
```

恢复出来的物理副本应与 source system identifier 相同；完成 promotion 后
应进入一个新 timeline。本章 evidence 只记录关系：

```text
system identifier relation = matches source
timeline 7 -> 8
```

不公开原始 identifier。

### recovery 输入

现代 PostgreSQL 通过 signal file 进入恢复：

```text
recovery.signal
  archive recovery / PITR

standby.signal
  standby mode, can continue waiting for WAL/stream
```

常见设置：

```conf
restore_command = 'fetch %f into %p'
recovery_target_time = '...+00'
recovery_target_name = '...'
recovery_target_xid = '...'
recovery_target_lsn = '...'
recovery_target_inclusive = on|off
recovery_target_timeline = 'latest'
recovery_target_action = 'pause'|'promote'|'shutdown'
```

同一轮只能选择一种 target kind。没有 target 时通常重放到可用 WAL 尾部。

### target form

| target | 优势 | 风险/前提 |
|---|---|---|
| time | 人和事故日志易理解 | 时区、提交时间、精度 |
| XID | 对目标事务明确 | wraparound/识别来源；inclusive |
| LSN | 物理边界精确 | 业务语义难读 |
| name | 预先设置，教学/发布边界清晰 | 事故后无法补建 |
| immediate | 最快到一致点 | 不是最新业务状态 |
| end of WAL | 尽量最新 | 可能包含逻辑错误 |

命名点：

```sql
SELECT pg_create_restore_point('before_risky_change');
```

只在 primary 上有意义，并写入 WAL。它不是 backup；没有 base 与 archive，
名字本身什么也恢复不了。

### inclusive / exclusive

恢复到 time、XID、LSN 时，要明确目标 record/transaction 是否包含。误删场景
常想恢复到 destructive transaction **之前**：

```text
target transaction known
  recovery_target_xid = destructive xid
  recovery_target_inclusive = false
```

但必须在实验中验证具体 target type 的语义。不要只凭自然语言“到某时刻”。

### target timeline

恢复链可能包含多次 promote。`latest` 让恢复沿 archive 中可达的最新 timeline
继续；`current`/具体数字会限制选择。

危险例子：

```text
choose an old backup
target time belongs to a child timeline
force target_timeline=current
```

结果可能根本到不了目标，或走错历史。选择前应画出 lineage：

```text
backup timeline
  -> history file
      -> parent fork LSN
          -> target timeline
```

### 恢复到只读与 promotion

hot standby 到达 consistent state 后可先接受只读连接，然后才到 target 并
执行 action。本章实际日志顺序：

```text
consistent recovery state reached
database system is ready to accept read-only connections
recovery stopping at restore point
selected new timeline ID: 8
archive recovery complete
end-of-recovery checkpoint
database system is ready to accept connections
```

因此完成条件应为：

```sql
SELECT pg_is_in_recovery();          -- must be false
SHOW transaction_read_only;          -- must be off
BEGIN;
CREATE TEMP TABLE write_probe(x int);
INSERT INTO write_probe VALUES (1);
ROLLBACK;
```

只读 SELECT 成功不是 promotion proof。

### 配置文件不在 WAL 历史里

恢复到过去不会自动恢复：

```text
postgresql.conf external changes
pg_hba.conf
pg_ident.conf
Patroni YAML/DCS policy
TLS certificates
Pigsty inventory
DNS/proxy config
application secrets
```

这些必须由 versioned declaration、配置备份与平台自动化重建。把配置也塞进
data directory 不是充分答案，因为事故可能同时损坏或需要在新环境重写。

## 21.2.3 复制槽、归档失败与 WAL 保留者 {#item-21-2-3}

### 谁让 `pg_wal` 不能回收

WAL retention 的常见“持有人”：

```text
checkpoint/recovery requirement
archive not yet successful
physical replication slot
logical replication slot
wal_keep_size
backup in progress
standby/restartpoint needs
```

它们并不是同一种保护，也不能相互替代。

### archive backlog

当 archive command 失败：

```text
completed segment remains needed
  -> retry
      -> pg_wal grows
          -> filesystem full
              -> PostgreSQL PANIC/offline
```

数据库继续运行一段时间不代表故障无害。官方文档指出，`pg_wal` 所在文件系统
填满会导致 PANIC；事务不会因此神奇地归档到远端。

监控：

```sql
SELECT archived_count,
       last_archived_wal,
       last_archived_time,
       failed_count,
       last_failed_wal,
       last_failed_time,
       stats_reset
FROM pg_stat_archiver;
```

解释要注意：

```text
failed_count = 21
last successful archive is after last failure
```

可能表示历史上失败过、当前已经恢复。不能仅因累计 count > 0 就报“当前失败”。
反之，长时间没有新 WAL 的系统，`last_archived_time` 老也未必故障。结合：

- current WAL segment；
- archive queue/backlog；
- WAL generation rate；
- repository maximum；
- disk free；
- recent command errors。

### archive lag 与数据风险

两个间隙：

```text
operational backlog
  current WAL - last archived WAL

disaster data gap
  source lost时，last business truth - last independently durable WAL
```

可用 byte 估计：

```sql
SELECT pg_wal_lsn_diff(
         pg_current_wal_lsn(),
         'last known archived LSN'
       );
```

但 repository 通常按 segment 报告，边界内还有 partially filled segment。
生产 RPO 应用时间和业务 token 做补充。

### 为什么低流量也有 archive delay

archive command 通常对完成的 segment 工作。若写入很少：

```text
important transaction commits
segment remains open
no archive object yet
```

选择：

- `archive_timeout` 定期强制 switch；
- 关键变更后 `pg_switch_wal()`；
- pgBackRest `check` 触发/验证；
- streaming WAL 到独立系统。

代价是更多 segment object 和带宽。策略应由 RPO 与成本反推。

### physical replication slot

physical slot 保护某个 consumer 尚未接收的 WAL：

```sql
SELECT slot_name,
       slot_type,
       active,
       restart_lsn,
       wal_status,
       safe_wal_size,
       inactive_since
FROM pg_replication_slots;
```

如果 consumer 永久消失而 slot 保留，WAL 可无限增长，除非
`max_slot_wal_keep_size` 设限。设限后 slot 也可能变为不可继续，需重建
replica。

槽保护 streaming consumer，不等于 archive 成功：

```text
slot retained WAL on primary disk
  != independent disaster copy
```

### logical slot 更容易被忽视

logical slot 还关联 catalog horizon：

- 保留 WAL；
- 可能保留 dead tuples/catalog rows；
- consumer lag 影响磁盘与 vacuum；
- failover/同步 slot 有版本与配置前提。

监控 slot 要看：

```text
active
restart_lsn
confirmed_flush_lsn
wal_status
safe_wal_size
xmin/catalog_xmin
owner and consumer
```

没有 owner 的 slot 是容量事故候选。

### `wal_keep_size`

`wal_keep_size` 是最近 WAL 的最低保留量，帮助无 slot standby 应对短暂断开。
它：

- 不是硬上限；
- 不保证某个 consumer 的准确位置；
- 不写远端仓库；
- 不提供长期 PITR；
- 不替代 slot 或 archive。

### 多个保留者叠加

最终 `pg_wal` 需要保留到最老需求：

\[
LSN_{recycle\ frontier}
=\min(
LSN_{checkpoint},
LSN_{archive},
LSN_{slots},
LSN_{backup},
LSN_{standby}
)
\]

概念上谁最老，谁控制回收边界。因此磁盘告警时不要只看“archive 正常”，
还要看所有 slot、backup lock 与 standby 状态。

### 本章沙箱事实

正式演练前后观察到：

```text
archive_mode=on
archive command configured
repository stanza status=ok
one S3-compatible repository
target restore-point segment <= repository maximum segment
source timeline remains 7
restore timeline becomes 8
```

累计 archiver 历史包含早期失败，但最近成功晚于最后失败；本章没有把累计
失败数误判为当前中断。

### 故障处理顺序

archive backlog：

```text
1. stop unsafe cleanup/expiration
2. measure pg_wal free space and growth rate
3. identify exact archive error and repository availability
4. preserve source WAL
5. restore archive path/credential/capacity
6. confirm backlog drains and repository maximum advances
7. prove a restore target, not just count successes
8. write incident and prevention
```

不要先：

```text
delete pg_wal files
drop unknown slots
reset stanza
expire repository aggressively
```

这些动作可能把可恢复性问题变成不可恢复。

### 本节原生检查单

```sql
SHOW wal_level;
SHOW archive_mode;
SHOW archive_command;       -- do not copy secrets into tickets/evidence
SHOW archive_timeout;
SHOW wal_keep_size;
SHOW max_slot_wal_keep_size;

SELECT * FROM pg_stat_archiver;

SELECT slot_name, slot_type, active, restart_lsn,
       confirmed_flush_lsn, wal_status, safe_wal_size
FROM pg_replication_slots;

SELECT pg_current_wal_lsn(),
       pg_walfile_name(pg_current_wal_lsn());

SELECT system_identifier
FROM pg_control_system();    -- compare securely; do not publish raw id

SELECT timeline_id, redo_lsn, checkpoint_lsn
FROM pg_control_checkpoint();
```

在 replica 上不能调用 primary-only current WAL 函数；使用 replay/receive
位置并标注 observation point。

## 小结

物理 recoverability 的核心不是“有一个 tar 包”，而是：

```text
one reviewed physical lineage
+ complete base image
+ exact backup metadata
+ continuous WAL
+ reachable timeline history
+ compatible recovery runtime
```

下一节把这些对象放进仓库依赖图：full、diff、incr 应如何保留，怎样防止
正确的自动过期删除仍被后代依赖的祖先，以及为什么加密不等于不可变。

---

[上一节：从恢复场景设计备份](../01/) · [返回本章目录](../) · [下一节：备份仓库与保留策略](../03/) ·
[查看全书目录](/toc/) · [查看索引中心](/indexes/)
