# 恢复流程与验证

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

---

恢复是一场受控的数据分叉：

```text
select historical lineage
  -> copy base
      -> replay WAL
          -> stop at boundary
              -> possibly promote onto new timeline
                  -> decide what may consume this history
```

每一步都可能生成一个能启动、却不应交付的实例。因此流程必须先写停止条件，
再写命令。

## 21.4.1 选择备份集、目标时间和恢复位置 {#item-21-4-1}

### 先冻结事故事实

开始恢复前，保留：

```text
incident start and detection time in UTC
suspected destructive transaction/request
source cluster identity and current timeline
current repository catalog
archive maximum and gaps
relevant application/audit logs
last known correct business marker
authority and requested outcome
```

不要一边猜 target，一边让日志、WAL、对象 lifecycle 和源状态继续变化而没有
快照证据。

### target 不是一句“十分钟前”

把 target 写成结构化决策：

```yaml
kind: name
value: before_release_20260729
inclusive: not-applicable
timezone: UTC
source_evidence:
  - pg_create_restore_point result
  - release change record
expected:
  present:
    - schema version 41
    - business token A
  absent:
    - migration transaction B
approved_by:
  - incident commander
  - data owner
```

时间 target 应包含 offset：

```text
2026-07-29 20:10:44.000+00
```

避免依赖 session/local timezone。

### target 类型选择

```text
known pre-created boundary
  name

known destructive transaction
  xid + inclusive=false, after verifying identity

known physical log point
  lsn

only trusted event time
  time + explicit timezone/inclusive

need earliest consistent database
  immediate

need latest independently archived state
  end of archive, with RPO evidence
```

XID 会 wrap；日志中同一个数字必须与正确 cluster/epoch/时间关联。LSN 精确但
不直接表达业务。time 易懂但可能有 clock/commit ambiguity。

### 选择能到达 target 的 backup

候选 base 必须：

```text
finish before or otherwise be valid for target
belong to same physical lineage
have all dependent backup objects
have continuous required WAL to target
have reachable timeline history
have usable encryption key
fit target runtime/version
```

对 time target，pgBackRest 可以选择一份结束时间早于 target 的可用 backup。
对 name/XID，工具不能总是自动知道 target 落在哪份历史中；pgBackRest
User Guide 明确提示 name/XID target 无法自动选择 backup。正式 run 因此用
`--set=<exact-label>` 固定刚创建的 full。

### “最新备份”可能太新

误删发生在 10:00：

```text
backup A ends 09:00
backup B starts 11:00
```

选择 B 无法回到 10:00 之前，因为其文件状态已经包含误删；需要 A 加 WAL。
“latest”不是无条件正确。

### backup set 与 recovery target 分离

```text
--set
  chooses base/dependency set

--type / --target
  chooses where WAL replay stops
```

一个是起点，一个是终点。混淆会导致：

- 起点晚于目标；
- 自动选择错误；
- 恢复到 WAL 尾部而不是事故前；
- 以为 backup label 就是业务时刻。

### 恢复位置

优先顺序：

```text
new host/failure domain
  best isolation

same host, new path/process/socket/port
  useful lab compromise with explicit exception

overwrite original data directory
  destructive replacement only, separate authority
```

日常验证和误删取数应选择新环境。覆盖原集群会：

- 消灭当前证据；
- 影响服务；
- 把 target 错误变成二次事故；
- 与 Patroni/DCS 冲突；
- 让失败回退更难。

### 空间预算

至少：

\[
Space \ge
RestoredData
+WALWorkingSet
+Temp
+Logs
+SafetyMargin
\]

若保留源数据副本，可能需要两倍以上。tablespace、sparse file、reflink、
object cache 与 filesystem reserved blocks 都要计入。

恢复前：

```bash
df -h
df -i
findmnt
lsblk
```

确认 path 真正位于预期设备，而不是 root filesystem 上一个空目录。

### 权限与隔离

恢复目录：

```text
owner postgres
mode 0700
not symlink
new and empty
not /pg/data
not a Patroni member path
```

接入：

```text
listen_addresses=''
private Unix socket mode 0700
custom restrictive pg_hba
distinct port
no service registration
no DNS/VIP/proxy route
```

数据副本本身可能含生产敏感数据；“测试恢复”不能降低访问控制。

### archive push 隔离

