# 隔离恢复策略

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

---

隔离恢复的目标不是“找一台空机器”，而是让恢复候选同时满足：

```text
cannot overwrite source evidence
cannot join source HA control plane
cannot receive application traffic
cannot emit unintended external effects
cannot contaminate source WAL archive
can still read the exact backup/WAL it needs
can be identified, validated, stopped and deleted exactly
```

这六条不是同一个开关。`listen_addresses=''` 能关闭 TCP 入站，不会自动阻止逻辑订阅、
HTTP 扩展或脚本向外连接；custom PGDATA 不接入 Patroni，也不会自动得到只读 repository
凭据。隔离要逐层证明。

## 32.3.1 不覆盖仍可取证的原集群 {#item-32-3-1}

### 原集群同时是现场、差异源和回退资产

即使原数据已经错误，它仍保存：

- 错误之后的合法提交；
- 当前业务 key、version 与外部事件身份；
- 未归档 WAL 和最近 commit 线索；
- session、job、slot、subscription 与路由状态；
- 与恢复候选进行差异比较的基准；
- 如果目标判断错误，重新选择候选所需的证据。

因此默认拓扑应是：

```text
live source --------------------------> preserved
    |                                      |
    +--> backup/WAL --> candidate A        +--> good-after audit
                   \--> candidate B        +--> current-state manifest
```

而不是：

```text
live source PGDATA --overwrite--> guessed target
```

