28.6 `amcheck` 与例行完整性检查
备份没有报错,不表示每个 B-tree 都满足搜索不变量;checksum 开启,也不表示 heap 与 index 逻辑一致。
完整性不是一个布尔值,而是多层故障面:
page bytes
-> relation structure
-> heap/index logical correspondence
-> constraints and business invariants
-> backup artifacts
-> actual recoverabilityamcheck 覆盖其中重要的一段:heap 与若干 index access method 的结构/逻辑检查。它是
检测工具,不是修复器,更不是 backup 或 restore 的替代品。
28.6.1 bt_index_check 与更深检查的成本
bt_index_check:日常 B-tree 基线
CREATE EXTENSION amcheck;
SELECT bt_index_check(
index => 'app.orders_pkey'::regclass,
heapallindexed => false,
checkunique => true
);它检查多种 B-tree 不变量。若发现逻辑不一致,通常抛错;无输出/无错意味着:
在本次检查覆盖的范围内没有发现问题。
不意味着:
这个索引、heap、存储设备和所有备份绝对无损坏。
锁:
index AccessShareLock
heap AccessShareLock与普通 SELECT 使用的 relation lock mode 相同。官方文档把它视为 live production
日常轻量检查的较好折中。
先筛合法对象
批量检查不要把 temp、invalid、非 B-tree 对象误传:
SELECT
bt_index_check(
index => c.oid,
heapallindexed => i.indisunique,
checkunique => i.indisunique
),
n.nspname,
c.relname,
c.relpages
FROM pg_index AS i
JOIN pg_class AS c ON c.oid = i.indexrelid
JOIN pg_namespace AS n ON n.oid = c.relnamespace
JOIN pg_am AS am ON am.oid = c.relam
WHERE am.amname = 'btree'
AND c.relkind = 'i'
AND c.relpersistence <> 't'
AND i.indisready
AND i.indisvalid
ORDER BY c.relpages DESC;对 unique index,checkunique=true 检查重复 entry 中不应有多个可见版本;这是额外
工作,但更贴合 uniqueness 语义。
bt_index_parent_check:更强锁、更深结构
SELECT bt_index_parent_check(
index => 'app.orders_pkey'::regclass,
heapallindexed => true,
rootdescend => true,
checkunique => true
);它是 bt_index_check 的超集:
- 检查 parent/child relationship;
- 检查缺失 downlink;
rootdescend=true为每个 leaf tuple 从 root 重新搜索;- 可结合 heapallindexed/checkunique。
代价:
index ShareLock
heap ShareLock这会阻止 concurrent INSERT/UPDATE/DELETE,也阻止 relation 的 VACUUM 和其他
utility command。锁只在函数运行期间持有,不是整个外部 transaction 都持有;但大型
索引检查时间可能很长,所以仍需要窗口。
它不能在 hot standby 上执行;bt_index_check 可以。不要在 replica 迁移脚本里把两者
当同义函数。
rootdescend 不一定是最有价值的生产检查
rootdescend 最初也服务于 B-tree feature development。它可能显著增加资源和时间,
但对现实中某些损坏类型的额外检出价值有限。
分层策略:
routine:
bt_index_check
selected stronger check:
bt_index_check + heapallindexed + checkunique
maintenance window / incident:
bt_index_parent_check
optional rootdescend不要因为参数叫“更彻底”就每天全库打开。
不止 B-tree
PostgreSQL 18 amcheck 还提供:
gin_index_check
verify_heapamverify_heapam 检查 table/sequence/materialized view 的物理格式和逻辑结构,返回每个
发现问题的 block/offset/attribute/message。
SELECT *
FROM verify_heapam(
relation => 'app.orders'::regclass,
on_error_stop => false,
check_toast => false,
skip => 'none',
startblock => NULL,
endblock => NULL
);边界:
check_toast=true很慢;- TOAST 或其 index 损坏时,检查 toast value 理论上可能导致 server crash,很多情况 会只报错,但不能当无风险;
skip=all-visible/all-frozen降低成本,也降低覆盖;startblock/endblock可做 chunk;- 依赖的内部设施本身损坏时,函数可能无法继续。
对疑似 heap corruption,先按事故窗口、备份和证据保全运行,不要把全表
verify_heapam(check_toast=true) 当 cron。
调试日志
交互诊断可:
SET client_min_messages = DEBUG1;
SELECT bt_index_check('app.orders_pkey', true, true);会显示更多检查上下文。生产自动化默认不应把 DEBUG 细节写入公开日志;错误信息可能 泄漏数据结构或可推断内容。
权限不是“能执行即可”
amcheck function 可授权给非超级用户,但官方文档提示安全与隐私风险。独立维护角色应:
no application writes
no broad role membership
function execute only where needed
secure log/evidence destination
audited schedule
no public error payloadPigsty managed cluster 可用专门运维角色和私有日志采集;不要让普通应用角色在任意表上 调用结构取证函数。
28.6.2 heapallindexed、锁与业务窗口
heapallindexed 回答更强的问题
普通 B-tree structure check 主要从 index 看 index。heapallindexed=true 增加:
每一个应该有 index entry 的 heap tuple,是否都能在目标 index 的摘要结构中找到?
内部类似一次“dummy CREATE INDEX CONCURRENTLY”:
scan target index
-> build in-memory fingerprint summary
-> scan heap as hypothetical index input
-> verify expected entries are represented这能发现 heap/index 不一致,而这种 cross-check 不会在普通 index scan 中自动完成。
它是概率摘要
摘要受 maintenance_work_mem 限制。PostgreSQL 官方说明,为使每个应被索引的 heap
tuple 漏检不一致的概率不超过约 2%,近似需要每 tuple 2 bytes memory;更少内存时,
漏检概率缓慢上升。
因此:
heapallindexed passed once仍不是数学上的 100% 证明。例行重复检查会给单个缺失/畸形 tuple 新的发现机会。
计划 memory:
$$ M_{\text{fingerprint}} \approx 2 \times N_{\text{tuples}} $$
只是质量量级,不是固定 allocation。对于 1 billion tuples,2 GB 量级已超过许多默认
maintenance_work_mem;不能以为 boolean 参数零成本。
heapallindexed 不改变 relation lock mode
对同一个函数:
bt_index_check(heapallindexed=false/true)
-> AccessShareLock remains
bt_index_parent_check(heapallindexed=false/true)
-> ShareLock remains但运行时间和 I/O 通常增加数倍,锁持有时长随之增加。锁 mode 没变,不等于业务 影响没变。
业务窗口要看四个预算
scope:
relations: [...]
total_bytes: ...
total_tuples: ...
locks:
function: bt_index_check
mode: AccessShareLock
lock_timeout: ...
resources:
maintenance_work_mem: ...
jobs: ...
io_budget: ...
cpu_budget: ...
service:
p95_budget: ...
replica_lag_budget: ...
abort_threshold: ...若使用 parent check,再明确:
write blocking expected
maximum check duration
application retry behavior
DDL/vacuum exclusionpg_amcheck 批量编排
PostgreSQL client utility:
pg_amcheck \
--database=appdb \
--schema=app \
--heapallindexed \
--checkunique \
--progress \
--jobs=2更深:
pg_amcheck \
--database=appdb \
--table=app.critical_orders \
--parent-check \
--heapallindexed \
--checkunique \
--jobs=1注意:
--parent-check/--rootdescend会用更强 relation locks;--rootdescend隐式选择 parent check;--jobs是并发连接,直接放大 server I/O/CPU/lock impact;- 默认选 table 时也检查 dependent B-tree indexes 和 TOAST;
--install-missing会改变数据库,应纳入 extension change,而不是巡检时隐式执行;- PostgreSQL 18 的
pg_amcheck文档说明该工具面向 PostgreSQL 14+ server; - client/server 版本和 option 集必须在 automation 中记录。
不要一次扫全库最大并发
较稳妥:
catalog + small critical indexes
-> largest/highest-risk index batches
-> rotating coverage
-> periodic deep window对象优先级:
constraint/primary indexes
high write volume
recent crash/storage incident
collation version change
replica divergence suspicion
large/old/rarely read objects
previous invalid/reindex artifact同一时段避免叠加:
backup full scan
vacuum freeze
reindex
scrub
bulk load
major analytical scan结果模型
每个对象至少记录:
{
"database": "appdb",
"relation": "app.orders_pkey",
"function": "bt_index_check",
"heapallindexed": true,
"checkunique": true,
"parent_check": false,
"rootdescend": false,
"started_at": "...",
"ended_at": "...",
"server_version": "18.x",
"maintenance_work_mem": "...",
"result": "passed",
"error_sqlstate": null
}“cron exit 0”没有对象级 coverage,无法证明哪些 relation 被跳过。
正式实验
一次性 churn_pkey:
bt_index_check(
heapallindexed=true,
checkunique=true
) pass, 0.0597 s
bt_index_parent_check(
heapallindexed=true,
rootdescend=true,
checkunique=true
) pass, 0.0789 s表仅 50,000 live rows、无 concurrent business writes,时间只用于证明流程,不能作为 生产吞吐基准。实验在检查后还执行 concurrent reindex,并验证:
one final named index
indisready=true
indisvalid=true
indislive=true
invalid fixture indexes=0
cc artifacts=0完整性检查与重建后 catalog 状态形成闭环。
28.6.3 amcheck 不替代数据页 checksum 和备份恢复
四种证明覆盖不同问题
| 机制 | 主要回答 | 不能回答 |
|---|---|---|
| data checksum | 读到的数据页 bytes 是否匹配上次写入 checksum | B-tree 排序/heap-index 逻辑、可恢复性 |
amcheck | relation/access-method 结构与部分逻辑对应是否一致 | 所有磁盘 bytes、所有业务语义、备份可用 |
| backup manifest verification | 备份文件/size/checksum/所需 WAL 是否匹配 manifest | server 一定能恢复、业务结果正确 |
| restore drill | 能否启动、replay、打开并验证数据/SLO | 所有未来故障都可恢复 |
这四层是相加,不是互相替代。
data checksum 的边界
PostgreSQL 18 默认启用 page checksum,但可被 cluster-level 禁用。确认:
SHOW data_checksums;启用时:
- data page 写入时更新 checksum; -每次读取 page 时验证; -只保护 data pages; -不覆盖内部数据结构和 temporary files; -cluster 级启停,不是 per-table。
page 已在 shared buffer 时,amcheck 可能检查的是 buffer 中版本,不一定在该时刻重新读 filesystem;它若触发磁盘读且 checksum 失败,也可能报 checksum error。
第 28 章 run 记录:
data_checksums = on但这仍不意味着 storage、RAM 或所有 page 被本次实验读过。
amcheck 能发现 checksum 看不到的东西
例如:
operator class violates ordering rules
OS collation behavior changes
primary/standby collation environment differs
heap tuple missing corresponding index entry
access method implementation bug
logical structure valid bytes but wrong links/orderchecksum 只知道 bytes 是否与写入时一致;错误逻辑也可以被“正确”地写入并拥有有效 checksum。
备份验证也不是恢复
pg_verifybackup 可:
- 读 backup manifest;
- 检查 system identifier/manifest checksum;
- 对比缺失、额外、size 不同文件;
- 比较 file checksum;
- 对 plain backup 解析恢复所需 WAL。
官方文档同样明确:
即使 verify 通过,也应做 test restore,并验证数据库可运行、数据正确。
原因:
manifest cannot prove every server recovery check
valid WAL checksum can still encode nonsensical action due to bug
archive access/KMS/network may fail
recovery config/timeline/target may be wrong
application schema/semantic checks may fail
RTO may exceed objectivePigsty 的 pgBackRest backup/PITR 流程应同时产出 repository check、restore drill 和业务 验收;第 31 章会完整展开恢复证明。
amcheck 通过不等于“备份健康”
可能:
live primary amcheck pass
backup missing WAL
restore impossible也可能:
backup files verify pass
live index logically inconsistent还可能:
primary index inconsistent
standby has different corruption state需要按 failure domain 分别检查。
发现 corruption 后不要立即“修”
第一反应不应是:
REINDEX everything
ignore_checksum_failure=on
zero damaged page
delete relation file
fail over blindly先:
1. preserve error, SQLSTATE, block/index identity and logs
2. identify primary/replica/timeline/version/storage
3. stop avoidable writes to affected scope
4. check whether error reproduces on another copy
5. verify checksums, storage/kernel events and recent changes
6. validate backup and recovery points
7. classify heap vs index vs VM vs catalog vs hardware
8. choose repair/rebuild/restore with evidence如果确认只有可重建 secondary index 损坏,reindex 可能合理;如果 heap、TOAST、system catalog 或 multiple copies 损坏,简单 reindex 可能失败或掩盖证据。
转入:
不同结果的安全路由
| 结果 | 动作 |
|---|---|
| check pass | 记录 coverage/time/version;继续其他层 |
| lock timeout | 本轮 inconclusive;换窗口,不算 pass |
| query canceled/resource gate | inconclusive;缩 scope/jobs |
| checksum failure | corruption incident;保全证据 |
| amcheck invariant error | corruption incident;定位 heap/index |
| server crash during deep check | highest severity;停止重复触发 |
| invalid index only | 依赖/heap 验证后评估 concurrent reindex |
| backup verify pass | 仍做 restore drill |
例行计划
continuous:
checksum/log/storage alerts
daily:
metadata inventory, invalid indexes, recent error correlation
weekly rotating:
bt_index_check on critical/high-change B-trees
monthly/window:
heapallindexed selected objects
backup manifest verification
quarterly / after incident:
parent-check/deep checks where justified
full restore drill + business validation
after upgrade/collation/storage event:
targeted expanded coverage频率应按数据变化量和 failure risk,不按“每月一号”机械套用。
完整性 SLO
coverage:
critical_btree_days: 7
all_btree_days: 30
deep_selected_days: 90
backup:
manifest_verify_each_backup: true
restore_drill_days: 30
response:
checksum_or_amcheck_error_page_minutes: 5
preserve_evidence: true
automatic_repair: false这样“例行检查”才是可审计服务,不是散落脚本。
本节检查清单
object type / validity / persistence
bt_index_check vs parent-check selection
heapallindexed/checkunique/rootdescend flags
relation lock mode and duration
maintenance_work_mem and jobs
I/O/CPU/service budget
object-level coverage record
private error evidence
data_checksums state
backup manifest verification
restore drill recency
corruption response route
no automatic destructive repair延伸阅读
- PostgreSQL 18:amcheck
- PostgreSQL 18:pg_amcheck
- PostgreSQL 18:Data Checksums
- PostgreSQL 18:pg_verifybackup
- PostgreSQL 18:Backup Manifest Format
上一节:分区生命周期 · 返回本章目录 · 下一节:实战:建立维护节奏 · 查看全书目录 · 查看索引中心