PITR promote 会产生新 timeline。验证实例若继续向 source repository
archive，可能污染共享历史。pgBackRest 官方指南建议，对会 promote 但不成为
新 primary 的 reporting/testing cluster 使用：

```text
--archive-mode=off
```

正式实验既在 restore option 中设置，又查询有效：

```sql
SHOW archive_mode;  -- off
```

仅修改 `archive_command=''` 会让 WAL 积在本地，并不等同于清晰的验证实例
policy。

### authority

恢复至少分三种授权：

```text
read-only catalog/capture
  no database or repository mutation

isolated restore drill
  synthetic source writes + new backup/path/postmaster

production replacement/cutover
  stops/overwrites/reroutes real service
```

本章 formal 只有第二种沙箱授权。不能拿它的确认 token 去生产。

## 21.4.2 启动前核对时间线与 WAL 完整性 {#item-21-4-2}

### preflight 清单

在 `postgres` 启动前确认：

```text
exact source/stanza/system lineage
exact backup label and type
backup status and dependency completeness
backup start/stop WAL
target kind/value/inclusive
target WAL covered by archive
timeline history available
restore_command configured
recovery.signal present
standby.signal absent unless desired
tablespace path and ownership
PostgreSQL major/binaries/extensions
recovery-critical max settings
network/HBA/archive isolation
log destination writable
```

把它写入 evidence，不要只在人的终端滚过。

### repository catalog

pgBackRest：

```bash
sudo -iu postgres \
  pgbackrest --stanza=pg-test --repo=1 --output=json info
```

检查：

```text
stanza status code
database/repository id
PostgreSQL version
backup label/type/error
start/stop timestamp
start/stop archive
logical/repository bytes
archive min/max by archive id
locks
```

不要把输出中的 repository secret 或 raw system identifier 复制进公开 evidence。

### WAL 文件名比较的限制

同一 archive ID、固定 segment size 和 timeline 语境中，固定长度 WAL filename
可帮助判断 target segment 是否不晚于 max。更完整的验证需要：

- 确认中间没有 gap；
- 确认 history file；
- 让 restore path 实际逐段读取；
- 对 object checksum；
- 记录 archive ID 与 lineage。

只有 `max >= target` 不能证明中间全在。正式实验同时跑 `pgbackrest check`
和真实 restore；真实 replay 是最终 gap detector。

### `restore_command`

恢复过程请求 WAL：

```conf
restore_command = 'pgbackrest ... archive-get %f "%p"'
```

返回合同与 archive push 类似：

```text
0
  requested file delivered

nonzero
  file unavailable; PostgreSQL may try pg_wal/stream/next source as applicable
```

不要让 command 把其他 cluster 同名 WAL 放入 `%p`。

### signal file 与 generated settings

pgBackRest restore 会生成 `postgresql.auto.conf` recovery settings，并创建
`recovery.signal`。检查：

```bash
test -f "$PGDATA/recovery.signal"
test ! -f "$PGDATA/standby.signal"   # for this promoted PITR design
sed -n '1,200p' "$PGDATA/postgresql.auto.conf"
```

输出可能包含路径或凭据参数，证据应做 secret-safe 投影，而不是无脑上传。

### 同一 system identifier

物理 backup 和 source 应同一 lineage：

```sql
SELECT system_identifier
FROM pg_control_system();
```

比较关系：

```text
source == restored
```

如果不同：

- backup 来自另一 cluster；
- stanza/path 混淆；
- evidence 目标错误。

不要“修” system identifier 让它相等；停止并调查。

### timeline 必须可达

记：

```text
backup timeline = Tb
target timeline = Tt
```

要求存在从 \(T_b\) 到 \(T_t\) 的合法 history path。PITR promote 后的新
timeline \(T_n\) 应满足：

\[
T_n > T_t
\]

本章：

```text
source/target timeline 7
restored promoted timeline 8
```

如果 promotion 后仍报告 7，要确认观察的是 current WAL timeline、checkpoint
timeline 还是 recovery 尚未完成。

### checkpoint timeline 会滞后

`pg_control_checkpoint()` 报最近 checkpoint。一个 replica 可能已在接收/重放
更新 timeline，但其 checkpoint control info 仍较旧。第 20 章已遇到这种
情况。

因此：

```text
primary current WAL filename timeline
Patroni TL
replica receive/replay evidence
checkpoint timeline
```

要标注 observation semantics，不能强行要求所有数字在任意瞬间相等。

