数据抢救与工程取证——起死回生
35 数据抢救与工程取证——起死回生
当数据库报告 checksum failure、invalid page、索引不一致或 collation version mismatch, 目标不再是“让错误消失”,而是回答四个必须分开的问题:
“起死回生”不是在唯一副本上尝试越来越危险的参数。专业抢救的第一动作通常是停止 新增写入、保存原始现场、制作可重复的操作副本,并让每次尝试都从同一个证据点分叉。
学习完成标准
完成本章后,读者应能:
- 区分物理页/存储异常、索引/排序规则派生异常与业务语义损坏;
- 解释 detection、repair、extraction、rebuild 为什么是四种不同工作;
- 为停写、只读、storage snapshot、PostgreSQL backup 与 clone 写出一致性边界;
- 用 SHA-256、只读介质、操作日志和副本树维护工程级 chain of custody;
- 记录 system identifier、timeline、版本、extension、locale/ICU、硬件与最近变更;
- 解释 data checksum 保护的是 data page,不覆盖 temp file、所有内部结构或业务语义;
- 正确使用
SHOW data_checksums与离线pg_checksums --check; - 用日志中的 relation/block 线索和
pg_relation_filepath定位对象,但不手改文件; - 判断存储、内存、内核或文件系统仍不可信时,应先修基础设施;
- 区分
bt_index_check、bt_index_parent_check、heapallindexed与verify_heapam; - 评估
amcheck的锁、I/O、CPU、内存、隐私与 hot-standby 限制; - 识别 collation provider/version drift,并按“重建依赖对象后刷新版本”处置;
- 解释“索引可重建”为什么不能证明 heap 与业务数据安全;
- 按备份、健康副本、逻辑来源、分段抽取的优先级选择恢复源;
- 把
ignore_checksum_failure、zero_damaged_pages、ignore_invalid_pages、pg_resetwal限定为克隆现场的最后手段; - 写出停止自救、升级厂商/文件系统/硬件/数据库专业支持的触发线;
- 区分“能启动、能查询、结构一致、业务可信、可恢复”五个验收层次;
- 为不可恢复范围、估计方法、法律/合规通知和客户沟通保留证据;
- 在盲测中区分 1-bit heap page 异常与 collation-derived mismatch;
- 把
reset:host当作需独立审批的 L3 重建风险类别,而不是普通维护命令。
三类损坏,三种恢复来源
| 类别 | 典型证据 | 主要恢复来源 | 不应默认做 |
|---|---|---|---|
| 物理 | checksum、invalid page、I/O error、块读取失败 | 可信 backup、健康副本、存储 snapshot、可读抽取 | 原地改页、删文件、忽略错误继续写 |
| 派生 | amcheck、collation version、错误索引结果 |
heap/源表 + 正确规则重建 | 把 REINDEX 当作 heap 已安全 |
| 语义 | 约束外不变量、ledger/事件/业务对账 | PITR、审计日志、上游事实、补偿 | 用物理工具“修”业务写错 |
分类可以重叠。一次坏内存写可能同时损坏 heap 和 index;错误 ICU 版本可能让结构在 不同节点上被不同比较规则解释;应用误写也可能生成完全合法的 page/checksum。遇到 冲突证据应扩大范围,不要强选最便宜的修法。
信任阶梯
每一层只能证明自己的命题。pg_ctl start 成功不能证明索引返回正确结果;
pg_checksums 全绿不能证明 collation、约束外余额或外部副作用正确;
业务抽样正确也不能证明每个 relation block 都可读。
正式实验
本章对 managed pg-test 只做 before/after L0 read-only capture。真实 fault 全部在
pg-test-3 的一次性 PostgreSQL 18.6 中:
runner 随机安排两个 blind case,正式顺序为:
第一例只在 disposable catalog 中把 exact ICU collation stored version 从 153.121
改为 run-specific fake value:
它模拟的是版本元数据不一致,不冒充真实 ICU 排序语义升级。
第二例在 stopped clone 上只翻转 heap block 2 中一个 byte:
两个 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_failure、zero_damaged_pages或pg_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 实战:在克隆环境分类并恢复
- 35.7.1 仅在启用 checksum 的专用镜像注入可控页异常
- 35.7.2 另设索引或 collation 异常,随机隐藏故障类型
- 35.7.3 用备份、重建或抽取恢复并验证不变量
- 35.7.4 用
reset:host重建 Pigsty L3,不复用损坏现场
权威参考
PostgreSQL:
- Data Checksums
pg_checksumsamcheck- Collation Support
- Developer Options and damaged-page warnings
- System Information Functions
Pigsty:
pigcommand overview and context- Backup and Restore
- Run Playbooks with Ansible
- Parameters and Infrastructure as Code
上一章:过载保护与资源故障判型——李代桃僵 · 返回下卷导读 · 下一章:事故复盘、控制固化与平台演进——举一反三 · 查看全书目录 · 查看索引中心
35.1 现场保护与操作边界
数据损坏现场与普通性能事故不同:每一次 checkpoint、vacuum、restart、reindex、文件 删除甚至查询,都可能改变证据或覆盖本来还能读取的内容。第一目标是建立一个不再 变化的证据点,不是赶紧让告警变绿。
35.1.1 停写、只读、快照、克隆与证据哈希
先冻结新增变量
按影响面与 authority 选择:
“只读”要区分三层:
| 层 | 能证明什么 | 不能证明什么 |
|---|---|---|
| 应用只读 | 正常业务不发写请求 | 运维、直连、后台任务不会写 |
| 数据库 transaction read-only | 该 session 不执行普通写 | 所有 session/后台都停止 |
| 存储 snapshot/read-only mount | 捕获某个块状态 | snapshot 一定应用一致、源存储健康 |
不要把 promotion 当作“自动停写旧主”。若故障域是共享存储或坏内存,健康副本也可能 已接收同一损坏;若旧主没有 fence,切换还会增加两个可写历史的风险。
快照有一致性等级
文件系统/storage snapshot 只有在覆盖所有 tablespace、WAL 路径与必要元数据,且写入
顺序语义可靠时,才可作为 crash-consistent PostgreSQL 副本。只复制 base/ 不是
PGDATA snapshot。
本章正式实验使用 clean-stopped known-good snapshot。生产未必有条件 clean stop; 那时应保留 crash-consistent snapshot,并在克隆上让 PostgreSQL recovery。
原始、来源、操作副本分开
不要让“原始损坏副本”与“可信恢复源”共用一个标签 backup。前者用于解释发生了什么,
后者用于恢复正确状态。
哈希证明什么
对封存文件树记录:
哈希相同只证明两次计算之间字节相同,不证明第一次采集时就正确,也不证明未遗漏 tablespace、WAL 或外部对象。manifest 本身也应签名/受控保存,且不能把口令、密钥、 原始业务数据无界导出到普通 evidence 目录。
35.1.2 记录硬件、内核、日志、版本和最近变更
建立同一 UTC 时间线
至少记录:
日志要保留原时区、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。
最近变更不等于根因
变更与首个症状相邻,只是候选因果:
硬盘 error 同时出现在升级后,不应因为“刚升级”就忽略设备证据;反之硬件告警也可能 是读取已存在坏页时才触发。
35.1.3 不在唯一副本上反复试错
每次尝试都会消耗选择权
因此工作树应是:
H1 失败后重建 H2,不在 H1 上连续叠加参数。每个 clone 保存输入 hash、动作顺序、输出、 退出状态与结论。
“只有一份”时怎么办
若无法制作 snapshot/clone:
- 停止非必要写入和自动恢复;
- 记录为什么不能复制(空间、设备状态、加密、访问权);
- 评估块级镜像/专业数据恢复而非数据库内试错;
- 明确每个读取会否加剧介质故障;
- 升级事故级别和专业支持;
- 由数据 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。
自动化也会改现场
临时隔离后检查并暂停可能的自动动作:
不是所有自动化都要停,而是必须知道哪些会写现场,并由事故指挥统一决定。
现场保护完成定义
完成这些才进入下一节的分类。
返回本章目录 · 下一节:先分类再抢救 · 查看全书目录 · 查看索引中心
35.2 先分类再抢救
“corruption”不是一个恢复方案。至少要区分:存储的字节/页是否错误、从源数据派生的 结构是否错误、业务意义是否错误。检测工具、恢复来源和可接受损失都不同。
35.2.1 物理页、存储与 checksum 错误
物理异常的证据链
典型入口:
这些信号仍需定位:
checksum failure 说明“读出的 data page 与页内 checksum 不一致”,不自动说明是磁盘: 坏内存、DMA/controller、虚拟化、filesystem、软件 bug 或离线篡改都可能改变字节。
交叉副本不能只比较 SQL 行
在相同 system identifier/lineage 下,可比较:
物理 replica 可能从 primary 接收已经损坏的逻辑变化,也可能各自发生局部介质损坏; 共享存储/镜像又可能让“多个副本都有同样错误”并不独立。恢复源要有独立故障域与实际 校验,不是名字叫 replica 就可信。
默认路线
只有在没有健康来源且明确接受损失时,才考虑 best-effort extraction。
35.2.2 索引、排序规则与派生结构错误
派生结构可以重建,但先证明 source
index、materialized view、search vector、rollup 与 cache 都由别的状态生成。典型信号:
先关闭 planner 路径做诊断时,只在 clone 或有界 read-only 查询里进行:
这只能帮助比较,不是修复,也不能保证 seqscan 本身绕过 heap 损坏。
collation 是外部语义依赖
collation object 把 SQL 名称映射到 libc 或 ICU provider。text B-tree 的顺序依赖比较
规则;provider 升级后,旧 index 中 tuple 的物理顺序可能不再满足新比较规则。仅更新
collversion 会消除 mismatch 提示,却不会重新排列已有 index。
因此安全顺序是:
数据库级 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 全绿也可能数据完全错误
PostgreSQL 会把这些合法写入按 WAL、checksum 和 replication 正确保存。物理工具不会 知道“余额不应为负”或“订单总额应等于明细”。
业务不变量应提前定义:
跨行、跨表、跨系统不变量需要 reconciliation:
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 |
分类置信度
不要把一个工具的结果写成绝对结论:
pg_checksums 找到 1 个坏 checksum,不等于整个集群只有 1 处问题:可能还有未覆盖对象、
业务语义损坏或未来才读出的设备错误。反过来全绿也只覆盖被扫描的 data pages。
停止线
以下任一出现,路线转 STOP_AND_ESCALATE:
- system identifier、timeline 或 binary/PGDATA 归属不明;
- 原始 evidence 已被多次写入,动作时间线无法复原;
- checksum、amcheck、业务结果相互矛盾;
- 所有已知 backup/replica 可能共享故障;
- 唯一副本无法安全克隆;
- 需要
zero_damaged_pages、ignore_invalid_pages或pg_resetwal才能继续; - 数据损失范围涉及监管、财务、隐私或安全事件。
上一节:现场保护与操作边界 · 返回本章目录 · 下一节:页与 checksum 证据 · 查看全书目录 · 查看索引中心
35.3 页与 checksum 证据
PostgreSQL data checksum 是发现部分静默字节变化的重要机制,但不是万能完整性证明。 理解其单位、触发时机和未覆盖面,才能正确解释一次成功或失败的检查。
35.3.1 checksum 是否启用及其检测边界
先确认,而不是假设
在线只读确认:
data checksum 是 cluster 级属性,不按 database/table 单独启用。启用时,每个 data page
在写入时更新 checksum,读取时校验。PostgreSQL 18 上游 initdb 默认启用 checksum,
但 --no-data-checksums 或部署工具的显式选项仍可关闭;因此生产判断必须读取实际
control state,不能从版本号反推。
离线检查:
官方要求 PostgreSQL clean shutdown 后运行 pg_checksums。返回码为 0 表示本次
扫描没有 checksum error,非 0 表示至少检测到一次失败。不要只解析人类输出而忽略
exit status,也不要对正在运行或有其他 writer 的 PGDATA 执行。
如需限定已知 relation filenode,可用:
这适合复验定位,不替代 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:
使用 pg_relation_filenode/path,不要假设 OID 等于当前 filenode。TRUNCATE、REINDEX、
某些 ALTER/rewrites 会更换 filenode;tablespace 又改变相对路径。
PostgreSQL 默认 block size 常见为 8192,但应从实际 control data确认:
日志块号 $b$ 对应 relation fork 内的字节范围:
其中 $B$ 是实际 block size。这只是定位数学,不是授权用十六进制编辑器修改该范围。
先确定 fork 与 segment
大 relation 会分成 segment;relation 还有:
错误路径、block 和 relation identity 一起保存。index 的 block 2 与 heap 的 block 2 不是同一数据;同名 relation 在不同 database/tablespace 也不同。
日志与 SQLSTATE
保留:
本章正式 bit-flip case 的在线顺序扫描返回 XX001,但教材没有把 raw stderr 导出,
因为错误文本可能带 object/query 信息。生产证据可在受控位置保存完整原日志,再制作
去敏投影用于协作。
物理定位不等于根因定位
找到 base/5/16395 block 2 只说明错误出现在哪里。根因仍可能在:
需要跨层证据,而不是仅替换这一文件。
35.3.3 存储故障先修基础设施,再谈数据库重建
不要把健康数据恢复到坏底座
如果设备仍报告 error、filesystem 不稳定、memory test 未过或 hypervisor path 不可信, 在原主机恢复 backup 可能再次损坏干净数据。安全路线:
RAID rebuild、filesystem repair、cloud volume detach/attach 都会改变现场,应由对应专业
团队纳入同一时间线。数据库团队不要在唯一卷上自行执行 fsck -y。
健康底座的验收
“新 VM”也不自动独立:它可能仍使用相同宿主机、storage pool、image 或坏备份。
重建后仍要扫描
新副本需要:
若从 physical backup 恢复,损坏页可能被原样复制;“restore 成功”只说明工具完成传输。 第 21 章的 restore drill 要与本章的 integrity checks 组合。
本节结论
下一节处理 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_check、heapallindexed 与锁成本
选择正确强度
安装 supplied extension:
单个 B-tree 轻量检查:
检查 heap tuple 是否都有 index 表示:
更全面的 parent/child 与 root descent:
函数返回 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 一次性开最强选项。
一个风险排序:
执行前记录 relation size、锁等待、statement timeout、I/O 预算、replica lag 与停止线。
verify_heapam
它可按 block 范围返回 heap/tuple 结构问题,但 check_toast=true 较慢;若依赖结构本身
损坏,检查也可能 error,极端情况下存在 crash 风险。优先在 clone 执行,并对输出做
隐私审查;错误信息虽偏结构,仍可能泄露数据特征。
35.4.2 collation 版本变化与索引顺序异常
版本 mismatch
数据库 default collation:
provider:
操作系统或 ICU 升级可能改变 text comparison。旧 index 是按旧规则构建的,新 backend 按新规则搜索时,binary search 可能走错方向并返回错误答案。primary/standby provider 版本不一致也可能让只读副本先暴露问题。
枚举 index 的 collation 依赖
表达式 index、operator class、partition、materialized view 与 extension objects 还需结合
pg_depend、DDL 和应用查询盘点。indcollation=0 只表示该 index key 不使用 collation,
不是整个 object 无外部语义依赖。
修复顺序
在 provider 已稳定、工作副本验证后:
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。
至少组合:
查询等价性要控制 snapshot
若比较 seqscan 与 indexscan:
两个独立时间点的 count 不同,可能只是并发写,不是 index corruption。对生产主库最好 在 repeatable-read/read-only snapshot 或 clone 中比较。
REINDEX 的生产风险
普通 REINDEX 与 REINDEX CONCURRENTLY 的锁、空间、WAL、失败状态和支持对象不同。
损坏场景下 concurrently 也未必是正确路线:它需要继续依赖当前系统结构和写入并发。
先在 clone 证明:
如果坏的是 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 不写另一个 track 的来源。先得到可用候选,再按 RPO、完整性、时间和风险 选择,不要等在唯一 damaged instance 上的“修复”失败后才想起恢复 backup。
物理来源也可能复制损坏
backup 工具成功退出不等于每个 page 正确;physical replication/WAL 会忠实传播合法 page changes,也不能修复已在 source 中的逻辑错误。每个候选都跑:
35.5.2 对可读数据做分段抽取和校验
只在 clone 上建立 extraction map
先按稳定业务 key/partition 分块:
每块记录:
不要用 OFFSET/LIMIT 做可恢复分块;行在并发/错误下可能跳动,复杂度也高。ctid 可辅助
定位 physical block,但会随 UPDATE/VACUUM/rewrite 改变,不能作为业务去重身份。
二分定位只是抽取方法
若某 key range 读取失败,可以在 clone 上继续二分:
最终报告:
不能把“成功导出了 99% range”说成“只丢 1% 行”:坏块上的行数、TOAST 引用和跨表 关系可能未知。
导入到新库再验证
不要直接把 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 上的所有行,应在
已经放弃恢复该页后才考虑。它不是“自动修页”。
最低授权合同
危险参数只为了抽取仍可读数据,产出的 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 边界。
升级包
不要为了“给专家一个更干净的环境”先删除日志、重启十次或运行 repair。未经请求不要 把含 PII/凭据/密钥的完整 PGDATA 或日志发给外部支持;先走安全与法律批准、最小披露和 加密传输。
时间与选择权
专业支持不是承诺一定恢复所有数据,而是避免用不可逆试错继续缩小选择空间。若业务 deadline 更早,可并行恢复已验证 backup,同时保留 damaged original 供后续取证;恢复 服务与确定根因不必串行。
上一节:索引、collation 与 amcheck · 返回本章目录 · 下一节:工程取证与业务验证 ·
查看全书目录 · 查看索引中心
35.6 工程取证与业务验证
数据库抢救有两个交付对象:一个是可以承担服务的新状态,另一个是解释“原状态发生了 什么、哪些数据仍不确定”的证据链。只交付一个能启动的 cluster,会把未知损失留给 业务和下一次事故。
35.6.1 保留原始证据、操作副本和完整时间线
证据树
敏感原件放在受控 evidence store,协作仓库只放去敏 projection。不要因为教材/工单需要 “可见”就把 raw PGDATA、query、凭据或客户记录提交 Git。
append-only 时间线
每条事件:
后来的认识不要覆盖当时记录;追加 correction。这样复盘才能区分“当时可见事实”与 “事后才知道的事实”。
工具可复现性
保存:
同名 pg_checksums、amcheck 或 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:
业务不变量要有定义版本
只有 row count 不够。相同行数可以包含错误值、重复/缺失互相抵消、错误 tenant 或错误 时间窗。本章实验同时比较 row count、sum ID、sum balance 和有序内容 digest,但真实 业务还要使用有意义的 ledger/token。
抽样与全量
全量验证可能超过 RTO,可分层:
必须明确哪些是全量、哪些是抽样、抽样方法与遗漏风险。不能把“抽查 100 行正确”写成 “数据已完整恢复”。
35.6.3 记录不可恢复范围与合规沟通
用区间与集合表达 unknown
不要把 unknown 填成 0。若坏页内容不可读,精确行数可能无法知道;可以报告下界/上界 和估计方法。
技术 RPO 与业务损失分开
一个 RPO 数字不能覆盖四类损失。
合规与沟通
由法律、安全、隐私和业务 owner 判断是否触发通知。技术团队提供可审计事实:
对客户不要说“数据库坏了但已经修好”这种不可证实概括。应说明已确认影响、仍在验证 范围、临时控制和下一次更新时间;不公布攻击/隐私细节前先走授权。
结案条件
第 36 章将把这份证据包转化为长期控制。
上一节:抽取、跳过与重建策略 · 返回本章目录 · 下一节:实战:在克隆环境分类并恢复 · 查看全书目录 · 查看索引中心
35.7 实战:在克隆环境分类并恢复
前六节建立了抢救原则,本节把它们压进一次可复验的盲测。演练不是“制造一个错误, 然后按预先知道的答案修掉”,而是同时维护三条相互独立的证据链:
如果分类器偷看答案、恢复动作改写原始 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 的静态检查:
预期输出至少包含:
如需核对当前 Pigsty sandbox,capture 只通过 SSH 读取目标身份、拓扑和服务投影,不改
数据库:
完整演练则是 L3:会在 pg-test-3 创建私有临时 PostgreSQL 集群、停止并复制它,
再修改操作副本。因此 runner 要求五项精确 guard,缺一项就在创建远端目录之前失败:
这些值不是“我愿意承担风险”的通用开关。它们共同声明:目标正是本书的无数据、 无流量 sandbox,且授权范围只到 disposable clone。换成生产主机、生产数据或生产 流量后,本命令没有任何授权意义。
隔离布局
runner 在 pg-test-3 上创建:
临时实例设置 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.6 集群,包含 12,000 行确定性数据和 至少八个 heap block。源实例干净停止并冻结 known-good 后,runner 才为物理场景创建 独立 clone,并在 postmaster 已停止 时对目标 heap relation 执行:
这个动作的目的不是模拟真实磁盘故障机理,而是生成一个可重复的、checksum 能检测到的 坏页。实验同时保存 relation mutation 前后的 SHA-256;运行中的 relation file、 managed PGDATA 和唯一恢复来源永远不被改写。
随后取得两类互补证据:
离线检查回答“磁盘上的 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 的假值:
这个构造只模拟“catalog 记录的版本与当前 provider version 不同”。它没有安装另一版 ICU,也没有证明比较函数或索引顺序真的变化。相应证据是:
因此,“checksum 全绿”和“B-tree 结构检查通过”不能否定 collation-derived state 已经过期;反过来,version mismatch 也不能被夸大成 heap page 已损坏。
隐藏答案,分类证据
runner 随机安排两个场景,并给每个场景生成无语义的 case_id。classifier 只能读取
blind-packets.json 中七组字段:
场景标签与注入细节保存在独立的 hidden-answers.json,不作为 classifier 输入。合法
route 是显式、可评审的合取谓词:
若两个谓词同时成立,或两个都不成立,分类器必须返回:
未知状态不是“选一个最像的”。正确动作是保留原件、停止写入和重复实验,列出缺少的 事实与恢复来源,并在危险参数之前升级。
正式 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,再在新副本上完成:
四个业务不变量同时匹配才算实验恢复成功。row_count 能发现大块缺失,却发现不了值
被替换;两项求和能捕获部分数值漂移,却可能相互抵消;确定顺序的内容 digest 再覆盖
每行关键字段。真实系统还应加入账务平衡、对象存在性、外部事件和幂等副作用等业务
事实,不能照抄这四项就宣称可信。
原始 damaged case 在验证期间保持逐文件 digest 不变,直到最终统一清理。这样即使 恢复结果后来被推翻,调查者仍能从同一原件重新分叉。
collation 异常:先重建派生对象,后承认新版本
collation 场景也不直接操作 original case。runner 创建单独 working copy,并严格按 以下语义顺序执行:
第一步让 exact dependent index 按当前 provider 规则重建,第二步才把 catalog 中 stored version 刷新为 actual version。若颠倒顺序,warning 会消失,但旧派生对象 可能仍由旧规则构造;那只是消音,不是修复。
正式结果:
实际事故不能仅凭 collation 名称猜依赖关系。应先枚举所有依赖对象,确认 index、 partition、materialized view、constraint 与应用排序语义,规划锁和容量,再对 exact 对象重建。若 heap、checksum 或业务证据同时异常,应回到扩大调查范围,而不是继续套用 这条单因果 route。
验证证据包,而不是相信终端输出
一次完整 run 的关键产物是:
before.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 的情况下复验:
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。
宿主机重建前至少要具备:
- incident commander 明确 exact host、故障域与数据库权威所在;
- 原始证据和 storage snapshot 已保存在目标之外;
- 流量已排空,且健康节点或恢复环境拥有数据库权威;
- 已验证的 backup、健康 cluster source 或重建路径被明确命名;
- inventory、Pigsty release、package repository 与 secret source 全部固定版本;
- 替代容量、失败回退和生产 destructive approval 已就绪。
安全状态机是:
关键字是 clean 和 fresh。不要把可疑 PGDATA 当作新实例恢复源,不要在未保存 唯一证据时格式化磁盘,也不要因为 service 成功启动就跳过 lineage 与业务验收。原 PGDATA 只有在证据保留期、合规义务和 incident owner 共同批准后,才进入单独的清理 流程。
本实验刻意停在生产门外
正式演练只验证了 L3 rebuild decision contract,没有执行 managed host 的移除、 重装或恢复:
因此本节可以支持团队评审“何时应重建、重建应满足什么”,不能提供生产重建时长、 数据可恢复比例或变更批准。真实生产执行要重新解析 exact inventory、当前 Pigsty 版本、故障域、备份恢复演练结果和业务 RTO/RPO,并走独立破坏性门禁。
到这里,本章形成了一条完整原则:
保留原件,用证据分类;从可信来源生成新状态,用物理与业务不变量共同验收;只有在 基础设施可信度也恢复后,才让流量回归。
上一节:工程取证与业务验证 · 返回本章目录 · 下一章:事故复盘、控制固化与平台演进——举一反三 · 查看全书目录 · 查看索引中心