32.6 实战:随机恢复目标演练
本节把前五节压成一次可重放实验。它不把 target 写死在脚本里,也不只恢复一个“预期会 成功”的候选,而是要求:
source audit independently yields damage XID
inclusive candidate must prove damage is present and be rejected
exclusive candidate must prove damage is absent and be accepted
raw exclusive candidate must expose post-target data loss
reconciliation must preserve every audited legitimate write
all cleanup and safety claims must survive adversarial mutation实验会创建并删除一次性 synthetic schema,创建两个 side-restore 目录后精确删除,并 保留一份新 full backup。它只适用于已经确认的开发沙箱;guard 字符串不是生产授权。
32.6.1 在隐藏时间窗内注入误更新
先读四份合同
- 环境、fixture 与验收:
requirements.json - 状态机、允许与禁止动作:
recovery-contract.json - 人类可读安全边界:
lab-contract.md - 拓扑与数据流:
topology.mmd
静态检查不连接远端:
static/labs/ch32/task.sh lint只读预检可在一个独立新目录运行:
export PG36_EVIDENCE_DIR="$(
mktemp -d "${TMPDIR:-/tmp}/pg36-ch32-capture.XXXXXX"
)"
static/labs/ch32/task.sh capture它验证:
target identity and three-member Patroni topology
pg-test-1 is sole running primary
pg-test-2/3 are streaming replicas
PostgreSQL major and cluster_name
fixture schema absent
restore prefix empty and port free
repository readable
Pig pitr required flags present
source and upstream evidence hashescapture 的在线 mutation 为零,不创建 backup,不恢复目录,也不改变 Patroni/DCS/路由。
完整演练需要新的私有 evidence directory
export PG36_EVIDENCE_DIR="$(
mktemp -d "${TMPDIR:-/tmp}/pg36-ch32.XXXXXX"
)"
export PG36_CH32_TARGET=pg36-l2-vagrant/pg-test
export PG36_CH32_NONPRODUCTION=true
export PG36_CH32_PRODUCTION_DATA=false
export PG36_CH32_PRODUCTION_TRAFFIC=false
export PG36_CH32_CONFIRM=RANDOM_XID_PITR_RECONCILE_CH32
static/labs/ch32/task.sh drill:pitr任一 guard 缺失,runner 在 source mutation 前退出。evidence directory 已非空也会退出, 避免把新 run 混进旧证据。正式实现见:
fixture 是一份可独立计算的账
test.pg36_ch32 有 5,000 个账户:
account_id 1 .. 5000
opening balance 100000 + account_id cents
status activeopening total:
$$ 5000 \times 100000
- \frac{5000 \times 5001}{2} =512{,}502{,}500 $$
runner 用系统随机源选择连续 1,000 个 victim。目标账户范围、run ID 与事故时刻只在本次 私有证据中确定,恢复命令没有预置 XID 或固定时间。
四笔状态和一份 fresh backup
顺序:
base transaction
create 5,000 accounts and run marker
write base audit
fresh full backup
safe-before transaction
+100 cents to all 1,000 victims
insert 1,000 safe ledger rows
write safe XID audit
damage transaction
set all 1,000 victim balances to zero
status = mispriced
create 1,000 fixture-only pending outbox rows
write damage XID audit in the same transaction
post-target transaction
+700 cents to first 100 victims
insert 100 legitimate ledger rows
write post-target XID auditdamage 和 outbox 在同一事务中,因此 inclusive candidate 要么同时看到二者,要么实验
失败。外部 dispatcher 不存在,external_dispatch_enabled=false 写入 audit 与合同。
注意 source 在 damage 后、修补前的当前值:
first 100 victims balance=700, status=mispriced
other 900 victims balance=0, status=mispriced而正确历史值应是 opening + 100,前 100 个还要再加合法的 700。这个设计使“直接写回 recovered value”必然丢数据,逼迫操作者处理 good-after。
target 来自 source,不来自注入代码常量
runner 在三笔事务完成后,单独查询:
pg36_ch32.incident_audit
where run_id = exact marker
and stage = damage取得 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 观察:
base audit present
safe-before audit present
safe ledger rows 1,000
damage audit with target XID present
mispriced victim rows 1,000
pending wrong outbox 1,000
post-target audit/ledger absent / 0
decision rejected这证明默认 inclusive XID 语义停在目标事务之后。若 validator 只检查 pig_result.success
而没有检查 damage 数据,这个错误候选会看起来完全健康。
exclusive 候选才是历史正确边界
base audit present
safe-before audit present
safe ledger rows 1,000
damage audit absent
pending wrong outbox 0
post-target audit/ledger absent / 0
victims active 1,000
victim balance sum deterministic opening + 100 each
decision acceptedsource timeline 为 11;两个隔离候选沿 current 重放并 promote 到 timeline 12。因为
archive_mode=off,两个实验分支不会进入共享 repository。
accepted 的含义是“可作为历史正确数据源”,不是“可以直接切流”。它明确缺少 100 笔 post-target ledger。
从 candidate 提取,再与 source audit 合并
exclusive candidate 导出 1,000 行:
account_id
recovered_balance_cents
status=active私有 evidence 只保存 count 和导出集合 SHA-256。source 独立导出 100 条
post-target ledger,要求 key 集合正好等于预期 100 个账户,每条 delta 都是 700。
runner 构造 typed temporary table:
account_id
recovered_balance_cents
post_delta_cents
expected_current_balance_cents一笔事务中先验证 1,000 行 current preimage:
first 100 expected current balance = 700
remaining 900 expected current balance = 0
all status = mispriced完全匹配后才执行:
new balance = recovered balance + audited post delta
status = active
pending wrong outbox -> canceled条件更新或 outbox row count 不是 1,000,事务抛错并回滚。
最终 manifest 独立可算
safe 增量:
$$ 1000 \times 100 = 100{,}000 $$
post-target 合法增量:
$$ 100 \times 700 = 70{,}000 $$
最终总额:
$$ 512{,}502{,}500 + 100{,}000 + 70{,}000 =512{,}672{,}500 $$
正式结果:
accounts 5,000
total balance 512,672,500 cents
victims active 1,000
victims mispriced 0
safe ledger 1,000
post-target ledger 100
pending wrong outbox 0
canceled wrong outbox 1,000
external dispatch 0runner 最后按 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 |
共同阶段:
target identification 0.183 s
fresh full backup 2.090 s
pgBackRest check 0.630 s
reconciliation 0.200 s
backup logical size 42,355,954 bytes
repository delta 5,751,304 bytes这是本地 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
本次没有测量:
incident detection
human target investigation
production approval
large-scale validation
business-owner reconciliation
application deployment/config change
pool drain and traffic switch
cache/search rebuild
observation window因此公开 evidence 固定:
timings_are_sandbox_observations = true
production_rto_claimed = false生产演练应测 end-to-end:
t0 incident declared
t1 mutation fenced
t2 target approved
t3 candidate engine ready
t4 candidate business-valid
t5 good-after/external reconcile complete
t6 service traffic restored
t7 observation window exited分别报告阶段时长与 critical path,不能只取最快的 restore copy。
版本恢复与版本迁移是两项工时
物理 backup 要由兼容的 PostgreSQL major 与 extension library 启动。若事故时生产已经 升级,而目标 backup 属于旧 major,runbook 可能需要:
acquire exact old server binaries
acquire matching extension libraries
recreate locale/collation runtime
restore and validate on old major
export selected data
transform/import into current major
validate cross-version semantics这段工时要独立记录,不能用“目标主机已经装了新版 PG”抵消。不能直接拿 PG18 server
启动 PG17 physical PGDATA;跨 major 迁移应使用第 30 章的 pg_upgrade、逻辑迁移或
对象提取路径。
本次 source、backup 与 candidate 都是 PostgreSQL 18.4,没有执行 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 输出恢复证据和备份体系改进项
私有证据包
preflight-evidence.json
exercise-manifest.json
source-before.json
fixture.json
backup.json
inclusive-plan.json
inclusive-recovery.json
exclusive-plan.json
exclusive-recovery.json
reconciliation.json
source-after.json
cleanup.json
negative-report.json
validation-report.json
public-summary.json
review.txt目录为 0700,文件为 0600。review 拒绝 SCRAM verifier、private key、clear password、
credential URI 和 raw system identifier。公开摘要只保留版本、规模、计数、时间、
候选决策、安全结论和 gate。
对完成的同一证据目录,可以离线重跑:
export PG36_EVIDENCE_DIR=/private/existing/ch32-run
static/labs/ch32/task.sh verify
static/labs/ch32/task.sh reviewall 也只执行 verify + review,不会再次创建 fixture、backup 或 restore。修改 12 个
实验源文件中的任何一个后,旧 preflight/source hash 会失败;应开启新 run,不应给旧
证据重新盖章。
32 个反例不是装饰
validator 将每个完整 evidence 深拷贝后实际注入变异,要求全部被拒绝:
authority:
production data allowed
managed PGDATA / Patroni mutation allowed
backup expiration allowed
target:
time replaces audited XID
exclusive disabled
timeline changes to latest
archive mode enabled
TCP listener enabled
preflight:
target/primary/topology changed
source or upstream hash changed
recovery:
damage XID or backup label changed
archive coverage false
inclusive damage missing or candidate accepted
exclusive damage/post present or safe missing
reconciliation:
repaired rows = 999
post-target write lost
external dispatch = 1
cleanup/governance:
route changed
restore root remains
production gate approved正式 run:
declared counterexamples rejected 32 / 32
live evidence mutants rejected 32 / 32
source files hash-bound 12 / 12
secret material absent从一次成功演练生成改进 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.4 | 旧 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 场景和通过证据。只写“加强监控”无法改变下一次 恢复结果。
终局结论
本章正式证据支持:
exact audited XID targeting demonstrated
inclusive/exclusive boundary demonstrated
isolated Pig side restore demonstrated
post-target reconciliation demonstrated
exact fixture/root cleanup demonstrated
source HA preservation demonstrated它不支持:
production cutover approved
worst-case archive RPO met
production RTO met
external side effects compensated
cross-major restore completed
regional disaster recovery completed所以最终决策保持:
business_cutover_performed = false
production_ch32_gate = pending下一章切换响应目标:当数据本身正确,但 writer、timeline 或节点拓扑失去唯一性时,怎样
完成主从切换、fencing、pg_rewind 与故障节点重建。
上一节:数据验证与安全回切 · 返回本章目录 · 下一章:故障切换与集群重建——力挽狂澜 · 查看全书目录 · 查看索引中心