### recovery-critical 参数

WAL 记录某些 source 参数。恢复实例不能把它们设得更低。重点：

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

正式脚本先查询 source，再在 isolated start 中显式携带。第一次开发启动因
`max_connections=20 < 500` 被 PostgreSQL 正确拒绝，未修改源集群。

这类失败的 SOP：

```text
stop
read PostgreSQL FATAL/DETAIL/HINT
compare with captured source settings
correct isolated runtime
start as a new reviewed attempt
never weaken source or edit WAL/control data
```

### extension 与 preload

物理 backup catalog 可能引用 extension。恢复环境若缺 `.so`：

- startup 可能因 `shared_preload_libraries` 失败；
- 查询对象可能缺函数/type；
- background worker 可能连接外部系统。

验证实例需要决定：

```text
install exact extension packages
or explicitly disable reviewed preload components
```

本章 synthetic fixture 不依赖 preload，因此 isolated instance 设：

```conf
shared_preload_libraries = ''
```

这不能证明生产 extension 全部可用；属于本章小数据边界。

### 不让恢复实例自动加入平台

同一 `cluster_name` 不是 Patroni membership，但为避免混淆，本章使用：

```conf
cluster_name = 'pg36-ch21-restore'
```

同时：

```text
no Patroni process
no DCS registration
no HAProxy service
no exporter discovery
no archive push
```

如果目标是正式替换 cluster，加入平台是另一个经过审查的阶段。

### 日志是证据

关注顺序：

```text
starting backup recovery
restored history/WAL from archive
starting point-in-time recovery to ...
redo starts
consistent recovery state reached
ready to accept read-only connections
recovery stopping at target
selected new timeline
archive recovery complete
end-of-recovery checkpoint
ready to accept connections
```

错误：

```text
requested WAL not found
recovery ended before configured target
invalid checkpoint record
insufficient parameter settings
could not load library
tablespace path failure
permission denied
```

不要只保存最后一行 `database system is ready`。

### 两阶段 readiness

正式 runner：

1. 用 `pg_ctl -w` 等到第一条连接；
2. 立即记录 `pg_is_in_recovery()` 与 `transaction_read_only`；
3. 继续轮询到 recovery=false；
4. 查询 effective isolation；
5. 做 rollback-only write；
6. 才声明 promotion complete。

观测：

```text
first connection:
  in_recovery=true
  transaction_read_only=true

355.896 ms later:
  in_recovery=false
  transaction_read_only=false
```

这不是理论角落，而是本章真实运行结果。

## 21.4.3 数据库一致不等于业务数据正确 {#item-21-4-3}

### 引擎一致性

PostgreSQL recovery complete 能说明：

- WAL record 可重放到一致点；
- control/catalog/transaction state 满足引擎；
- 数据库可按当前模式接受连接；
- promotion 时建立了新 timeline。

不能说明：

- target 是事故前正确边界；
- 所有正确交易都在；
- 错误交易都不在；
- 外部系统一致；
- 应用 schema/code 匹配；
- 秘密与权限适合交付；
- 报表金额正确。

### 业务不变量

为每个服务维护可执行 invariant：

```text
orders
  no orphan order lines
  order total = sum(lines)
  accepted payment token unique
  known checkpoint token present
  destructive release token absent

ledger
  debits = credits per journal
  immutable entries not missing
  sequence/event continuity

tenant SaaS
  no row crosses tenant boundary
  tenant counts match reference
  row-level security policies installed
```

SQL 示例：

```sql
SELECT order_id
FROM order_line l
LEFT JOIN orders o USING (order_id)
WHERE o.order_id IS NULL
LIMIT 1;

SELECT journal_id
FROM ledger_entry
GROUP BY journal_id
HAVING sum(debit) <> sum(credit)
LIMIT 1;
```

零行是某项证据，不是万能健康。

### 正向与反向标记

一个强 boundary test 同时要求：

```text
before target marker present
at/allowed target marker present
after target marker absent
```

本章：

```text
base     present
keep     present
discard  absent
```

只验证 `keep` 存在，可能实际恢复到了 WAL 尾部，`discard` 也在；反向断言能
发现 overshoot。

### token 要不可混淆

正式 marker：

```text
run_id + stage
unique token
database commit timestamp
primary key(run_id, stage)
unique(token)
```

恢复后按 exact run ID 查询，避免把旧演练行当成本轮成功。

