跳转到主要内容

35 数据抢救与工程取证——起死回生

当数据库报告 checksum failure、invalid page、索引不一致或 collation version mismatch, 目标不再是“让错误消失”,而是回答四个必须分开的问题:

detect
  哪个对象、块或不变量出现异常?检测覆盖什么、没有覆盖什么?

preserve
  哪份是原始证据,哪份是可信恢复来源,怎样证明它们没有被改写?

recover
  应重建派生对象、从健康来源恢复,还是只能分段抽取可读数据?

validate
  数据库能启动之外,业务不变量、恢复链与故障域是否重新可信?

“起死回生”不是在唯一副本上尝试越来越危险的参数。专业抢救的第一动作通常是停止 新增写入、保存原始现场、制作可重复的操作副本,并让每次尝试都从同一个证据点分叉。

学习完成标准

完成本章后,读者应能:

  1. 区分物理页/存储异常、索引/排序规则派生异常与业务语义损坏;
  2. 解释 detection、repair、extraction、rebuild 为什么是四种不同工作;
  3. 为停写、只读、storage snapshot、PostgreSQL backup 与 clone 写出一致性边界;
  4. 用 SHA-256、只读介质、操作日志和副本树维护工程级 chain of custody;
  5. 记录 system identifier、timeline、版本、extension、locale/ICU、硬件与最近变更;
  6. 解释 data checksum 保护的是 data page,不覆盖 temp file、所有内部结构或业务语义;
  7. 正确使用 SHOW data_checksums 与离线 pg_checksums --check
  8. 用日志中的 relation/block 线索和 pg_relation_filepath 定位对象,但不手改文件;
  9. 判断存储、内存、内核或文件系统仍不可信时,应先修基础设施;
  10. 区分 bt_index_checkbt_index_parent_checkheapallindexedverify_heapam
  11. 评估 amcheck 的锁、I/O、CPU、内存、隐私与 hot-standby 限制;
  12. 识别 collation provider/version drift,并按“重建依赖对象后刷新版本”处置;
  13. 解释“索引可重建”为什么不能证明 heap 与业务数据安全;
  14. 按备份、健康副本、逻辑来源、分段抽取的优先级选择恢复源;
  15. ignore_checksum_failurezero_damaged_pagesignore_invalid_pagespg_resetwal 限定为克隆现场的最后手段;
  16. 写出停止自救、升级厂商/文件系统/硬件/数据库专业支持的触发线;
  17. 区分“能启动、能查询、结构一致、业务可信、可恢复”五个验收层次;
  18. 为不可恢复范围、估计方法、法律/合规通知和客户沟通保留证据;
  19. 在盲测中区分 1-bit heap page 异常与 collation-derived mismatch;
  20. reset:host 当作需独立审批的 L3 重建风险类别,而不是普通维护命令。

三类损坏,三种恢复来源

类别 典型证据 主要恢复来源 不应默认做
物理 checksum、invalid page、I/O error、块读取失败 可信 backup、健康副本、存储 snapshot、可读抽取 原地改页、删文件、忽略错误继续写
派生 amcheck、collation version、错误索引结果 heap/源表 + 正确规则重建 把 REINDEX 当作 heap 已安全
语义 约束外不变量、ledger/事件/业务对账 PITR、审计日志、上游事实、补偿 用物理工具“修”业务写错

分类可以重叠。一次坏内存写可能同时损坏 heap 和 index;错误 ICU 版本可能让结构在 不同节点上被不同比较规则解释;应用误写也可能生成完全合法的 page/checksum。遇到 冲突证据应扩大范围,不要强选最便宜的修法。

信任阶梯

process started
  < SQL endpoint responds
  < physical/structural checks pass
  < relational and business invariants pass
  < replica/archive/backup lineage passes
  < observation window remains clean

每一层只能证明自己的命题。pg_ctl start 成功不能证明索引返回正确结果; pg_checksums 全绿不能证明 collation、约束外余额或外部副作用正确; 业务抽样正确也不能证明每个 relation block 都可读。

正式实验

本章对 managed pg-test 只做 before/after L0 read-only capture。真实 fault 全部在 pg-test-3 的一次性 PostgreSQL 18.6 中:

/tmp/pg36-ch35-forensics-<run-id>
private Unix socket
data_checksums=on
12,000-row deterministic fixture
stopped known-good snapshot
separate case and working copies

runner 随机安排两个 blind case,正式顺序为:

COLLATION_METADATA -> PHYSICAL_HEAP_PAGE

第一例只在 disposable catalog 中把 exact ICU collation stored version 从 153.121 改为 run-specific fake value:

offline bad checksums        0
amcheck structural pass   true
version mismatch          true
route       REINDEX_AND_REFRESH_COLLATION
repair order  REINDEX -> REFRESH VERSION
repair elapsed            267.719 ms
business invariants match true

它模拟的是版本元数据不一致,不冒充真实 ICU 排序语义升级。

第二例在 stopped clone 上只翻转 heap block 2 中一个 byte:

bytes changed                 1
offline bad checksums         1
online sequential scan    XX001
route       RESTORE_FROM_KNOWN_GOOD_COPY
in-place repair           false
recovered bad checksums       0
business invariants match true

两个 mutated original case 在各自恢复期间保持逐文件 digest 不变,known-good snapshot 也保持不变;最后停止所有临时 postmaster 并删除 exact root。managed PGDATA、服务、 路由、Patroni/DCS 和 reset:host 均未触碰。

验证器构造并拒绝 35 个真实 mutant,包括打开生产/managed 边界、弱化 checksum 阈值、 blind evidence 泄露答案、原地修复、REFRESH 先于 REINDEX、业务 digest 漂移和谎报 清理成功。公开证据见 rescue-run.json

本章边界

