# 实战：在克隆环境分类并恢复

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

---

前六节建立了抢救原则，本节把它们压进一次可复验的盲测。演练不是“制造一个错误，
然后按预先知道的答案修掉”，而是同时维护三条相互独立的证据链：

```text
现场链：managed before/after + original case digest + known-good digest
判断链：blind packet -> classifier predicate -> route
恢复链：selected source -> transformation -> physical/business validation
```

如果分类器偷看答案、恢复动作改写原始 case，或验收只看进程启动，这三条链中至少有
一条会断。实验的价值正在于让这些捷径变成机器可拒绝的反例。

## 35.7.1 仅在启用 checksum 的专用镜像注入可控页异常 {#item-35-7-1}

### 先读合同，再允许 mutation

实验入口不是脚本参数，而是六份可评审合同：

- [`requirements.json`](/labs/ch35/requirements.json)：目标身份、fixture、风险边界和验收条件；
- [`classification-contract.json`](/labs/ch35/classification-contract.json)：盲包字段、两条合法 route
  及未知证据的停止规则；
- [`negative-cases.json`](/labs/ch35/negative-cases.json)：必须被验证器拒绝的反例；
- [`lab-contract.md`](/labs/ch35/lab-contract.md)：人可读的现场、操作副本和禁止动作说明；
- [`topology.mmd`](/labs/ch35/topology.mmd)：managed sandbox 与 disposable root 的信任边界；
- [`l3-rebuild-plan.json`](/labs/ch35/l3-rebuild-plan.json)：只规划、不执行的宿主机重建状态机。

先做不接触 PostgreSQL 的静态检查：

```bash
static/labs/ch35/task.sh lint
```

预期输出至少包含：

```text
status=lint-ok
declared_counterexamples=35-schema-valid
source_files_hash_bound=13
managed_reset_host_executed=false
production_ch35_gate=pending
```

如需核对当前 Pigsty sandbox，`capture` 只通过 SSH 读取目标身份、拓扑和服务投影，不改
数据库：

```bash
evidence_dir="$(mktemp -d /tmp/pg36-ch35-capture.XXXXXX)"
PG36_EVIDENCE_DIR="$evidence_dir" \
  static/labs/ch35/task.sh capture
```

完整演练则是 L3：会在 `pg-test-3` 创建私有临时 PostgreSQL 集群、停止并复制它，
再修改操作副本。因此 runner 要求五项精确 guard，缺一项就在创建远端目录之前失败：

```bash
evidence_dir="$(mktemp -d /tmp/pg36-ch35-evidence.XXXXXX)"

PG36_EVIDENCE_DIR="$evidence_dir" \
PG36_CH35_TARGET=pg36-l2-vagrant/pg-test \
PG36_CH35_NONPRODUCTION=true \
PG36_CH35_PRODUCTION_DATA=false \
PG36_CH35_PRODUCTION_TRAFFIC=false \
PG36_CH35_CONFIRM=CLONE_CLASSIFY_RECOVER_CH35 \
  static/labs/ch35/task.sh drill:forensics
```

这些值不是“我愿意承担风险”的通用开关。它们共同声明：目标正是本书的无数据、
无流量 sandbox，且授权范围只到 disposable clone。换成生产主机、生产数据或生产
流量后，本命令没有任何授权意义。

### 隔离布局

runner 在 `pg-test-3` 上创建：

```text
/tmp/pg36-ch35-forensics-<UUID>/
  source/          checksum-enabled fixture
  known-good/      clean shutdown 后冻结的可信快照
  cases/           每个场景独立的 original evidence copy
  working/         修复或恢复所使用的新副本
  sockets/         mode 0700 的私有 Unix socket
```

临时实例设置 `listen_addresses=''`，不进入 Patroni、DCS、HAProxy、PgBouncer、备份仓库
或业务路由。managed `pg-test` 只做 before/after 只读投影。cleanup 还必须同时满足：

1. 路径符合 exact UUID root；
2. root 中的 marker 与本次 run identity 相符；
3. 所有临时 postmaster 已停止；
4. original case 和 known-good 的保留性已经验证。

只有四项都成立，runner 才删除整个 exact root；它不会根据模糊 glob 删除目录。

### 物理页场景

fixture 是启用 data checksum 的 PostgreSQL 18.6 集群，包含 12,000 行确定性数据和
至少八个 heap block。源实例干净停止并冻结 known-good 后，runner 才为物理场景创建
独立 clone，并在 **postmaster 已停止** 时对目标 heap relation 执行：

```text
block             2
offset in block   512
operation         byte XOR 0x01
bytes changed     1
```

这个动作的目的不是模拟真实磁盘故障机理，而是生成一个可重复的、checksum 能检测到的
坏页。实验同时保存 relation mutation 前后的 SHA-256；运行中的 relation file、
managed PGDATA 和唯一恢复来源永远不被改写。

随后取得两类互补证据：

```text
offline: pg_checksums --check -> bad checksum = 1, exit non-zero
online:  sequential heap scan -> SQLSTATE XX001
```

