35.7 实战:在克隆环境分类并恢复
前六节建立了抢救原则,本节把它们压进一次可复验的盲测。演练不是“制造一个错误, 然后按预先知道的答案修掉”,而是同时维护三条相互独立的证据链:
现场链: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 的专用镜像注入可控页异常
先读合同,再允许 mutation
实验入口不是脚本参数,而是六份可评审合同:
requirements.json:目标身份、fixture、风险边界和验收条件;classification-contract.json:盲包字段、两条合法 route 及未知证据的停止规则;negative-cases.json:必须被验证器拒绝的反例;lab-contract.md:人可读的现场、操作副本和禁止动作说明;topology.mmd:managed sandbox 与 disposable root 的信任边界;l3-rebuild-plan.json:只规划、不执行的宿主机重建状态机。
先做不接触 PostgreSQL 的静态检查:
static/labs/ch35/task.sh lint预期输出至少包含:
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 读取目标身份、拓扑和服务投影,不改
数据库:
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,缺一项就在创建远端目录之前失败:
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 上创建:
/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 还必须同时满足:
- 路径符合 exact UUID root;
- root 中的 marker 与本次 run identity 相符;
- 所有临时 postmaster 已停止;
- original case 和 known-good 的保留性已经验证。
只有四项都成立,runner 才删除整个 exact root;它不会根据模糊 glob 删除目录。
物理页场景
fixture 是启用 data checksum 的 PostgreSQL 18.4 集群,包含 12,000 行确定性数据和 至少八个 heap block。源实例干净停止并冻结 known-good 后,runner 才为物理场景创建 独立 clone,并在 postmaster 已停止 时对目标 heap relation 执行:
block 2
offset in block 512
operation byte XOR 0x01
bytes changed 1这个动作的目的不是模拟真实磁盘故障机理,而是生成一个可重复的、checksum 能检测到的 坏页。实验同时保存 relation mutation 前后的 SHA-256;运行中的 relation file、 managed PGDATA 和唯一恢复来源永远不被改写。
随后取得两类互补证据:
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 异常,随机隐藏故障类型
第二种故障故意不破坏 page
collation 场景从同一 known-good snapshot 创建另一份 case。它只在 disposable catalog
中临时允许 allow_system_table_mods,把实验专用 ICU collation 的 stored version
改成带 run identity 的假值:
actual version 153.121
stored version 0.pg36-ch35-278fcd34这个构造只模拟“catalog 记录的版本与当前 provider version 不同”。它没有安装另一版 ICU,也没有证明比较函数或索引顺序真的变化。相应证据是:
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 中七组字段:
case_id
observed_at
checksum
relation
collation
amcheck
business场景标签与注入细节保存在独立的 hidden-answers.json,不作为 classifier 输入。合法
route 是显式、可评审的合取谓词:
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若两个谓词同时成立,或两个都不成立,分类器必须返回:
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 用备份、重建或抽取恢复并验证不变量
物理异常:恢复新副本,不改现场
物理场景命中 RESTORE_FROM_KNOWN_GOOD_COPY 后,runner 不在坏页上写回正确 byte,
也不复制单个 block 覆盖现场。它从停止状态的 known-good snapshot 创建一份新的
recovery copy,再在新副本上完成:
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,并严格按 以下语义顺序执行:
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 会消失,但旧派生对象 可能仍由旧规则构造;那只是消音,不是修复。
正式结果:
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 的关键产物是:
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.jsonbefore.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。
对一个已有完整 bundle,可在不再连接或修改 PostgreSQL 的情况下复验:
PG36_EVIDENCE_DIR=/path/to/ch35-evidence \
static/labs/ch35/task.sh allall 只消费现有证据。正式 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,不复用损坏现场
这是风险类别,不是复制粘贴命令
本书把 reset:host 用作一个内部风险标签:当操作系统、存储、package 或 PostgreSQL
基线不再可信时,从声明式 inventory 和可信数据源重建整台 L3。它不是 Pigsty 中已经
替读者填好目标的普通命令,也不授权删除任何 host。机器可读计划见
l3-rebuild-plan.json。
宿主机重建前至少要具备:
- incident commander 明确 exact host、故障域与数据库权威所在;
- 原始证据和 storage snapshot 已保存在目标之外;
- 流量已排空,且健康节点或恢复环境拥有数据库权威;
- 已验证的 backup、健康 cluster source 或重建路径被明确命名;
- inventory、Pigsty release、package repository 与 secret source 全部固定版本;
- 替代容量、失败回退和生产 destructive approval 已就绪。
安全状态机是:
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 的移除、 重装或恢复:
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,并走独立破坏性门禁。
到这里,本章形成了一条完整原则:
保留原件,用证据分类;从可信来源生成新状态,用物理与业务不变量共同验收;只有在 基础设施可信度也恢复后,才让流量回归。
上一节:工程取证与业务验证 · 返回本章目录 · 下一章:事故复盘、控制固化与平台演进——举一反三 · 查看全书目录 · 查看索引中心