正式实验没有:

  • 注入真实磁盘、控制器、内存、内核或文件系统故障;
  • 模拟真实 ICU/libc 比较语义改变;
  • 在唯一副本或 managed PGDATA 上改一个字节;
  • 使用 ignore_checksum_failurezero_damaged_pagespg_resetwal
  • 执行 Pigsty host 移除、重装或生产恢复。

因此它证明的是机制与证据链,不是生产数据可恢复比例或 host rebuild RTO。最终门禁 保持 production_ch35_gate=pending

阅读前后关系

本章目录

35.1 现场保护与操作边界

35.2 先分类再抢救

35.3 页与 checksum 证据

35.4 索引、collation 与 amcheck

35.5 抽取、跳过与重建策略

35.6 工程取证与业务验证

35.7 实战:在克隆环境分类并恢复

权威参考

PostgreSQL:

Pigsty:


上一章:过载保护与资源故障判型——李代桃僵 · 返回下卷导读 · 下一章:事故复盘、控制固化与平台演进——举一反三 · 查看全书目录 · 查看索引中心

35.1 现场保护与操作边界

数据损坏现场与普通性能事故不同:每一次 checkpoint、vacuum、restart、reindex、文件 删除甚至查询,都可能改变证据或覆盖本来还能读取的内容。第一目标是建立一个不再 变化的证据点,不是赶紧让告警变绿。

35.1.1 停写、只读、快照、克隆与证据哈希

先冻结新增变量

按影响面与 authority 选择:

stop application writes at admission
drain write service routes
revoke/disable affected writer credentials when authorized
place exact application in maintenance/read-only mode
fence a suspect instance from accepted database authority
stop PostgreSQL cleanly when evidence and recovery design require it

“只读”要区分三层:

能证明什么 不能证明什么
应用只读 正常业务不发写请求 运维、直连、后台任务不会写
数据库 transaction read-only 该 session 不执行普通写 所有 session/后台都停止
存储 snapshot/read-only mount 捕获某个块状态 snapshot 一定应用一致、源存储健康

不要把 promotion 当作“自动停写旧主”。若故障域是共享存储或坏内存,健康副本也可能 已接收同一损坏;若旧主没有 fence,切换还会增加两个可写历史的风险。

快照有一致性等级

crash-consistent
  相当于某一瞬间掉电,必须有完整 PGDATA + tablespace + WAL,启动时 recovery

application-consistent
  数据库与外部系统的业务 checkpoint/ledger 一致

clean-stopped
  PostgreSQL clean shutdown 后复制,适合离线工具与确定性实验

文件系统/storage snapshot 只有在覆盖所有 tablespace、WAL 路径与必要元数据,且写入 顺序语义可靠时,才可作为 crash-consistent PostgreSQL 副本。只复制 base/ 不是 PGDATA snapshot。

本章正式实验使用 clean-stopped known-good snapshot。生产未必有条件 clean stop; 那时应保留 crash-consistent snapshot,并在克隆上让 PostgreSQL recovery。

原始、来源、操作副本分开

original evidence
  发现异常时的最早可保存状态;只读、封存

known-good source
  经备份/副本/快照验证的恢复来源;只读、封存

working copy A/B/C
  每项假设从同一 source/evidence 分叉,可丢弃

不要让“原始损坏副本”与“可信恢复源”共用一个标签 backup。前者用于解释发生了什么, 后者用于恢复正确状态。

哈希证明什么

对封存文件树记录:

algorithm: SHA-256
relative path
size
digest
capture UTC
source device/snapshot identity
collector/tool version

哈希相同只证明两次计算之间字节相同,不证明第一次采集时就正确,也不证明未遗漏 tablespace、WAL 或外部对象。manifest 本身也应签名/受控保存,且不能把口令、密钥、 原始业务数据无界导出到普通 evidence 目录。

35.1.2 记录硬件、内核、日志、版本和最近变更

建立同一 UTC 时间线

至少记录:

identity:
  cluster_system_identifier: ...
  timeline_and_lsn: ...
  instance_host_storage_ids: ...
software:
  postgres_server_and_binary: ...
  extensions: ...
  os_kernel_libc_icu: ...
  filesystem_storage_firmware: ...
configuration:
  data_checksums: ...
  full_page_writes: ...
  locale_collation_versions: ...
  tablespaces_and_wal_paths: ...
events:
  first_user_symptom: ...
  first_postgres_error: ...
  kernel_storage_memory_events: ...
  deploy_upgrade_failover_backup: ...
  power_or_hypervisor_event: ...

日志要保留原时区、sequence/journal cursor、rotation 边界与原始文件哈希。摘抄一行 invalid page in block 7 会丢掉前后的 I/O error、backend identity 和 relation context。

版本是故障证据

以下差异都可能改变解释:

  • PostgreSQL major/minor 与启动该 PGDATA 的 binary;
  • extension shared library 与 SQL extension version;
  • libc/ICU/tzdata;
  • filesystem/kernel/storage firmware;
  • primary 与 standby 的 OS/collation provider;
  • backup/restore 工具和 repository format。

不要用“应该都是 PG18”代替实际 server_version_num、package build 和 binary hash。 同 major 的不同操作系统镜像也可能带不同 ICU/libc。

最近变更不等于根因

变更与首个症状相邻,只是候选因果:

change:
  at: ...
  scope: ...
  expected_effect: ...
supports:
  - exact evidence
contradicts:
  - exact evidence
experiment_on_clone:
  - falsifiable check

硬盘 error 同时出现在升级后,不应因为“刚升级”就忽略设备证据;反之硬件告警也可能 是读取已存在坏页时才触发。

35.1.3 不在唯一副本上反复试错

每次尝试都会消耗选择权

start/recovery        may replay WAL and update control state
SELECT                may set hint bits or expose more damaged pages
VACUUM                may remove versions and update visibility maps
REINDEX               replaces derived evidence
pg_checksums --enable rewrites relation blocks in place
zero_damaged_pages    discards all tuples on a page in memory
pg_resetwal           invents WAL/control continuity