离线检查回答“磁盘上的 checksum 是否匹配”，在线扫描回答“服务实际读取该页时发生
什么”。两者都不是修复动作。实验没有启用 `ignore_checksum_failure`、
`zero_damaged_pages`，也没有运行 `pg_resetwal` 或删除任何 `pg_wal` 文件。

## 35.7.2 另设索引或 collation 异常，随机隐藏故障类型 {#item-35-7-2}

### 第二种故障故意不破坏 page

collation 场景从同一 known-good snapshot 创建另一份 case。它只在 disposable catalog
中临时允许 `allow_system_table_mods`，把实验专用 ICU collation 的 stored version
改成带 run identity 的假值：

```text
actual version   153.121
stored version   0.pg36-ch35-278fcd34
```

这个构造只模拟“catalog 记录的版本与当前 provider version 不同”。它没有安装另一版
ICU，也没有证明比较函数或索引顺序真的变化。相应证据是：

```text
offline bad checksums       0
stored != actual         true
bt_index_check passes    true
relation kind            index-derived
```

因此，“checksum 全绿”和“B-tree 结构检查通过”不能否定 collation-derived state
已经过期；反过来，version mismatch 也不能被夸大成 heap page 已损坏。

### 隐藏答案，分类证据

runner 随机安排两个场景，并给每个场景生成无语义的 `case_id`。classifier 只能读取
`blind-packets.json` 中七组字段：

```text
case_id
observed_at
checksum
relation
collation
amcheck
business
```

场景标签与注入细节保存在独立的 `hidden-answers.json`，不作为 classifier 输入。合法
route 是显式、可评审的合取谓词：

```text
RESTORE_FROM_KNOWN_GOOD_COPY
  checksum.enabled = true
  AND checksum.offline_bad_checksums >= 1
  AND relation.kind = heap
  AND collation.version_mismatch = false

REINDEX_AND_REFRESH_COLLATION
  checksum.offline_bad_checksums = 0
  AND collation.version_mismatch = true
  AND amcheck.structural_check_passed = true
  AND relation.kind = index-derived
```

若两个谓词同时成立，或两个都不成立，分类器必须返回：

```text
STOP_AND_ESCALATE
```

未知状态不是“选一个最像的”。正确动作是保留原件、停止写入和重复实验，列出缺少的
事实与恢复来源，并在危险参数之前升级。

正式 run `278fcd34-48af-49e0-97c9-c5e5e4161c78` 的随机顺序是：

| 顺序 | 隐藏场景 | 关键盲证据 | 分类 route |
|---:|---|---|---|
| 1 | `COLLATION_METADATA` | checksum 0、version mismatch、amcheck pass | `REINDEX_AND_REFRESH_COLLATION` |
| 2 | `PHYSICAL_HEAP_PAGE` | checksum 1、heap、scan `XX001` | `RESTORE_FROM_KNOWN_GOOD_COPY` |

这里公布 hidden truth 是实验完成后的教学复盘；分类发生时，答案并未进入算法输入。

## 35.7.3 用备份、重建或抽取恢复并验证不变量 {#item-35-7-3}

### 物理异常：恢复新副本，不改现场

物理场景命中 `RESTORE_FROM_KNOWN_GOOD_COPY` 后，runner 不在坏页上写回正确 byte，
也不复制单个 block 覆盖现场。它从停止状态的 known-good snapshot 创建一份新的
recovery copy，再在新副本上完成：

```text
pg_checksums --check       bad checksums = 0
amcheck                    pass
row_count                  12,000
sum_id                     72,006,000
sum_balance                588,702,000
ordered_content_digest     5c04d26539b8fa55f52e13e6a78f8bdd
```

四个业务不变量同时匹配才算实验恢复成功。`row_count` 能发现大块缺失，却发现不了值
被替换；两项求和能捕获部分数值漂移，却可能相互抵消；确定顺序的内容 digest 再覆盖
每行关键字段。真实系统还应加入账务平衡、对象存在性、外部事件和幂等副作用等业务
事实，不能照抄这四项就宣称可信。

原始 damaged case 在验证期间保持逐文件 digest 不变，直到最终统一清理。这样即使
恢复结果后来被推翻，调查者仍能从同一原件重新分叉。

### collation 异常：先重建派生对象，后承认新版本

collation 场景也不直接操作 original case。runner 创建单独 working copy，并严格按
以下语义顺序执行：

```sql
REINDEX INDEX public.pg36_ch35_code_idx;
ALTER COLLATION public.pg36_ch35_icu REFRESH VERSION;
```

第一步让 exact dependent index 按当前 provider 规则重建，第二步才把 catalog 中
stored version 刷新为 actual version。若颠倒顺序，warning 会消失，但旧派生对象
可能仍由旧规则构造；那只是消音，不是修复。

正式结果：

```text
repair order               REINDEX -> REFRESH VERSION
elapsed                    267.719 ms
version mismatch after     false
offline bad checksums      0
amcheck after              pass
four invariants            all match
```