PostgreSQL 官方恢复流程也建议在空间允许时先复制整个 cluster data directory 与
tablespace；至少保存可能尚未归档的 `pg_wal`
（[Recovering Using a Continuous Archive Backup](https://www.postgresql.org/docs/18/continuous-archiving.html#BACKUP-PITR-RECOVERY)）。
对仍在线的 source，不应把这理解成随意复制活动 PGDATA；应通过受支持的备份、存储快照
或停机证据流程完成。

### 先决定 source 的运行状态

三种常见状态：

| source 状态 | 何时选择 | 必须控制 |
|---|---|---|
| 继续服务 | 影响局部，可围住错误对象 | bad writer、受影响 key、事后写审计 |
| 只读/全局写围栏 | 影响广，good-after 难追踪 | 所有入口、后台作业、长事务 |
| 停止并保全 | 完整性未知或仍快速恶化 | WAL、内存外证据、启动权限 |

“source 继续服务”不能写成“什么都不做”。至少要冻结错误 job/service identity，记录围栏
时间，保存合法写 manifest。反之，也不要因为要做 PITR 就自动停全站；停写是业务可用性
决策，必须由影响范围与对账能力支撑。

### 保存事实，不保存秘密扩散

事故证据包可以记录：

```text
cluster and member identity
system identifier relation or protected digest
timeline and LSN
backup label and archive range
effective recovery parameters
object/row manifests and business-key digests
UTC action timeline
source-file and evidence hashes
```

不应把这些内容直接放进公开工单：

```text
SCRAM verifiers
TLS private keys
cloud/repository credentials
raw connection URIs
unredacted business rows
unbounded query text or bind values
```

私有证据目录需要最小权限、保留期限与访问日志；公开摘要只保留足以复核结论的环境轮廓、
计数、状态和散列。

### 任何覆盖动作都要单独授权

managed PGDATA restore 至少会：

1. 停止或绕开 HA 生命周期；
2. 替换当前物理数据；
3. 改变 timeline 与可继续恢复的路径；
4. 使旧节点与 DCS/其他成员产生身份冲突风险；
5. 让回到当前状态依赖另一条恢复路径。

它是独立的高风险状态迁移，不应因为 side restore 验证通过就自动获批。本章实验从不
执行 managed restore，也不把 side candidate 注册为 Patroni member。

## 32.3.2 选择备份、WAL 与目标环境 {#item-32-3-2}

### 备份必须早于目标并能走到目标

对候选 backup $B_i$，最低条件是：

$$
\operatorname{stop}(B_i) < T_{\text{target}}
$$

并且从 backup 一致点到 target 的所有必需 WAL 与 timeline history 都可读。选择最近的
合格 backup 通常减少重放量，但不能只按 label 字符串或“最新”按钮决定：

```text
stanza identity matches source
database system lineage matches
PostgreSQL major and block/checksum properties compatible
backup status has no error
full/diff/incr dependencies all retained
backup stop point is before target
archive min/max and history cover target timeline
tablespace mapping is available
repository key and credentials are usable
```

本章正式实验在 fixture base 创建后新做一份 full backup，再提交 safe、damage 与
post-target 三笔事务；这样既测试 backup 后 WAL 重放，也避免选到目标之后才完成的
backup。

### “archive max 大于目标”只是必要条件

WAL segment 名的范围比较可以证明仓库至少走到某个 segment，却不能单独证明：

- 中间每一个 segment 都存在；
- 对象内容未损坏；
- 所需 timeline history 存在；
- repository key 可用；
- restore command 权限和网络可用；
- 目标记录确实在宣称的 segment 中。

所以先运行 repository check，再实际恢复。测试环境可以主动 `pg_switch_wal()` 缩短等待，
生产不能把频繁 switch 当作归档性能修复；`archive_timeout` 太短会生成大量未填满但仍为
完整尺寸的 segment。

### 固定 exact backup，而不是让工具临场猜

恢复票据应保存：

```text
stanza             pg-test
repo               1
backup label       20260730-030910F
backup type        full
backup stop        timestamp + LSN + WAL
target             XID/time/LSN/name
inclusive          true/false
timeline           current/latest/N + rationale
target action      pause/promote/shutdown
```

运行 `pig pitr` 时使用 `-b/--set` 指定这份 backup。若 plan 解析出的 effective label、
target、timeline 或 data directory 与票据不一致，应在 restore 前阻断。

### 目标环境要匹配物理要求

物理恢复不是把 data directory 交给任意 PostgreSQL：

- server major 必须与物理格式匹配；
- 所需 extension shared libraries、locale/collation provider 要可用；
- data checksums、page/block/WAL segment 属性要被理解；
- tablespace 目标必须存在、映射正确且容量足够；
- 恢复关键参数不能低于源端需要；
- OS 用户、owner、mode 与文件系统语义要正确。

恢复关键参数常包括：

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

如果恢复主机当前配置比 backup 要求低，recovery 可能拒绝启动。正式实验读取 source
effective settings，再显式传给隔离 postmaster；这不是把整个 source 配置无审查复制过来。

### 先算空间，再决定保留策略

至少预算：

$$
\text{space}
  \ge \text{restored PGDATA}
  + \text{tablespaces}
  + \text{temporary WAL}
  + \text{export/intermediate data}
  + \text{safety margin}
$$

若同时恢复 inclusive 与 exclusive，可顺序复用主机端口和空间，但应保留各自 manifest
与日志；不要同时启动两个继承相同 identity、port 和外部配置的副本。

空间不足时，禁止以 `expire` 旧 backup 或删除 archive WAL 作为脚本的隐式副作用。本章
实验会留下新 full backup，因为清理 repository 不在授权范围内。

## 32.3.3 控制网络、凭据和外部副作用 {#item-32-3-3}

### 六层隔离清单

| 层 | 本章正式实验 | 真实生产演练还要考虑 |
|---|---|---|
| 文件 | 随机 marker 下的 custom `-D` | 独立卷、tablespace、快照权限 |
| 进程 | 手工 `pg_ctl`，一次只启一个 | cgroup/container/systemd identity |
| 入站 | `listen_addresses=''`，mode-0700 Unix socket | host firewall/security group |
| 身份 | private HBA，仅本机 `postgres peer` | secrets replacement、least privilege |
| 控制面 | 不加入 Patroni/DCS/HAProxy/PgBouncer | DNS/VIP/controller admission |
| 出站/副作用 | fixture 没有 dispatcher，archive off | egress deny、subscription/job/webhook 隔离 |

最后一行最容易被漏掉。`listen_addresses=''` 只是不接受 TCP 连接，不会阻止数据库或宿主
进程主动向外连接。真正的取证环境应在网络层默认 deny egress，再为只读 backup/WAL
读取开精确 allowlist。

### 恢复出来的凭据仍然有效

物理 backup 包含当时的：

- database roles 与 password verifier；
- `pg_hba.conf`、证书引用与部分服务配置；
- extension、job、subscription 和 FDW metadata；
- application-owned secret（若错误地存进业务表）。

因此不能让恢复实例监听原端口或加载原 HBA。本章在 command line 覆盖：

```text
listen_addresses=''
unix_socket_directories=<private-root>/socket
unix_socket_permissions=0700
hba_file=<private-root>/pg_hba.restore.conf
ssl=off
shared_preload_libraries=''
primary_conninfo=''
primary_slot_name=''
archive_mode=off
```

private HBA：

```text
local all postgres peer
local all all      reject
```

这些设置适合本章沙箱，不是所有生产恢复的通用模板。例如需要业务验证账号时，应新建
一次性、最小权限的验证入口，而不是重新开放原应用角色。

### `archive_mode=off` 防止污染 source 历史

隔离 candidate promote 后会产生新 timeline 和新 WAL。若它继承
`archive_mode=always/on` 与 source repository 写凭据，实验分支可能写入共享 archive，
增加 timeline 选择歧义，甚至与 retention 发生交互。

本章通过 pgBackRest restore 参数和 postmaster command line 双重确认
`archive_mode=off`。这不影响 `restore_command` 在 recovery 期间**读取**所需 WAL；
它只禁止候选成为新的 archive producer。

更严格的设计还应让恢复凭据 repository read-only。因为具备 restore 权限的 host 往往
也配置了 backup 写权限，单靠操作人员承诺不是权限隔离。

### 控制数据库内的自动执行器

恢复实例中可能存在：

```text
pg_cron / pgAgent jobs
logical subscriptions
background worker extensions
LISTEN/NOTIFY consumers
outbox relay
trigger-driven HTTP calls
foreign data wrappers
application sidecars and local service units
```

应在启动前列出它们，并从三层阻断：

1. **网络层**：默认拒绝出站；
2. **进程层**：不启动 application/relay/agent，按评审禁用 background workers；
3. **数据层**：把 outbox/queue 置隔离状态，使用幂等键和 sandbox endpoint。

本章只证明 synthetic fixture 的 `external_dispatch_count=0`，没有宣称通用 egress 隔离
已经通过。读者把实验迁移到真实系统时，必须补这一门。

### Pigsty 的 managed 与 side restore 边界

当前 `pig pitr` 有两种生命周期：

```text
managed data directory
  may stop Patroni
  ensures PostgreSQL is stopped
  restores managed PGDATA
  may start PostgreSQL
  leaves Patroni stopped
  does not rejoin HA or switch traffic

custom -D side restore
  requires pre-created postgres-owned directory
  requires --no-restart
  does not stop Patroni
  does not manage default PostgreSQL service
  operator starts isolated postmaster manually
```

完整语义见
[`pig pitr`](https://pigsty.io/docs/pig/pitr/)。这里最重要的不是记选项，而是从 plan
核对实际 boundary。路径是否为 side restore 由有效 managed PGDATA 与解析后的路径决定，
不是简单看字符串里有没有 `/pg/data`。

正式实验先执行：

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

plan 必须明确：

```text
boundary       pitr:side-restore
data directory exact candidate path
service lifecycle not-managed
backup set     exact reviewed label
target         exact source-audited XID
timeline       current
confirmation   required
```

结构化执行不会交互询问；只有审批完成后才使用 `--yes`。不要把 `--yes` 写进没有 guard、
没有 exact target、会指向 managed PGDATA 的通用脚本。

### 隔离验收

启动候选后，同时从 OS 与 SQL 两侧检查：

```text
OS:
  no TCP listener on candidate port
  private socket exists and mode is 0700
  postmaster.pid belongs to exact candidate
  managed Patroni process remains unchanged

SQL:
  cluster_name identifies candidate
  listen_addresses is empty
  archive_mode is off
  ssl is off for private socket-only lab
  shared_preload_libraries is empty in this fixture
  pg_is_in_recovery() eventually becomes false
```

停止时只允许：

```text
pg_ctl -D <exact-marker-root>/data stop
verify PID and socket absent
delete <exact-marker-root>
```

宽泛 `pkill postgres`、按端口杀进程、删除未解析变量路径都不属于可接受清理。

---

[上一节：恢复目标与时间线](../02/) · [返回本章目录](../) · [下一节：执行恢复并观察进度](../04/) ·
[查看全书目录](/toc/) · [查看索引中心](/indexes/)