因此工作树应是:

evidence E0 (immutable)
  ├─ clone H1: test hardware/filesystem interpretation
  ├─ clone P1: PostgreSQL structural checks
  ├─ clone X1: best-effort logical extraction
  └─ clone R1: candidate recovery

H1 失败后重建 H2,不在 H1 上连续叠加参数。每个 clone 保存输入 hash、动作顺序、输出、 退出状态与结论。

“只有一份”时怎么办

若无法制作 snapshot/clone:

  1. 停止非必要写入和自动恢复;
  2. 记录为什么不能复制(空间、设备状态、加密、访问权);
  3. 评估块级镜像/专业数据恢复而非数据库内试错;
  4. 明确每个读取会否加剧介质故障;
  5. 升级事故级别和专业支持;
  6. 由数据 owner 明确接受任何不可逆动作。

时间压力不增加数据副本。越接近唯一来源,越应减少实验。

35.1.4 绝不手工删除 pg_wal 或原始损坏文件

不要用文件系统动作伪装修复

pg_wal、relation segment、visibility/free-space map、control file 与 tablespace symlink 共同构成 PostgreSQL 状态。手工删除看似坏掉或“旧”的文件:

  • 不更新 catalog/control/WAL recovery 语义;
  • 可能把局部损坏扩大为数据库无法启动;
  • 破坏 replica/PITR/backup 所需 lineage;
  • 覆盖或消灭故障根因证据;
  • 让后续专业工具无法比较原始字节。

即使某个损坏 index 最终可重建,也先保存其 identity、size、hash、错误与依赖关系, 再在 working copy 或受控生产变更中用 PostgreSQL DDL 重建;不要直接 rm index relation file。

自动化也会改现场

临时隔离后检查并暂停可能的自动动作:

Patroni restart/reinit/failover
systemd restart policy
Kubernetes liveness restart
filesystem repair/fsck
cloud auto-replace
backup retention/prune
logrotate and core cleanup
autovacuum/maintenance jobs
monitoring remediation

不是所有自动化都要停,而是必须知道哪些会写现场,并由事故指挥统一决定。

现场保护完成定义

accepted writer stopped or isolated
original evidence identity and hash recorded
known-good recovery candidates named
at least one working copy available, or copy blocker escalated
automatic mutators inventoried
time/version/hardware/change evidence preserved
destructive actions and owners explicitly gated

完成这些才进入下一节的分类。


返回本章目录 · 下一节:先分类再抢救 · 查看全书目录 · 查看索引中心

35.2 先分类再抢救

“corruption”不是一个恢复方案。至少要区分:存储的字节/页是否错误、从源数据派生的 结构是否错误、业务意义是否错误。检测工具、恢复来源和可接受损失都不同。

35.2.1 物理页、存储与 checksum 错误

物理异常的证据链

典型入口:

checksum mismatch
invalid page / invalid page header
could not read/write block
short read
I/O error / filesystem remount read-only
storage medium error / controller reset
unexpected relation segment size or missing file

这些信号仍需定位:

one block / one relation / one tablespace / one device / many hosts
heap / index / toast / catalog / WAL / control
primary only / replica too / backup copy too
read-time detection / write-time failure / recovery-time failure

checksum failure 说明“读出的 data page 与页内 checksum 不一致”,不自动说明是磁盘: 坏内存、DMA/controller、虚拟化、filesystem、软件 bug 或离线篡改都可能改变字节。

交叉副本不能只比较 SQL 行

在相同 system identifier/lineage 下,可比较:

same relation identity and logical object
same or corresponding block/LSN context
primary, replica, backup and storage snapshot
kernel/storage errors on each host

物理 replica 可能从 primary 接收已经损坏的逻辑变化,也可能各自发生局部介质损坏; 共享存储/镜像又可能让“多个副本都有同样错误”并不独立。恢复源要有独立故障域与实际 校验,不是名字叫 replica 就可信。

默认路线

preserve corrupt original
verify hardware/storage path
identify last known-good source
restore/reseed a new working copy
validate physical + logical + business state

只有在没有健康来源且明确接受损失时,才考虑 best-effort extraction。

35.2.2 索引、排序规则与派生结构错误

派生结构可以重建,但先证明 source

index、materialized view、search vector、rollup 与 cache 都由别的状态生成。典型信号:

amcheck raises B-tree invariant error
same predicate gives seqscan/indexscan different rows
unique index behavior conflicts with expected equality
collation stored version differs from provider actual version
OS/ICU upgrade changes comparison rules
primary and standby use different provider versions

先关闭 planner 路径做诊断时,只在 clone 或有界 read-only 查询里进行:

BEGIN READ ONLY;
SET LOCAL enable_indexscan = off;
SET LOCAL enable_indexonlyscan = off;
SET LOCAL enable_bitmapscan = off;
-- bounded invariant query
ROLLBACK;

这只能帮助比较,不是修复,也不能保证 seqscan 本身绕过 heap 损坏。

collation 是外部语义依赖

collation object 把 SQL 名称映射到 libc 或 ICU provider。text B-tree 的顺序依赖比较 规则;provider 升级后,旧 index 中 tuple 的物理顺序可能不再满足新比较规则。仅更新 collversion 会消除 mismatch 提示,却不会重新排列已有 index。

因此安全顺序是:

inventory affected collation dependencies
  -> verify on clone
      -> REINDEX affected indexes/materialized structures
          -> validate
              -> REFRESH VERSION metadata

数据库级 default collation 还会影响更多对象;要从 catalogs 枚举依赖,不能只重建 报错的第一条 index。

派生异常也可能来自 heap