实际事故不能仅凭 collation 名称猜依赖关系。应先枚举所有依赖对象，确认 index、
partition、materialized view、constraint 与应用排序语义，规划锁和容量，再对 exact
对象重建。若 heap、checksum 或业务证据同时异常，应回到扩大调查范围，而不是继续套用
这条单因果 route。

### 验证证据包，而不是相信终端输出

一次完整 run 的关键产物是：

```text
evidence/
  before.json
  after.json
  classification.json
  negative-report.json
  public-summary.json
  validation-report.json
  review.txt
  exercise/
    blind-packets.json
    hidden-answers.json
    exercise-evidence.json
    cleanup.json
    run-manifest.json
    source-manifest.json
```

`before.json` 与 `after.json` 证明 managed system identifier、timeline 和 topology
没有变化；`source-manifest.json` 把 13 份 runner/contract 源文件绑定到 hash；
`run-manifest.json` 绑定本次输入与输出；`cleanup.json` 证明 exact root 已删除且没有
临时 postmaster 遗留。raw evidence 保留在受控位置，对外只发布去敏摘要
[`rescue-run.json`](/labs/ch35/rescue-run.json)。

对一个已有完整 bundle，可在不再连接或修改 PostgreSQL 的情况下复验：

```bash
PG36_EVIDENCE_DIR=/path/to/ch35-evidence \
  static/labs/ch35/task.sh all
```

`all` 只消费现有证据。正式 bundle 的 validator 不仅检查 happy path，还把合同的
35 个反例逐一变成 live mutant 并确认全部拒绝，覆盖：

- 把生产数据或 managed PGDATA 伪装成实验目标；
- 弱化 checksum 阈值或泄露 hidden answer；
- 在 original case 上原地修复；
- 先 `REFRESH VERSION`、后 `REINDEX`；
- 业务 digest 漂移却宣称恢复成功；
- 临时 postmaster 未停或 root 未删却宣称 cleanup 成功。

这类负向验证回答的是“哪些错误证据绝不能通过”，比再打印一次
`status=completed` 更能约束未来脚本回归。

## 35.7.4 用 `reset:host` 重建 Pigsty L3，不复用损坏现场 {#item-35-7-4}

### 这是风险类别，不是复制粘贴命令

本书把 `reset:host` 用作一个内部风险标签：当操作系统、存储、package 或 PostgreSQL
基线不再可信时，从声明式 inventory 和可信数据源重建整台 L3。它不是 Pigsty 中已经
替读者填好目标的普通命令，也不授权删除任何 host。机器可读计划见
[`l3-rebuild-plan.json`](/labs/ch35/l3-rebuild-plan.json)。

宿主机重建前至少要具备：

1. incident commander 明确 exact host、故障域与数据库权威所在；
2. 原始证据和 storage snapshot 已保存在目标之外；
3. 流量已排空，且健康节点或恢复环境拥有数据库权威；
4. 已验证的 backup、健康 cluster source 或重建路径被明确命名；
5. inventory、Pigsty release、package repository 与 secret source 全部固定版本；
6. 替代容量、失败回退和生产 destructive approval 已就绪。

安全状态机是：

```text
preserve + hash evidence
  -> fence suspect host from authority and routes
  -> provision clean host or verified clean storage
  -> apply pinned Pigsty node/PostgreSQL baseline
  -> restore trusted backup OR join as a fresh replica
  -> validate lineage/checksum/replication/route/backup/monitoring/business
  -> observe
  -> return traffic
```

关键字是 **clean** 和 **fresh**。不要把可疑 PGDATA 当作新实例恢复源，不要在未保存
唯一证据时格式化磁盘，也不要因为 service 成功启动就跳过 lineage 与业务验收。原
PGDATA 只有在证据保留期、合规义务和 incident owner 共同批准后，才进入单独的清理
流程。

### 本实验刻意停在生产门外

正式演练只验证了 L3 rebuild decision contract，没有执行 managed host 的移除、
重装或恢复：

```text
managed_reset_host_executed = false
managed_pgdata_mutated      = false
managed_service_changed     = false
managed_route_changed       = false
production_ch35_gate        = pending
```

因此本节可以支持团队评审“何时应重建、重建应满足什么”，不能提供生产重建时长、
数据可恢复比例或变更批准。真实生产执行要重新解析 exact inventory、当前 Pigsty
版本、故障域、备份恢复演练结果和业务 RTO/RPO，并走独立破坏性门禁。

到这里，本章形成了一条完整原则：

> 保留原件，用证据分类；从可信来源生成新状态，用物理与业务不变量共同验收；只有在
> 基础设施可信度也恢复后，才让流量回归。

---

[上一节：工程取证与业务验证](../06/) · [返回本章目录](../) · [下一章：事故复盘、控制固化与平台演进——举一反三](/postmortem-platform-improvement/) ·
[查看全书目录](/toc/) · [查看索引中心](/indexes/)