### 验证 schema 与语义

除了行：

- schema version；
- extension version；
- constraint/index validity；
- owner/privilege；
- RLS policy；
- sequence；
- collation；
- function/trigger；
- materialized view freshness；
- partition attachment；
- large object。

有些对象在物理 backup 中存在，但应用新版本可能期待更晚 schema。历史
database 与当前 application code 不能直接组合。

### 外部系统

数据库可能回到 10:00，消息队列、对象存储、支付平台仍在 10:30：

```text
database says payment pending
payment provider says captured

database row absent
object file already created

database event offset rewound
consumer has processed later events
```

恢复计划必须决定：

- 外部系统也回退？
- 数据库追赶？
- 做补偿/对账？
- 暂停哪些写入？
- replay event 是否幂等？

PITR 不能跨系统自动保持分布式一致。

### 误删取数的安全合并

推荐路径：

```text
isolated historical restore
  -> validate historical target
      -> export only affected data
          -> normalize identifiers/format
              -> compare against current
                  -> reviewed repair transaction
                      -> audit and reconcile
```

不要把历史 cluster 暴露给普通应用写入；它的 sequence、outbox 与定时任务
可能重新发出旧动作。

可在隔离实例中：

```text
disable external network
disable schedulers/background workers
use read-only role for analysts
export with COPY/pg_dump
record checksum/count
```

本章为了验证 promotion 做一次 temporary table 回滚写，不产生持久业务写。

### service cutover 是另一个 gate

在真正 replacement 场景：

```text
freeze or fence old writer
validate candidate
configure identity/secrets
register monitoring
update routing
drain/reconnect clients
run smoke/invariants
reconcile unknown outcomes
monitor backlog
retain old evidence
```

本章不执行 routing change，所以：

```text
database/data proof = accepted with exceptions
service cutover = not run
```

第 22 章继续连接与服务合同。

### 验证矩阵

| 维度 | 检查 | 失败动作 |
|---|---|---|
| lineage | same system ID relation | stop |
| timeline | new child after promote | stop |
| target | expected present/absent | stop/reselect |
| engine | recovery=false, writable | inspect logs |
| isolation | no TCP, archive off | stop immediately |
| schema | version/extensions/constraints | repair environment |
| business | domain invariants | reject candidate |
| external | reconciliation | keep isolated |
| source | original cluster unchanged | incident |
| shutdown | private postmaster stopped | force safe stop |

### 证据包

至少包含 secret-safe：

```text
authority and exact target
source preflight
repository/catalog projection
backup label and WAL range
target decision
restore options and path
recovery phase timestamps
effective settings
lineage relation and timeline
business queries/results
source postflight
shutdown state
exceptions
counterexamples
review hashes
```

不要包含：

```text
repository key
cipher pass
database password
raw secret inventory
unnecessary raw system identifier
```

### 失败也是证据

本章有两次开发/正式前失败：

```text
development:
  max_connections lower than source
  PostgreSQL rejects recovery start

first formal attempt:
  pgBackRest check given unsupported --repo=1
  pgBackRest exits 31 before restore
```

两次都没有覆盖 source，也没有留下运行中的 isolated postmaster。我们修复
runbook，而不是删除失败痕迹后假装一次成功。

成熟流程会统计：

```text
failed drills
failure phase
time to diagnose
unsafe side effects
runbook correction
repeat proof
```

### 完成定义

```text
restore command exit 0                      insufficient
PostgreSQL accepts read-only                insufficient
PostgreSQL promoted                         engine milestone
business boundary passes                    data milestone
source remains healthy                      safety milestone
isolated instance stopped                   containment milestone
service controlled and reconciled           service milestone
```

本章正式到 containment milestone，服务 cutover 保持未运行。

## 小结

恢复的核心动作不是“启动一个旧数据库”，而是连续回答：

```text
which history?
which target?
which evidence?
which isolation?
which completion state?
which business truth?
which authority to expose it?
```

下一节把流程映射到 Pigsty 和 pgBackRest：仓库、调度、凭据、观察入口以及
为什么平台自动化应承载合同，而不是掩盖 PostgreSQL 原生证据。

---

[上一节：备份仓库与保留策略](../03/) · [返回本章目录](../) · [下一节：用 pgBackRest 交付备份策略](../05/) ·
[查看全书目录](/toc/) · [查看索引中心](/indexes/)
