32.6 实战:随机恢复目标演练
本节把前五节压成一次可重放实验。它不把 target 写死在脚本里,也不只恢复一个“预期会 成功”的候选,而是要求:
实验会创建并删除一次性 synthetic schema,创建两个 side-restore 目录后精确删除,并 保留一份新 full backup。它只适用于已经确认的开发沙箱;guard 字符串不是生产授权。
32.6.1 在隐藏时间窗内注入误更新
先读四份合同
- 环境、fixture 与验收:
requirements.json - 状态机、允许与禁止动作:
recovery-contract.json - 人类可读安全边界:
lab-contract.md - 拓扑与数据流:
topology.mmd
静态检查不连接远端:
只读预检可在一个独立新目录运行:
它验证:
capture 的在线 mutation 为零,不创建 backup,不恢复目录,也不改变 Patroni/DCS/路由。
完整演练需要新的私有 evidence directory
任一 guard 缺失,runner 在 source mutation 前退出。evidence directory 已非空也会退出, 避免把新 run 混进旧证据。正式实现见:
fixture 是一份可独立计算的账
test.pg36_ch32 有 5,000 个账户:
opening total:
$$ 5000 \times 100000
- \frac{5000 \times 5001}{2} =512{,}502{,}500 $$
runner 用系统随机源选择连续 1,000 个 victim。目标账户范围、run ID 与事故时刻只在本次 私有证据中确定,恢复命令没有预置 XID 或固定时间。
四笔状态和一份 fresh backup
顺序:
damage 和 outbox 在同一事务中,因此 inclusive candidate 要么同时看到二者,要么实验
失败。外部 dispatcher 不存在,external_dispatch_enabled=false 写入 audit 与合同。
注意 source 在 damage 后、修补前的当前值:
而正确历史值应是 opening + 100,前 100 个还要再加合法的 700。这个设计使“直接写回 recovered value”必然丢数据,逼迫操作者处理 good-after。
target 来自 source,不来自注入代码常量
runner 在三笔事务完成后,单独查询:
取得 damage XID,再记录当前 LSN 与 required WAL segment。随后执行一次 WAL switch、
pgbackrest check,轮询 repository 直到 archive max 覆盖 required segment。若 XID
缺失、不是数字、与 damage transaction 返回值不一致或 archive 未覆盖,恢复不开始。
这不是说生产必须依赖一张同名 audit 表,而是证明目标身份必须从独立事实导出,并与 fixture/业务身份绑定。
32.6.2 独立定位目标、恢复、验证与回切
两个候选只有一个变量不同
| 参数 | inclusive | exclusive |
|---|---|---|
| backup | 同一 exact fresh full | 同一 exact fresh full |
| target | 同一 damage XID | 同一 damage XID |
| timeline | current |
current |
| action | promote |
promote |
| side directory | marker/inclusive | marker/exclusive |
| archive mode | off |
off |
| 差异 | 默认 inclusive | --exclusive |
两个 plan 都先被保存和校验。两个 postmaster 顺序启动在同一空闲端口,只监听各自 mode-0700 Unix socket;inclusive 停止并删除后才创建 exclusive。
inclusive 候选必须被否决
正式 run 观察:
这证明默认 inclusive XID 语义停在目标事务之后。若 validator 只检查 pig_result.success
而没有检查 damage 数据,这个错误候选会看起来完全健康。
exclusive 候选才是历史正确边界
source timeline 为 11;两个隔离候选沿 current 重放并 promote 到 timeline 12。因为
archive_mode=off,两个实验分支不会进入共享 repository。
accepted 的含义是“可作为历史正确数据源”,不是“可以直接切流”。它明确缺少 100 笔 post-target ledger。
从 candidate 提取,再与 source audit 合并
exclusive candidate 导出 1,000 行:
私有 evidence 只保存 count 和导出集合 SHA-256。source 独立导出 100 条
post-target ledger,要求 key 集合正好等于预期 100 个账户,每条 delta 都是 700。
runner 构造 typed temporary table:
一笔事务中先验证 1,000 行 current preimage:
完全匹配后才执行:
条件更新或 outbox row count 不是 1,000,事务抛错并回滚。
最终 manifest 独立可算
safe 增量:
post-target 合法增量:
最终总额:
正式结果:
runner 最后按 exact run marker 删除 pg36_ch32,停止两个 candidate,确认 socket/PID
消失后删除 exact roots;再次验证 Patroni 仍为一主两从、source system lineage 未变、
fresh backup 仍在 repository。它不执行 application cutover。
32.6.3 测量 RPO/RTO 并记录版本迁移工时
正式时间分解
公开证据
pitr-run.json 记录:
| 阶段 | inclusive | exclusive |
|---|---|---|
| plan | 0.219 s | 0.214 s |
| restore copy | 3.040 s | 2.974 s |
| start → promoted | 0.961 s | 0.939 s |
| candidate validation | 0.340 s | 0.503 s |
共同阶段:
这是本地 ARM64 四虚拟机、42.4 MB 合成数据的一次观测。磁盘缓存、已有 repository、 网络距离、压缩/加密、WAL 数量、表空间、extension 和并发都与生产不同。
一次事故有多种 RPO
| 口径 | 本次结果 | 含义 |
|---|---|---|
| archive RPO | target WAL covered | 没有注入 WAL 缺口 |
| raw exclusive loss | 100 rows | 为排除 damage,故意丢弃 target 后合法写 |
| reconciled fixture loss | 0 rows | 100 条 audited delta 全部合并 |
| external-system loss | 未测试 | 没有真实 payment/message provider |
所以不能只说“RPO=0”。更准确的结论是:
在这份 synthetic fixture、完整 audit 与无外部 dispatch 的条件下,exclusive PITR 原始缺少 100 笔合法写;经过条件对账后,已知 fixture 数据损失为 0。
若 production 没有完整 good-after audit,reconciled RPO 就是 unknown,不会因为 WAL 完整自动变成 0。
restore time 不是 RTO
本次没有测量:
因此公开 evidence 固定:
生产演练应测 end-to-end:
分别报告阶段时长与 critical path,不能只取最快的 restore copy。
版本恢复与版本迁移是两项工时
物理 backup 要由兼容的 PostgreSQL major 与 extension library 启动。若事故时生产已经 升级,而目标 backup 属于旧 major,runbook 可能需要:
这段工时要独立记录,不能用“目标主机已经装了新版 PG”抵消。不能直接拿 PG18 server
启动 PG17 physical PGDATA;跨 major 迁移应使用第 30 章的 pg_upgrade、逻辑迁移或
对象提取路径。
本次 source、backup 与 candidate 都是 PostgreSQL 18.6,没有执行 major migration。
所以本次只证明“同 major 运行时已就绪”的恢复路径,没有测得跨版本迁移工时。生产
报告应写 not required in this scenario,而不是虚构成 0 秒。
建议工时表:
| 工作 | started | ended | active labor | waiting | owner | evidence |
|---|---|---|---|---|---|---|
| old runtime acquire | ||||||
| extension compatibility | ||||||
| physical restore | ||||||
| logical extract/transform | ||||||
| target import | ||||||
| cross-version validation |
32.6.4 输出恢复证据和备份体系改进项
私有证据包
目录为 0700,文件为 0600。review 拒绝 SCRAM verifier、private key、clear password、
credential URI 和 raw system identifier。公开摘要只保留版本、规模、计数、时间、
候选决策、安全结论和 gate。
对完成的同一证据目录,可以离线重跑:
all 也只执行 verify + review,不会再次创建 fixture、backup 或 restore。修改 12 个
实验源文件中的任何一个后,旧 preflight/source hash 会失败;应开启新 run,不应给旧
证据重新盖章。
32 个反例不是装饰
validator 将每个完整 evidence 深拷贝后实际注入变异,要求全部被拒绝:
正式 run:
从一次成功演练生成改进 backlog
不要把结论写成“PITR 已掌握”。按发现分类:
| 类别 | 本次证明 | 下一轮故意测试 |
|---|---|---|
| target | exact serial damage XID | 并发事务与宽时间窗 |
| WAL | target 后 segment 已归档 | 缺一个 WAL、archive 延迟、坏 history |
| repository | 单一 sandbox repo 可读 | lost key、read-only credential、immutable/off-site |
| backup | fresh full | diff/incr dependency 与过期边界 |
| runtime | 同 PG18.6 | 旧 major、缺 extension、collation drift |
| data | 5,000 确定性账户 | 多 TB、tablespace、large object |
| reconcile | 100 audited deltas | 冲突写、未知 delta、人工 exception |
| side effects | fixture dispatcher absent | broker/payment/search sandbox |
| topology | source HA 未变 | 经授权的 managed restore/rejoin |
| service | 未切流 | application smoke、pool drain、观察窗 |
每项改进要有 owner、截止时间、下次 drill 场景和通过证据。只写“加强监控”无法改变下一次 恢复结果。
终局结论
本章正式证据支持:
它不支持:
所以最终决策保持:
下一章切换响应目标:当数据本身正确,但 writer、timeline 或节点拓扑失去唯一性时,怎样
完成主从切换、fencing、pg_rewind 与故障节点重建。
上一节:数据验证与安全回切 · 返回本章目录 · 下一章:故障切换与集群重建——力挽狂澜 · 查看全书目录 · 查看索引中心