heapallindexed 发现 heap tuple 没有对应 index tuple 时,原因可能是 index 本身,也 可能是 heap/visibility/transaction state 异常。REINDEX 后 amcheck 通过只是一个信号; 还要验证 heap、toast、constraints 和业务 invariants。

35.2.3 逻辑不变量、应用写错与语义损坏

checksum 全绿也可能数据完全错误

UPDATE accounts SET balance = 0;
wrong tenant predicate
duplicate external event applied twice
currency unit conversion error
missing ledger entry
referential rule enforced only in application
out-of-order CDC correction

PostgreSQL 会把这些合法写入按 WAL、checksum 和 replication 正确保存。物理工具不会 知道“余额不应为负”或“订单总额应等于明细”。

业务不变量应提前定义:

-- 结构约束能表达的尽量进入数据库
ALTER TABLE account
ADD CONSTRAINT balance_domain CHECK (balance >= 0);

跨行、跨表、跨系统不变量需要 reconciliation:

row count by partition/tenant
sum/count/min/max with known semantic
ledger debit = credit
event id uniqueness and sequence
source-system vs database token
snapshot + change-log continuity

MD5/SHA digest 适合证明同一有序投影是否相同,不证明投影本身业务正确;必须绑定 SQL、 排序、NULL/encoding 与 snapshot time。

恢复路线

  • 错误发生时间明确且需整体回退:PITR 到隔离实例,再做差异恢复;
  • 有审计/事件/ledger:按稳定业务 ID 重放或补偿;
  • 上游系统为事实源:重新同步并核对删除/版本;
  • 局部错误且可逆:受控 correction transaction;
  • 外部副作用已经发生:数据库恢复之外做业务补偿。

不要在原实例上反复 PITR。第 32 章的恢复目标、timeline 与 replay 验证仍适用。

35.2.4 三类问题的恢复来源不同

决策矩阵

已证明的异常 首选 source 典型动作 核心验收
heap data page verified backup/replica/snapshot restore/reseed new copy checksum + rows + business
index only verified heap REINDEX exact dependency amcheck + query equivalence
collation drift heap + intended provider/version reindex then refresh dependency + order + invariants
materialized/derived authoritative base tables/events rebuild/refresh source-to-derived reconciliation
app wrong write PITR/audit/event/upstream recover/diff/compensate business invariants
multiple/unknown immutable evidence + expert analysis stop and escalate missing facts resolved

分类置信度

不要把一个工具的结果写成绝对结论:

classification: physical-page
confidence: high
supports:
  - data_checksums=on
  - offline pg_checksums bad=1
  - scan raises XX001 on exact heap
contradicts:
  - none observed
not_proven:
  - root hardware component
  - other pages are clean
  - backups are independent and usable

pg_checksums 找到 1 个坏 checksum,不等于整个集群只有 1 处问题:可能还有未覆盖对象、 业务语义损坏或未来才读出的设备错误。反过来全绿也只覆盖被扫描的 data pages。

停止线

以下任一出现,路线转 STOP_AND_ESCALATE

  1. system identifier、timeline 或 binary/PGDATA 归属不明;
  2. 原始 evidence 已被多次写入,动作时间线无法复原;
  3. checksum、amcheck、业务结果相互矛盾;
  4. 所有已知 backup/replica 可能共享故障;
  5. 唯一副本无法安全克隆;
  6. 需要 zero_damaged_pagesignore_invalid_pagespg_resetwal 才能继续;
  7. 数据损失范围涉及监管、财务、隐私或安全事件。

上一节:现场保护与操作边界 · 返回本章目录 · 下一节:页与 checksum 证据 · 查看全书目录 · 查看索引中心

35.3 页与 checksum 证据

PostgreSQL data checksum 是发现部分静默字节变化的重要机制,但不是万能完整性证明。 理解其单位、触发时机和未覆盖面,才能正确解释一次成功或失败的检查。

35.3.1 checksum 是否启用及其检测边界

先确认,而不是假设

在线只读确认:

SHOW data_checksums;

SELECT data_page_checksum_version
FROM pg_control_init();

data checksum 是 cluster 级属性,不按 database/table 单独启用。启用时,每个 data page 在写入时更新 checksum,读取时校验。PostgreSQL 18 上游 initdb 默认启用 checksum, 但 --no-data-checksums 或部署工具的显式选项仍可关闭;因此生产判断必须读取实际 control state,不能从版本号反推。

离线检查:

pg_checksums --check --pgdata=/exact/clone/pgdata

官方要求 PostgreSQL clean shutdown 后运行 pg_checksums。返回码为 0 表示本次 扫描没有 checksum error,非 0 表示至少检测到一次失败。不要只解析人类输出而忽略 exit status,也不要对正在运行或有其他 writer 的 PGDATA 执行。

如需限定已知 relation filenode,可用:

pg_checksums --check \
  --filenode=16395 \
  --pgdata=/exact/clone/pgdata

这适合复验定位,不替代 full-cluster scan。 filenode 数字本身不包含 database/tablespace 身份,证据中仍要同时保存 catalog 对象 与完整相对路径,不能把这个过滤参数当作全局唯一对象标识。

覆盖与不覆盖

checksum 保护 data pages,但官方明确不覆盖:

  • internal data structures 的全部状态;
  • temporary files;
  • 业务语义与跨行不变量;
  • 是否遗漏了整个 relation/file;
  • collation/operator class 规则改变;
  • 被正确重写成错误内容的合法页面。

它也不是纠错码:能发现不一致,不能根据 checksum 自动恢复原字节。

读时检测的含义

在线读取坏页通常中止当前 transaction。若 page 仍在 shared buffer 且损坏发生在存储 副本上,何时重新从设备读会影响首次发现时间;因此“昨天没报错”不证明昨天设备上 没有坏块。离线全扫与经过业务访问的在线读覆盖不同。

