# 执行恢复并观察进度

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

---

一次 PITR 至少有四个不同的“完成”：

```text
restore copy complete
  -> PostgreSQL first accepts a connection
      -> configured recovery target is reached
          -> candidate data and business invariants pass
```

`pig pitr --no-restart` 成功只证明 pgBackRest restore 阶段完成；`pg_ctl -w start` 成功
可能只代表 hot standby 已可读；`pg_is_in_recovery() = false` 证明 recovery 已结束，
仍不证明目标业务状态正确。工具状态必须映射到这条状态机。

## 32.4.1 从已验证备份克隆恢复目标 {#item-32-4-1}

### 冻结一份可重放参数包

执行前把变量写入私有 evidence，而不是临场从 shell history 猜：

```text
stanza
repository number
backup label
source system-lineage relation
backup stop LSN/time/timeline
target type and exact normalized value
inclusive/exclusive
target timeline
target action
candidate root
PostgreSQL binary/version
recovery-critical settings
source file hashes
operator, reviewer and authorization
```

秘密只保存引用或受控位置，不复制进 JSON。参数包要能回答“同一份证据能否重放同一个
候选”，但不能变成凭据泄漏包。

### 先验证 repository 和 target 可达性

只读预检：

```bash
pgbackrest --stanza=pg-test --repo=1 --output=json info
pgbackrest --stanza=pg-test check
```

检查：

- stanza `status.code = 0`；
- exact backup label 存在且 `error=false`；
- backup type/dependency 与预期一致；
- backup end 在 target 之前；
- archive 与 timeline history 覆盖 target；
- restore host 有足够空间、正确版本和 extension library；
- candidate root 不存在，端口与 socket path 未占用。

`pgbackrest check` 成功仍不替代 restore。它验证 repository/stanza 的一组条件，不会运行
本章业务探针。

### 为 side restore 建一次性目录

本章 runner 只接受形如：

```text
/data/pg36-ch32-restore/run_YYYYMMDDTHHMMSSZ_random/inclusive
/data/pg36-ch32-restore/run_YYYYMMDDTHHMMSSZ_random/exclusive
```

的 exact path。目录必须：

```text
owner       postgres:postgres
mode        0700
symlink     false
preexisting false
managed PGDATA relation different
```

Pig 要求 custom `-D` 预先存在、归 DBSU 所有；它不会代为创建，因为在 destructive
classification、owner 检查和 plan 输出之前必须先知道具体目标路径。

### 先 plan，后 execute

inclusive 候选：

```bash
sudo -iu postgres pig pitr \
  -s pg-test -r 1 \
  -b "$BACKUP_LABEL" \
  --xid "$DAMAGE_XID" \
  --target-action=promote \
  --target-timeline=current \
  -D "$INCLUSIVE_ROOT/data" \
  --no-restart \
  --plan \
  -o json \
  -- \
  --archive-mode=off \
  --spool-path="$INCLUSIVE_ROOT/spool" \
  --log-path="$INCLUSIVE_ROOT/log"
```

审阅 plan 后，在受 guard 约束的自动化中把 `--plan` 换成 `--yes`。exclusive 候选只增加：

```text
--exclusive
```

不要同时改变 backup、timeline、target action 或验证探针，否则两个候选无法归因比较。

