跳至内容
32.6 实战:随机恢复目标演练

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 在隐藏时间窗内注入误更新

先读四份合同

静态检查不连接远端:

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 hashes

capture 的在线 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           active

opening 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 audit

damage 和 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 独立定位目标、恢复、验证与回切

两个候选只有一个变量不同

参数inclusiveexclusive
backup同一 exact fresh full同一 exact fresh full
target同一 damage XID同一 damage XID
timelinecurrentcurrent
actionpromotepromote
side directorymarker/inclusivemarker/exclusive
archive modeoffoff
差异默认 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                     accepted

source 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           0

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 记录:

阶段inclusiveexclusive
plan0.219 s0.214 s
restore copy3.040 s2.974 s
start → promoted0.961 s0.939 s
candidate validation0.340 s0.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 RPOtarget WAL covered没有注入 WAL 缺口
raw exclusive loss100 rows为排除 damage,故意丢弃 target 后合法写
reconciled fixture loss0 rows100 条 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 秒。

建议工时表:

工作startedendedactive laborwaitingownerevidence
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 review

all 也只执行 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 已掌握”。按发现分类:

类别本次证明下一轮故意测试
targetexact serial damage XID并发事务与宽时间窗
WALtarget 后 segment 已归档缺一个 WAL、archive 延迟、坏 history
repository单一 sandbox repo 可读lost key、read-only credential、immutable/off-site
backupfresh fulldiff/incr dependency 与过期边界
runtime同 PG18.4旧 major、缺 extension、collation drift
data5,000 确定性账户多 TB、tablespace、large object
reconcile100 audited deltas冲突写、未知 delta、人工 exception
side effectsfixture dispatcher absentbroker/payment/search sandbox
topologysource 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 与故障节点重建。


上一节:数据验证与安全回切 · 返回本章目录 · 下一章:故障切换与集群重建——力挽狂澜 · 查看全书目录 · 查看索引中心

最后更新于