ignore_checksum_failure=on 只会在报告 warning 后尝试继续;官方警告它可能导致 crash、传播或隐藏损坏。它不是高可用开关,本章实验保持 off。

35.3.2 日志、块号、关系文件与物理定位

从数据库 identity 映射到文件

对还可查询的 clone:

SELECT c.oid,
       n.nspname,
       c.relname,
       c.relkind,
       c.relfilenode,
       pg_relation_filenode(c.oid) AS current_filenode,
       pg_relation_filepath(c.oid) AS relative_path,
       pg_relation_size(c.oid) AS bytes
FROM pg_class AS c
JOIN pg_namespace AS n ON n.oid = c.relnamespace
WHERE c.oid = 'public.target_table'::regclass;

使用 pg_relation_filenode/path,不要假设 OID 等于当前 filenode。TRUNCATE、REINDEX、 某些 ALTER/rewrites 会更换 filenode;tablespace 又改变相对路径。

PostgreSQL 默认 block size 常见为 8192,但应从实际 control data确认:

SELECT database_block_size
FROM pg_control_init();

日志块号 $b$ 对应 relation fork 内的字节范围:

[b×B, (b+1)×B) [b \times B,\ (b+1)\times B)

其中 $B$ 是实际 block size。这只是定位数学,不是授权用十六进制编辑器修改该范围。

先确定 fork 与 segment

大 relation 会分成 segment;relation 还有:

main fork
_fsm free space map
_vm visibility map
_init unlogged initialization fork
TOAST relation and indexes

错误路径、block 和 relation identity 一起保存。index 的 block 2 与 heap 的 block 2 不是同一数据;同名 relation 在不同 database/tablespace 也不同。

日志与 SQLSTATE

保留:

SQLSTATE
severity
relation/database/backend identity if available
block number and file path
statement/query_id with privacy controls
UTC and log sequence
preceding kernel/storage messages

本章正式 bit-flip case 的在线顺序扫描返回 XX001,但教材没有把 raw stderr 导出, 因为错误文本可能带 object/query 信息。生产证据可在受控位置保存完整原日志,再制作 去敏投影用于协作。

物理定位不等于根因定位

找到 base/5/16395 block 2 只说明错误出现在哪里。根因仍可能在:

memory / CPU
filesystem / volume / controller / drive
hypervisor / cloud storage
DMA / firmware
PostgreSQL or extension bug
offline operator action
backup/restore transfer

需要跨层证据,而不是仅替换这一文件。

35.3.3 存储故障先修基础设施,再谈数据库重建

不要把健康数据恢复到坏底座

如果设备仍报告 error、filesystem 不稳定、memory test 未过或 hypervisor path 不可信, 在原主机恢复 backup 可能再次损坏干净数据。安全路线:

fence suspect host/storage from authority
preserve device and filesystem evidence
provision verified clean failure domain
restore/reseed from verified source
validate before accepting traffic
keep suspect media isolated for analysis

RAID rebuild、filesystem repair、cloud volume detach/attach 都会改变现场,应由对应专业 团队纳入同一时间线。数据库团队不要在唯一卷上自行执行 fsck -y

健康底座的验收

hardware/controller/drive diagnostics
kernel error-free observation window
filesystem consistency and mount semantics
memory/CPU health
power and time synchronization
firmware/driver/package baseline
write durability assumptions
independent backup/restore test

“新 VM”也不自动独立:它可能仍使用相同宿主机、storage pool、image 或坏备份。

重建后仍要扫描

新副本需要:

clean PostgreSQL startup/recovery
pg_checksums on a clean-stopped clone or scheduled validation
amcheck by risk-ranked object set
business invariants and query equivalence
replication/archive/backup health
service role and endpoint identity
monitoring and alert observation window

若从 physical backup 恢复,损坏页可能被原样复制;“restore 成功”只说明工具完成传输。 第 21 章的 restore drill 要与本章的 integrity checks 组合。

本节结论

checksum error
  -> page evidence
  != root component
  != automatic data-loss estimate
  != permission to edit/delete

下一节处理 checksum 可能发现不了、却会让查询返回错误答案的派生结构。


上一节:先分类再抢救 · 返回本章目录 · 下一节:索引、collation 与 amcheck · 查看全书目录 · 查看索引中心

35.4 索引、collation 与 `amcheck`

data checksum 可以证明 page 字节与页内 checksum 是否一致,却无法证明 B-tree 的 tuple 顺序、parent/child link、heap-to-index coverage 或比较规则仍然一致。amcheck 针对的是 relation 的结构与逻辑不变量,两者互补。

35.4.1 bt_index_checkheapallindexed 与锁成本

选择正确强度

安装 supplied extension:

CREATE EXTENSION IF NOT EXISTS amcheck;

单个 B-tree 轻量检查:

SELECT bt_index_check(
  index => 'public.orders_created_idx'::regclass,
  heapallindexed => false,
  checkunique => false
);

检查 heap tuple 是否都有 index 表示:

SELECT bt_index_check(
  index => 'public.orders_created_idx'::regclass,
  heapallindexed => true,
  checkunique => false
);

更全面的 parent/child 与 root descent:

SELECT bt_index_parent_check(
  index => 'public.orders_created_idx'::regclass,
  heapallindexed => true,
  rootdescend => true,
  checkunique => false
);

函数返回 void;没有抛错表示本次所检查的不变量未发现异常,不是“整个数据库完全 正确”。

锁与副本限制

PostgreSQL 18 官方边界:

函数 relation lock 特点
bt_index_check index + heap AccessShareLock 较轻,可用于 hot standby
bt_index_parent_check index + heap ShareLock 阻止 DML/VACUUM,不能用于 hot standby

heapallindexed=true 不提高 relation lock mode,但会显著增加时间、I/O 与内存工作; 其摘要结构受 maintenance_work_mem 约束。checkunique=true 又增加 unique visibility 检查。不要在事故主库上对所有大 index 一次性开最强选项。

