跳至内容
28.6 `amcheck` 与例行完整性检查

28.6 `amcheck` 与例行完整性检查

备份没有报错,不表示每个 B-tree 都满足搜索不变量;checksum 开启,也不表示 heap 与 index 逻辑一致。

完整性不是一个布尔值,而是多层故障面:

page bytes
  -> relation structure
      -> heap/index logical correspondence
          -> constraints and business invariants
              -> backup artifacts
                  -> actual recoverability

amcheck 覆盖其中重要的一段: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_heapam

verify_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 payload

Pigsty 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 exclusion

pg_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 是否匹配上次写入 checksumB-tree 排序/heap-index 逻辑、可恢复性
amcheckrelation/access-method 结构与部分逻辑对应是否一致所有磁盘 bytes、所有业务语义、备份可用
backup manifest verification备份文件/size/checksum/所需 WAL 是否匹配 manifestserver 一定能恢复、业务结果正确
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/order

checksum 只知道 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 objective

Pigsty 的 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 可能失败或掩盖证据。

转入:

第 35 章:数据抢救与工程取证

不同结果的安全路由

结果动作
check pass记录 coverage/time/version;继续其他层
lock timeout本轮 inconclusive;换窗口,不算 pass
query canceled/resource gateinconclusive;缩 scope/jobs
checksum failurecorruption incident;保全证据
amcheck invariant errorcorruption incident;定位 heap/index
server crash during deep checkhighest 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

延伸阅读


上一节:分区生命周期 · 返回本章目录 · 下一节:实战:建立维护节奏 · 查看全书目录 · 查看索引中心

最后更新于