Pig 的 first-class 参数负责 target、backup、timeline、data directory 与生命周期；
`--` 后的原生 pgBackRest 参数不能绕过这些边界。当前 CLI 在 structured output 下要求
显式 `--yes` 才执行，`--plan` 是无执行预览路径
（[`pig pitr` Safety Mechanisms](https://pigsty.io/docs/pig/pitr/#safety-mechanisms)）。

### 读懂 restore 结果

本章正式 run 的 Pig 结果包括：

```text
boundary/effective data dir  exact side path
side_restore                 true
managed data dir             /pg/data
patroni_stopped              false
postgres_restarted           false
backup_set                   exact fresh full
target_type                  xid
target_value                 source-audited XID
exclusive                    true or false
target_action                promote
target_timeline              current
```

`patroni_active=true` 在这里是好事：它描述 restore host 上原有 live replica 的 Patroni
没有被 side restore 停掉；隔离 candidate 尚未启动。不能把它误读成“候选已由 Patroni
管理”。

### 文件恢复失败时

若 pgBackRest restore 失败：

1. 保留 plan、stdout/stderr、candidate marker 与 repository snapshot；
2. 不启动部分恢复目录；
3. 判断是 backup、WAL、key、权限、空间、tablespace 还是版本问题；
4. 停止任何 exact candidate postmaster；
5. 只删除当前 marker 所有的 candidate；
6. 修复原因后开启新 run，不覆盖旧 evidence。

managed restore 失败时，Patroni 可能已停止且 PGDATA 可能只恢复了一部分；不能因失败而
直接重启 Patroni。side restore 则不应改变 managed 生命周期，这正是事故调查优先使用
它的原因。

## 32.4.2 监控 WAL 重放、目标达成与启动状态 {#item-32-4-2}

### 手工启动时覆盖危险继承配置

正式 runner 使用 PostgreSQL 18 的 `pg_ctl` 启动 custom PGDATA，并通过 `-o` 传入：

```text
config_file=<candidate>/data/postgresql.conf
hba_file=<candidate>/pg_hba.restore.conf
listen_addresses=''
port=55433
unix_socket_directories=<candidate>/socket
unix_socket_permissions=0700
ssl=off
archive_mode=off
primary_conninfo=''
primary_slot_name=''
shared_preload_libraries=''
logging_collector=off
cluster_name=pg36-ch32-inclusive|exclusive
```

以及 source 的五项 recovery-critical maxima。完整、安全转义与路径验证见
[`exercise.py`](/labs/ch32/exercise.py)，不要从上面的概念清单拼接未审阅 shell。

### 第一条连接可能发生在 recovery 中

若 `hot_standby=on`，PostgreSQL 达到一致状态后可以开放只读查询，而后继续重放 WAL。
因此：

```bash
pg_ctl -w start
```

的 `-w` 只等待 server ready，不保证 `target_action=promote` 已完成。第 21 章正式实验
已经实际观察到：

```text
first connection  recovery=true, transaction_read_only=true
later             recovery=false, writable=true
```

本章 runner 继续轮询：

```sql
SELECT json_build_object(
    'in_recovery', pg_is_in_recovery(),
    'transaction_read_only',
        current_setting('transaction_read_only')::boolean,
    'last_replay_lsn', pg_last_wal_replay_lsn(),
    'replay_paused', pg_is_wal_replay_paused()
);
```

只有 `pg_is_in_recovery() = false` 才表示 promote action 已完成。随后仍先运行只读业务
探针；“可写”只是候选能力，不是允许应用写入。

### 分解恢复时延

用一个总秒数会掩盖瓶颈。本章分开测：

```text
target_identification_ms
plan_ms
restore_copy_ms
start_to_first_connection_ms
start_to_promoted_ms
candidate_validation_ms
reconciliation_ms
```

关系：

$$
T_{\text{technical}}
 = T_{\text{identify}}
 + T_{\text{plan}}
 + T_{\text{copy}}
 + T_{\text{replay}}
 + T_{\text{validate}}
$$

真实 RTO 还要加：

$$
T_{\text{service}}
 = T_{\text{technical}}
 + T_{\text{reconcile}}
 + T_{\text{cutover}}
 + T_{\text{application validate}}
 + T_{\text{approval}}
$$

本章没有执行后三项，所以不会从 2.8 秒 restore 推导“生产 RTO 3 秒”。

### 监控哪些进度

| 阶段 | 观察 | 正常推进 | 阻断 |
|---|---|---|---|
| 文件 restore | pgBackRest log/result | files restored, exit 0 | missing backup/key/space |
| 启动 | postmaster log、PID、socket | startup then consistent state | config/library/tablespace error |
| archive replay | PostgreSQL log、replay LSN | requested WAL advances | repeated missing/corrupt WAL |
| target | log + target-specific data | stop before/after expected commit | target not reached |
| action | recovery functions | paused/promoted/shutdown as declared | unexpected action |
| validation | SQL manifest | expected safe/bad/post sets | any invariant mismatch |

没有通用的“百分比”能准确表示 archive recovery，因为最终 target 与可用 WAL、restore
latency、replay workload 都会影响剩余工作。比虚构 73% 更有用的是保存已请求 WAL、
last replay LSN、目标和时间趋势。

### target 未达到就是失败

PostgreSQL 明确规定：配置了 recovery target，但 archive recovery 在到达它之前结束，
server 会以 fatal error 停止
（[recovery_target_action](https://www.postgresql.org/docs/18/runtime-config-wal.html#GUC-RECOVERY-TARGET-ACTION)）。

常见原因：

- 所选 backup 本来就在 target 之后；
- 中间 WAL 缺失或不可读；
- timeline 选错；
- target time/XID/LSN 根本不在该历史；
- repository credential 或网络中断；
- target 格式/时区错误。

禁止把 `last replay LSN` 当成“近似成功点”继续交付。应回到血缘与 target 证据。

## 32.4.3 用原生文件、日志和 SQL 复核工具状态 {#item-32-4-3}

### 三个观察面互相校验

**文件/配置面**

```text
PG_VERSION
backup_label or backup metadata
postgresql.conf / include chain
postgresql.auto.conf
recovery.signal
standby.signal
tablespace links
postmaster.pid / postmaster.opts
```

现代 PostgreSQL 通过 `recovery.signal` 进入 targeted recovery；若同时有
`standby.signal`，standby mode 优先。pgBackRest 会把 recovery 设置写入有效配置，
操作者应读 effective result，不能只看手写模板。

**日志面**

关注事件顺序，而不是只搜 `ready`：

```text
selected timeline / restore command
redo starts at ...
restored log file ...
consistent recovery state reached
recovery stopping before|after target transaction/time/LSN
redo done
new timeline selected/created
database system is ready to accept connections
fatal target not reached or WAL unavailable
```

日志时间必须与第 32.2 节的时区证据绑定。敏感业务值、凭据和原始查询不应未经筛选进入
公开 evidence。

**SQL/控制面**

```sql
SELECT
    pg_is_in_recovery(),
    pg_is_wal_replay_paused(),
    pg_last_wal_receive_lsn(),
    pg_last_wal_replay_lsn(),
    pg_last_xact_replay_timestamp();

SELECT * FROM pg_control_checkpoint();
SELECT * FROM pg_control_recovery();
SELECT * FROM pg_control_system();
```

在 promote 后，一些 recovery 函数会返回 `NULL` 或历史最后值，这是状态语义，不是自动
错误。原始 system identifier 应在私有证据中受保护；公开摘要通常只需要
“matches source lineage=true”。

### 工具结论必须回到原生事实

| 工具说法 | 原生复核 |
|---|---|
| side restore | effective PGDATA 与 managed PGDATA 不同；Patroni 未停 |
| archive off | `SHOW archive_mode` |
| target promote | `pg_is_in_recovery() = false` 与日志 |
| exact timeline | backup/history/config 与 control data |
| no TCP | `SHOW listen_addresses` + OS socket table |
| candidate stopped | exact `postmaster.pid`/socket absent |
| backup retained | restore 后再次读取 repository catalog |

平台包装减少操作错误，但不会改变 PostgreSQL 的恢复语义。证据同时保留 Pig structured
result 与原生复核，才能在 CLI 版本变化或输出解释争议时继续审计。

### `pg_waldump` 的位置

`pg_waldump` 可在高级调查中识别 WAL record、事务 commit/abort 与 relation 变化，但：

- 需要匹配 PostgreSQL major；
- 输出是低层内部格式，不是业务审计；
- 应分析受保护副本或 archive 文件；
- 不应在运行中的 active `pg_wal` 上做会干扰现场的操作；
- 结果仍需与日志、catalog 和业务 identity 关联。

本章正式 run 不靠 `pg_waldump` 猜 XID，而是从同一错误事务写入的 source audit 取目标，
再由两个候选证明包含关系。

### 候选状态表

恢复完成后先填：

```yaml
candidate:
  backup_label: exact
  target:
    type: xid
    value_source: source-audit
    inclusive: true | false
    timeline: current
    action: promote
  runtime:
    in_recovery: false
    archive_mode: off
    tcp_listener: false
    private_socket: true
    patroni_managed: false
  data:
    safe_present: true | false
    damage_present: true | false
    post_target_present: true | false
  decision: accepted | rejected | blocked
```

下一节才把 accepted candidate 与当前 source、good-after manifest 和外部系统放在一起，
决定提取、修补或切换。

---

[上一节：隔离恢复策略](../03/) · [返回本章目录](../) · [下一节：数据验证与安全回切](../05/) ·
[查看全书目录](/toc/) · [查看索引中心](/indexes/)