一个风险排序:

exact reported index, heapallindexed=false
  -> exact index, heapallindexed=true on clone/low-load window
      -> parent check on writable clone or maintenance window
          -> wider object set by tablespace/provider/change scope

执行前记录 relation size、锁等待、statement timeout、I/O 预算、replica lag 与停止线。

verify_heapam

SELECT *
FROM verify_heapam(
  relation => 'public.orders'::regclass,
  on_error_stop => false,
  check_toast => true,
  skip => 'none',
  startblock => 0,
  endblock => 999
);

它可按 block 范围返回 heap/tuple 结构问题,但 check_toast=true 较慢;若依赖结构本身 损坏,检查也可能 error,极端情况下存在 crash 风险。优先在 clone 执行,并对输出做 隐私审查;错误信息虽偏结构,仍可能泄露数据特征。

35.4.2 collation 版本变化与索引顺序异常

版本 mismatch

SELECT n.nspname,
       c.collname,
       c.collprovider,
       c.collversion AS stored_version,
       pg_collation_actual_version(c.oid) AS actual_version,
       c.collversion IS DISTINCT FROM
         pg_collation_actual_version(c.oid) AS mismatch
FROM pg_collation AS c
JOIN pg_namespace AS n ON n.oid = c.collnamespace
WHERE c.collversion IS NOT NULL
ORDER BY mismatch DESC, 1, 2;

数据库 default collation:

SELECT datname,
       datcollversion AS stored_version,
       pg_database_collation_actual_version(oid) AS actual_version
FROM pg_database
ORDER BY datname;

provider:

d = database default
c = libc
i = ICU
b = builtin

操作系统或 ICU 升级可能改变 text comparison。旧 index 是按旧规则构建的,新 backend 按新规则搜索时,binary search 可能走错方向并返回错误答案。primary/standby provider 版本不一致也可能让只读副本先暴露问题。

枚举 index 的 collation 依赖

SELECT i.indexrelid::regclass AS index_name,
       x.ord AS key_position,
       x.collation_oid::regcollation AS collation
FROM pg_index AS i
CROSS JOIN LATERAL
  unnest(i.indcollation) WITH ORDINALITY AS x(collation_oid, ord)
WHERE x.collation_oid <> 0
ORDER BY 3, 1, 2;

表达式 index、operator class、partition、materialized view 与 extension objects 还需结合 pg_depend、DDL 和应用查询盘点。indcollation=0 只表示该 index key 不使用 collation, 不是整个 object 无外部语义依赖。

修复顺序

在 provider 已稳定、工作副本验证后:

1. inventory every affected derived object
2. REINDEX / rebuild each object under intended comparison rules
3. run amcheck and business/order invariants
4. ALTER COLLATION ... REFRESH VERSION
   or ALTER DATABASE ... REFRESH COLLATION VERSION
5. repeat checks on every role/replica after rollout

REFRESH VERSION 更新 catalog 记录,不会自动重建所有依赖对象。先 refresh 会让 warning 消失,却可能留下按旧规则排列的 index,是典型“消除检测器而没有消除缺陷”。

本章实验只伪造 stored version,实际 ICU 规则没有变化;因此它只证明流程与元数据, 不证明真实升级后的每个 index 必然有序。

35.4.3 索引可重建不意味着堆表数据安全

先证明 source relation

REINDEX 从 heap 读取 row 并生成新派生结构。如果 heap page、TOAST、visibility/XID 或 业务数据已经错误,新 index 可能只是忠实地索引了错误 source

至少组合:

data checksum / verify_heapam on source
TOAST readability
row count and partition coverage
constraints and foreign-key validation
business aggregates/ledger/token
seqscan vs indexscan bounded equivalence
amcheck after rebuild
backup/replica cross-check

查询等价性要控制 snapshot

若比较 seqscan 与 indexscan:

same transaction snapshot
same WHERE/ORDER BY/collation
stable deterministic projection
bounded result
explicit NULL and duplicate handling
same role/GUC/RLS context

两个独立时间点的 count 不同,可能只是并发写,不是 index corruption。对生产主库最好 在 repeatable-read/read-only snapshot 或 clone 中比较。

REINDEX 的生产风险

普通 REINDEXREINDEX CONCURRENTLY 的锁、空间、WAL、失败状态和支持对象不同。 损坏场景下 concurrently 也未必是正确路线:它需要继续依赖当前系统结构和写入并发。 先在 clone 证明:

source readable
new index validates
temporary disk/WAL capacity sufficient
unique conflicts understood
cutover and rollback defined

如果坏的是 system catalog index、heap 或唯一可信 source,停止套用普通 REINDEX runbook,升级抢救流程。


上一节:页与 checksum 证据 · 返回本章目录 · 下一节:抽取、跳过与重建策略 · 查看全书目录 · 查看索引中心

35.5 抽取、跳过与重建策略

抢救策略的优先级由“可信数据来源”决定,不由某个技巧是否能让 PostgreSQL 启动决定。 完整、已验证的 backup/健康副本通常优于在坏页周围挖数据;分段抽取又优于在唯一 PGDATA 上开启会丢数据的全局参数。

35.5.1 优先从备份、健康副本或逻辑来源恢复

候选来源矩阵

来源 优点 必须验证
physical backup + WAL 保留完整 PostgreSQL 状态,可 PITR restore、checksum、lineage、损坏是否已进入 backup
healthy physical replica RPO 较新,可快速重建 独立故障域、replay、checksum、同 system id/timeline
storage snapshot 快速封存大量字节 crash consistency、覆盖 tablespace/WAL、设备独立性
logical dump/export 隔离物理布局,适合重建 全对象覆盖、snapshot、权限/DDL/large object
upstream/event/ledger 可恢复业务事实 完整性、顺序、幂等、删除与外部副作用
corrupted clone extraction 最后可取回部分数据 明确缺口、不可验证范围、重复与编码

“最近”与“最好”不同。一个 5 分钟前但含坏页的 replica,不如 30 分钟前已 restore 验证 且有完整 WAL 的 backup;是否接受 RPO 由业务 owner 决定。

并行验证,不要串行赌注

track A: preserve and diagnose original
track B: restore latest known backup to isolation
track C: validate independent replica/snapshot
track D: prepare logical reconciliation

每条 track 不写另一个 track 的来源。先得到可用候选,再按 RPO、完整性、时间和风险 选择,不要等在唯一 damaged instance 上的“修复”失败后才想起恢复 backup。

物理来源也可能复制损坏

backup 工具成功退出不等于每个 page 正确;physical replication/WAL 会忠实传播合法 page changes,也不能修复已在 source 中的逻辑错误。每个候选都跑:

startup/recovery and system identifier/timeline
offline/online physical checks
amcheck/heap checks by scope
business invariants
archive/backup continuation

35.5.2 对可读数据做分段抽取和校验

只在 clone 上建立 extraction map

先按稳定业务 key/partition 分块:

COPY (
  SELECT id, tenant_id, occurred_at, payload
  FROM public.events
  WHERE id >= :lo AND id < :hi
  ORDER BY id
) TO STDOUT WITH (FORMAT binary);

每块记录:

range: "[lo, hi)"
snapshot: ...
rows: ...
min_max: ...
ordered_digest: ...
copy_exit_status: ...
error_relation_block: ...
destination_rows_digest: ...

不要用 OFFSET/LIMIT 做可恢复分块;行在并发/错误下可能跳动,复杂度也高。ctid 可辅助 定位 physical block,但会随 UPDATE/VACUUM/rewrite 改变,不能作为业务去重身份。

二分定位只是抽取方法

若某 key range 读取失败,可以在 clone 上继续二分:

[0, 1M) failed
  [0, 500k) pass
  [500k, 1M) failed
    ...

最终报告:

verified exported ranges
failed/unreadable ranges
rows known exported
rows estimated/unknown lost
duplicates and reconciliation rule
TOAST/large object coverage
constraints not yet validated

不能把“成功导出了 99% range”说成“只丢 1% 行”:坏块上的行数、TOAST 引用和跨表 关系可能未知。

导入到新库再验证

load into quarantine schema
retain source range/run id
reject or quarantine constraint conflicts
build indexes after source import when appropriate
validate counts/digests/business ledger
deduplicate by stable identity
record every transformation

不要直接把 best-effort 数据导回生产表覆盖已有正确数据。

35.5.3 危险恢复参数只在克隆现场、明确损失下使用

参数不是普通 troubleshooting 开关

手段 可能让什么继续 代价
ignore_checksum_failure=on 尝试读 checksum 不匹配页 可能 crash、隐藏/传播损坏
zero_damaged_pages=on 跳过坏 page header 内存中把整页置零,丢失该页所有行
ignore_invalid_pages=on recovery 忽略无效 page 引用 可能 crash、丢数据、隐藏/传播损坏
pg_resetwal 在缺 WAL/control continuity 时尝试启动 数据与事务一致性可能不可证明

官方对 zero_damaged_pages 的描述非常明确:它会销毁 damaged page 上的所有行,应在 已经放弃恢复该页后才考虑。它不是“自动修页”。

最低授权合同

target: exact disposable clone hash
original_evidence: immutable and independently stored
healthy_source_search: exhausted_or_documented
expected_loss:
  object_blocks_rows: ...
  business_effect: ...
parameter:
  name_value_scope: ...
  reason: ...
extraction_only: true
no_return_to_service: true
owner_approval: ...
stop_condition: ...

危险参数只为了抽取仍可读数据,产出的 cluster 永不直接回到服务。抽取后在全新 cluster 重建、验证。

不要发布复制粘贴“魔法命令”

每个损坏现场的 PG version、system id、WAL、checkpoint、relation 与硬件证据不同。 尤其 pg_resetwal 应由熟悉 PostgreSQL 内部与业务损失的人在 clone 上分析;本书实验 不执行它,也不提供绕过 guard 的快捷命令。

35.5.4 何时停止自救并升级到专业支持

技术停手线

  • system catalog、control file、WAL 或多个关键 relation 损坏;
  • 服务器反复 crash,core/stack 尚未保存;
  • 存储/内存仍报告硬件错误;
  • primary、replica、backup 对同一事实给出冲突结果;
  • 不知道某动作会否覆盖唯一恢复来源;
  • dangerous GUC/pg_resetwal 成为下一步;
  • encryption/key、filesystem、volume snapshot 需要专项能力;
  • 估计损失跨越 financial/security/compliance 边界。

升级包

business impact and decision deadline
immutable evidence manifest and access method
PostgreSQL/system/extension/locale versions
system id/timeline/LSN/control projections
exact errors with UTC and SQLSTATE
relation/fork/block mapping
kernel/storage/hardware evidence
backup/replica/source candidates
all actions already performed in order
questions requiring expert decision

不要为了“给专家一个更干净的环境”先删除日志、重启十次或运行 repair。未经请求不要 把含 PII/凭据/密钥的完整 PGDATA 或日志发给外部支持;先走安全与法律批准、最小披露和 加密传输。

时间与选择权

专业支持不是承诺一定恢复所有数据,而是避免用不可逆试错继续缩小选择空间。若业务 deadline 更早,可并行恢复已验证 backup,同时保留 damaged original 供后续取证;恢复 服务与确定根因不必串行。


上一节:索引、collation 与 amcheck · 返回本章目录 · 下一节:工程取证与业务验证 · 查看全书目录 · 查看索引中心

35.6 工程取证与业务验证

数据库抢救有两个交付对象:一个是可以承担服务的新状态,另一个是解释“原状态发生了 什么、哪些数据仍不确定”的证据链。只交付一个能启动的 cluster,会把未知损失留给 业务和下一次事故。

35.6.1 保留原始证据、操作副本和完整时间线

证据树

incident/
  manifest.json
  original/
    storage-snapshot-id
    pgdata/tablespace/wal hashes
    logs/kernel/hardware projections
  sources/
    backup-id + restore validation
    replica-id + lineage validation
  experiments/
    E001-input-hash/
      hypothesis
      commands/tool-versions
      output-projection
      conclusion
    E002-input-hash/
  recovery/
    selected-source
    transformations
    validation
  communication/
    decisions
    impact-estimates

敏感原件放在受控 evidence store,协作仓库只放去敏 projection。不要因为教材/工单需要 “可见”就把 raw PGDATA、query、凭据或客户记录提交 Git。

append-only 时间线

每条事件:

at_utc: ...
monotonic_or_sequence: ...
actor_or_automation: ...
target_identity: ...
action_or_observation: ...
input_evidence: ...
output_hash: ...
authority_ticket: ...
interpretation_at_the_time: ...
later_correction: ...

后来的认识不要覆盖当时记录;追加 correction。这样复盘才能区分“当时可见事实”与 “事后才知道的事实”。

工具可复现性

保存:

exact PostgreSQL binary/package version
tool options and locale/timezone
extension version and schema
script source hash
exit code
stdout/stderr raw location and redacted projection
input filesystem/snapshot identity

同名 pg_checksumsamcheck 或 ICU 在不同版本上可能有不同列、规则和输出。

35.6.2 区分“数据库能启动”与“业务数据可信”

五层验收

问题 示例证据
L1 process postmaster 是否稳定运行 service/PID/log/crash loop
L2 SQL catalog/事务是否可用 connect、read/write probe、control
L3 physical page/structure 是否一致 checksum、amcheck、heap check
L4 relational schema constraints/rows 是否一致 validate constraints、counts/digests
L5 business 业务事实是否可信 ledger、token、upstream reconciliation

恢复完成还要加 L6 operational:

replication
archive and backup
monitoring and alerting
service route and roles
capacity headroom
observation window

业务不变量要有定义版本

invariant: account_ledger_balanced
definition_version: git-sha
snapshot_or_cutoff: "..."
query_or_program_hash: "..."
scope:
  tenants: ...
  partitions: ...
expected:
  debit_minus_credit: 0
actual: ...
exceptions: [...]
owner_signoff: ...

只有 row count 不够。相同行数可以包含错误值、重复/缺失互相抵消、错误 tenant 或错误 时间窗。本章实验同时比较 row count、sum ID、sum balance 和有序内容 digest,但真实 业务还要使用有意义的 ledger/token。

抽样与全量

全量验证可能超过 RTO,可分层:

release gate:
  critical tenants/ledger + physical checks + current write probe

observation window:
  wider partitions, indexes, backups, reconciliations

post-incident:
  full historical scan where feasible

必须明确哪些是全量、哪些是抽样、抽样方法与遗漏风险。不能把“抽查 100 行正确”写成 “数据已完整恢复”。

35.6.3 记录不可恢复范围与合规沟通

用区间与集合表达 unknown

confirmed_recovered:
  id_ranges: [...]
  row_count: ...
confirmed_missing:
  business_ids: [...]
  reason: ...
possibly_affected:
  time_window: ...
  tenants: ...
  relation_blocks: ...
unknown:
  - rows formerly present on unreadable page
  - external side effects without idempotency ledger
estimation_method:
  source: ...
  confidence: ...

不要把 unknown 填成 0。若坏页内容不可读,精确行数可能无法知道;可以报告下界/上界 和估计方法。

技术 RPO 与业务损失分开

WAL/RPO gap
  recovery point 与事故点之间可能缺的数据库事务

physical extraction gap
  某些 page/object 无法读取

semantic gap
  数据存在但业务意义错误

external-effect gap
  数据库与支付、消息、邮件、对象存储不同步

一个 RPO 数字不能覆盖四类损失。

合规与沟通

由法律、安全、隐私和业务 owner 判断是否触发通知。技术团队提供可审计事实:

what systems/data classes were in scope
confidentiality vs integrity vs availability impact
earliest/latest affected time
confirmed/possible/unknown records
detection and containment time
recovery source and validation
evidence retention and access
next update time

对客户不要说“数据库坏了但已经修好”这种不可证实概括。应说明已确认影响、仍在验证 范围、临时控制和下一次更新时间;不公布攻击/隐私细节前先走授权。

结案条件

service and business invariants accepted
unknown/loss register signed by owners
original evidence retained per policy
temporary credentials/routes/clones removed
backup/replication/monitoring re-established
hardware/root-cause track assigned
postmortem and control actions scheduled

第 36 章将把这份证据包转化为长期控制。


上一节:抽取、跳过与重建策略 · 返回本章目录 · 下一节:实战:在克隆环境分类并恢复 · 查看全书目录 · 查看索引中心

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

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

先做不接触 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 还必须同时满足:

  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 执行:

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_failurezero_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.json

before.jsonafter.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 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,不复用损坏现场

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

本书把 reset:host 用作一个内部风险标签:当操作系统、存储、package 或 PostgreSQL 基线不再可信时,从声明式 inventory 和可信数据源重建整台 L3。它不是 Pigsty 中已经 替读者填好目标的普通命令,也不授权删除任何 host。机器可读计划见 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 已就绪。

安全状态机是:

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

关键字是 cleanfresh。不要把可疑 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,并走独立破坏性门禁。

到这里,本章形成了一条完整原则:

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


上一节:工程取证与业务验证 · 返回本章目录 · 下一章:事故复盘、控制固化与平台演进——举一反三 · 查看全书目录 · 查看索引中心