28.7 实战:建立维护节奏
前六节分别讨论了旧版本、autovacuum、冻结、膨胀、分区生命周期和物理完整性。
这些知识如果只停在若干 SQL 和参数上,仍然很容易变成“告警来了就跑一次
VACUUM”的被动运维。本节把它们收束成一条可重复的维护闭环:
本节同时提供一套可复现实验。它不是把固定阈值塞给读者,而是让读者亲眼验证四件事:
- 旧快照怎样改变普通
VACUUM的清理结果; - “空间已经可复用”和“文件已经缩小”为什么是两个结论;
- 索引检查、并发重建与分区退役怎样分别验收;
- 哪些异常仍属于日常维护,哪些必须立即转入资源事故或数据救援。
28.7.1 制造膨胀、长事务与分区到期
先读实验合同
本章实验只允许在已经确认的 Pigsty pg-test 开发沙箱执行,正式参考环境为:
完整合同在
static/labs/ch28/lab-contract.md,机器可校验的要求与动作白名单
分别在
requirements.json 和
maintenance-contract.json。
实验只创建带随机 run_id 注释的一次性数据库和角色。为避免 exporter 在建库与授权之间
抢先连接,runner 依次执行:
所有表都位于夹具数据库。只有 maint.churn 的表级 autovacuum 被临时关闭,以便让手工
VACUUM 的因果关系可复现;这不是生产建议。实验明确禁止:
换言之,这里制造的是可丢弃夹具上的现象,不是在真实库中“先破坏再学习”。
先锁定输入与上游证据
capture 在任何写入前验证:
- 第 19 章部署证据仍指向同一个非生产沙箱;
- 第 25 章确认目标为主库,并保留观测基线;
- 第 27 章没有把试验参数持久化;
- 数据库与角色在起点均不存在;
amcheck、pg_freespacemap、pg_visibility、pgstattuple可安装;- PostgreSQL 设置、复制槽、prepared transaction、文件系统空间和校验和状态可读;
- 11 个实验源文件的 SHA-256 与随后执行的版本一致。
这一步解决一个经常被忽略的问题:如果运行期间脚本、目标或前置状态发生变化,最终数字 即使“看起来正确”,也不能归到当前实验设计上。runner 因此在 capture 之后再次计算源文件 散列,不一致就失败关闭。
创建三组现象
夹具包含两类表。
第一类是一个 fillfactor = 70 的 heap,共写入 60,000 行,并建立主键和
(status, id) B-tree:
第二类是按日期范围分区的事件表:
数据内容、行数和边界都是确定的,因此归档前后可以比较:
仅比较文件大小或只执行一次 count(*) 都不够:前者不能证明逻辑内容,后者无法发现
同样行数下的值篡改。
建立一个可识别的旧快照
实验另开一个连接:
连接的 application_name 固定为 pg36-ch28-old-snapshot。runner 必须从
pg_stat_activity 同时看到:
只有这五个条件全匹配,后续才允许释放该会话。脚本不使用模糊的 query 文本、不按用户名
批量杀连接,也不把所有 idle in transaction 一锅端。
接下来在另一个事务中制造 churn:
结果应为:
remaining_rows 必须在下一条 SQL 命令中读取。PostgreSQL 的 data-modifying CTE
共享同一个命令级快照;若在同一条语句里再次 count(*),读到的仍可能是修改前的
60,000 行。这不是数据库“少提交了一次”,而是命令快照语义。类似地,不能让
UPDATE 与 DELETE 命中同一批行,再假设两个子语句会按书写顺序串行处理。
为什么这组夹具有教学价值
这组实验同时保留了四条互不替代的证据线:
| 现象 | 主要证据 | 回答的问题 |
|---|---|---|
| 旧快照 | backend_xmin、精确 holder identity |
谁还需要旧版本 |
| heap churn | pgstattuple、FSM、VM、关系大小 |
旧版本是否清掉、空间去哪里 |
| 索引维护 | amcheck、catalog、relfilenode |
结构是否通过检查、重建是否完成 |
| 分区到期 | 分区拓扑、CSV、manifest、回灌 | 数据是否先可恢复、再退出热表 |
任何一列都不能替代其他列。n_dead_tup 是估计值;文件大小不是可见性;amcheck
不是备份;CSV 存在也不等于可恢复。
28.7.2 从指标与原生视图判定维护优先级
先按风险排序,不按表大小排序
维护队列应该先回答“拖延会造成什么”,而不是“哪个数字最大”。一个实用的三层优先级是:
因此,一个 relfrozenxid 年龄危险但只有 2 GB 的表,可能比一个 2 TB、膨胀 20%、
仍有充足空间且正常被 vacuum 的表更紧急。维护分数可以帮助排序,但不能把 P0 平均进
一个漂亮的加权总分:
同层内再用 headroom、增长速率、业务关键度、预计锁时间和维护成本排序。
第一屏:全库安全边界
先看数据库年龄、活动快照、prepared transaction 和复制槽:
不要把“最老连接”自动等同于“清理阻塞者”。真正相关的是它是否持有旧
backend_xmin、prepared XID 或 slot xmin/catalog_xmin,以及时间线是否与问题吻合。
也不要看到 age() 大就立即运行一条万能命令;先按 28.3 的版本化流程判断处于常规态、
迫近 failsafe,还是已经进入事务 ID 硬停机状态。
第二屏:对象触发与进度
候选表至少需要这些原生信息:
其中:
pg_stat_user_tables是累计统计和估计,适合发现趋势,不是物理真值;pg_class给出 catalog 估计、年龄和大小,仍不能直接证明 dead tuple 百分比;pg_stat_progress_vacuum只能描述正在运行的进度,phase 切换不是线性 ETA;- 需要高成本物理确认时,再在已选对象上使用
pgstattuple,不要全库高频扫描; - 用
pg_visibility_map_summary和pg_freespace分别观察 VM 与 FSM,但不要把它们 解释成业务行正确性。
例如对单个已获批候选:
将 Pigsty 看板与 SQL 对齐
Pigsty 的 PostgreSQL 监控提供数据库、实例、表、查询、复制、WAL、磁盘和 autovacuum 等多层视图。看板负责快速发现关联:
原生视图负责复核对象、持有者、年龄、命令 phase 和 catalog 状态。正确的工作方式是:
不要从一张图直接跳到 VACUUM FULL、REINDEX 或终止会话。图表的采样、标签聚合和
保留周期都可能掩盖瞬态事实;反过来,单次 SQL 也无法替代时间序列。
本次实验如何判定
正式 run 的基线与 churn 后快照同时记录:
优先级判断如下:
- 没有 XID/MXID 或完整性 P0 信号;
- 人工旧快照明确保留 50,000 个物理 dead tuples,先解除精确保留者;
- 普通 vacuum 之后确认空间回收语义,再决定是否需要文件重写;
- 索引检查、并发重建和分区到期作为独立、可验收的维护动作执行。
这里的重要判断不是“dead tuple 多,所以 vacuum”,而是“先证明谁让 vacuum 不能完成, 再移除那个精确原因”。
28.7.3 执行清理、检查和分区退役并验证副作用
运行完整闭环
先做不接触数据库的合同检查:
然后为每次正式实验使用一个不存在或为空的私密绝对目录:
all 的顺序固定为:
也可以逐步执行:
capture 与 all 拒绝覆盖非空证据目录。私有目录包含原始 SQL 输出、进度采样、
归档 CSV、清理记录和 source hashes;公开仓库只保存字段白名单后的
maintenance-run.json,不保存口令、连接串、SSH
材料或原始业务数据。
第一步:旧快照存在时只运行普通 VACUUM
在 holder 仍有非空 backend_xmin 时,runner 对夹具表运行普通 VACUUM,并以较低的
session-local cost 设置放慢它,以便旁路采样:
正式 run 结果:
| 项目 | 结果 |
|---|---|
| 初始行数 | 60,000 |
| 更新行数 | 40,000 |
| 删除行数 | 10,000 |
| 当前行数 | 50,000 |
观察到 holder backend_xmin |
是 |
| vacuum 后物理 dead tuples | 50,000 |
| 进度采样 | 128 |
| 观察到 phase | initializing、scanning heap |
这不是“VACUUM 失效”。它遵守 MVCC,不能移除那个旧快照仍可能读取的版本。若此时反复
提高 cost limit、增加 worker 或改用 VACUUM FULL,都没有解决保留边界,反而会把
问题扩大成资源或锁事故。
第二步:只释放精确 holder,再完成冻结与统计
runner 只允许终止同时匹配数据库、角色、application、记录 PID 和非空
backend_xmin 的那一个夹具会话。任一属性改变都拒绝动作。释放后执行:
这里的 FREEZE 是为了在可控夹具上展示冻结与 VM 变化,不是声称所有日常 vacuum 都
必须加 FREEZE,也不是 PostgreSQL 18 已进入 XID 硬停机后的万能命令。
正式结果:
| 证据 | 基线 / 旧快照阶段 | 释放后 |
|---|---|---|
| 物理 dead tuples | 50,000(旧快照下 vacuum 后) | 0 |
| heap bytes | 61,440,000(初始) | 87,040,000 |
| FSM 可用字节 | — | 51,920,000 |
| all-visible pages | — | 10,625 |
| all-frozen pages | — | 10,625 |
age(relfrozenxid) |
14 | 2 |
| 进度采样 | 128 | 171 |
| 释放后 phase | — | scan heap、vacuum indexes、vacuum heap |
结论需要精确措辞:
初始 heap 为 61,440,000 字节,churn 后最终仍为 87,040,000 字节。普通 VACUUM
成功清理,并不承诺把中间空洞归还操作系统。它在适当条件下可能截断文件末尾的空页,
但这次证据不能推导出“普通 vacuum 永不缩文件”,也不能推导出“文件没缩,所以 vacuum
没用”。
第三步:检查索引,再验证并发重建
runner 先对主键执行两层 amcheck:
正式 run:
第二种检查更强,但会取得 ShareLock,阻塞并发 DML 与 VACUUM;不能因为本次只需
0.08 秒就假设大表也能随时运行。生产中应根据表大小、缓存状态、业务窗口和副本角色
安排,必要时先用较轻的 bt_index_check 或 pg_amcheck 分批巡检。
然后对次级索引执行:
验收不止是“命令返回 0”:
| 条件 | 正式结果 |
|---|---|
relfilenode 改变 |
是 |
| 原大小 | 3,227,648 bytes |
| 新大小 | 1,589,248 bytes |
| 同名有效索引 | 1 |
indisvalid / indisready / indislive |
全部为真 |
| 无效夹具索引 | 0 |
_ccnew / _ccold 残留 |
0 |
尺寸变小是这次数据分布的结果,不是并发重建的固定收益。真正的最低验收是新索引有效、 唯一目标明确、没有异常残留,并且写放大、临时空间、WAL、复制延迟和锁等待仍在预算内。
第四步:先分离、归档与回灌,再删除
过期分区执行:
DETACH ... CONCURRENTLY 不能放在显式事务块中;它分两次事务完成,存在 default
partition 时也不能直接使用。若中途留下 pending detach,应该检查 catalog 与作业状态,
按实际版本使用 FINALIZE,而不是盲目再次执行或直接 drop。
正式 run 的顺序和结果:
SHA-256 完整值在公开结果文件中为:
独立回灌表的行数、日期范围、金额合计和有序 digest 必须与 detach 前 manifest 一致。
只有 round_trip_validated = true 后,runner 才允许删除原分区。这条门槛把“对象已经
离开热路径”和“数据已经可以销毁”明确分开。
第五步:证明副作用已经收束
正式 run 最终证明:
验证器拒绝 28 个预声明反例,并对 14 个现场证据 mutant 验证失败关闭。review 校验私有
证据文件、归档、源散列和公开摘要。这样的负向验证很重要:只证明“正确文件能通过”
无法证明校验器真的会拦住数据库未清理、索引无效、digest 不一致或误用
VACUUM FULL 等失败状态。
一次沙箱成功只说明合同内流程可执行。它没有证明:
- 同样动作在生产表上的锁时间和 I/O 成本;
- 当前备份真的可恢复;
amcheck能替代 checksums、pg_verifybackup或恢复演练;- 生产窗口已经获批;
- 任何固定 dead tuple 比例适合所有表。
28.7.4 输出维护清单及 ch34/ch35 的安全路由
把“维护”拆成不同频率
健康维护不是每月一次“大扫除”,而是多种频率的闭环。
| 频率 | 观察与动作 | 必须留下的证据 |
|---|---|---|
| 连续 | XID/MXID headroom、磁盘、autovacuum backlog、复制槽、长事务、错误日志 | 告警身份、开始时间、当前 headroom |
| 每日 | top dead/churn 表、长事务、slot/prepared xact、失败 vacuum、分区边界 | 排序快照、owner、处置状态 |
| 每周 | autovacuum 覆盖、统计陈旧、HOT 比例、FSM/VM 抽查、索引增长 | 趋势、候选对象、是否需要实验 |
| 每月 | 表级 override 审计、冻结策略、空间与 WAL 预算、归档回灌抽检 | 参数来源、容量预算、恢复证据 |
| 每季度 | checksums/pg_amcheck 策略、备份 manifest、整库恢复演练 |
完整性报告、恢复 RTO/RPO |
| 事件驱动 | 大批量导入/更新/删除、版本升级、分区切换、schema change 后 | 前后统计、对象状态、回退结果 |
频率不是硬编码。写入速率、表大小、SLO、冻结 headroom 和恢复要求决定实际周期。 关键是每项都有 owner、阈值来源、验收和升级路线。
一张可执行的生产工单
任何主动维护动作至少填写:
没有 target OID、精确命令、预算和停手条件的工单,不应进入生产。
Pigsty 命令是入口,不是审批
在目标节点上,Pigsty 提供本地 PostgreSQL 维护封装:
它们分别封装 vacuumdb 或 pg_repack,降低日常操作摩擦,但不改变 PostgreSQL
本身的锁、WAL、磁盘、MVCC 和版本语义。特别注意:
- 不要把
pig pg vacuum mydb --full当作常规清理;VACUUM FULL需要ACCESS EXCLUSIVE并重写表; pig pg freeze适用于明确的冻结任务,不是 PostgreSQL 18 事务 ID 硬停机状态下 可以无条件执行的急救口诀;pig pg repack --plan先列出计划对象,但正式执行仍要预算额外空间、写放大、 WAL、复制延迟和最终锁;pig pg kill默认 dry-run 是好习惯;即使加-x,也必须先用 PID、数据库、角色、 application、事务时间和保留证据锁定目标。
平台让命令一致,证据链才决定命令是否应该执行。
明确停手条件
以下任何条件出现,都应停止扩大动作,保留现场并重新判断:
“命令还在跑”不是继续等待的充分理由;“已经跑了很久”也不是取消的充分理由。应根据 预先声明的预算、progress phase、阻塞图和副作用趋势决定。
分流到第 34 章:资源与过载事故
如果数据结构没有明确损坏,但维护动作或 backlog 正在威胁服务,应转入 第 34 章:过载保护与资源故障判型:
第 34 章回答的是“如何止血、保护前台、恢复资源 headroom,并在容量与并发边界内重排 维护”,不是在压力中继续加大 vacuum 或 reindex 并发。
分流到第 35 章:数据救援与取证
出现以下证据时,应停止把问题称为“普通膨胀”,转入 第 35 章:数据抢救与工程取证:
进入救援路线后,优先保护证据和可恢复性:
不要在唯一生产副本上反复尝试 zero_damaged_pages、手工删文件或未经验证的 catalog
修改。这些动作可能把可调查的局部损坏变成不可逆的数据丢失。
本章最终验收清单
完成第 28 章后,读者应该能逐项回答:
- 我能用触发公式、表级 override 与版本参数解释 autovacuum 为什么启动;
- 我能区分统计估计、物理抽查、VM、FSM 和关系大小各自证明什么;
- 我能找到实际保留旧版本的事务、prepared xact 或 replication slot;
- 我能同时检查 XID 与 MXID headroom,而不是只背一个 wraparound 数字;
- 我知道普通
VACUUM、VACUUM FULL、REINDEX CONCURRENTLY与pg_repack的锁、空间和 WAL 边界; - 我能把分区 detach、归档、回灌验证和最终删除拆成独立门槛;
- 我能为
amcheck选择检查层级与窗口,并知道它不能替代备份恢复; - 我能写出含目标、假设、预算、停手、验收和恢复的生产维护工单;
- 我能判断事件应留在日常维护,还是升级到第 34 或第 35 章;
- 我不会把一次沙箱成功描述成生产安全证明。
本章参考实验的最终判定为:
这三个结论缺一不可:流程已经跑通,副作用已经收束,但生产变更仍未获授权。
进一步阅读:
- PostgreSQL 18:Routine Vacuuming
- PostgreSQL 18:VACUUM
- PostgreSQL 18:VACUUM Progress Reporting
- PostgreSQL 18:Table Partitioning
- PostgreSQL 18:REINDEX
- PostgreSQL 18:amcheck
- Pigsty:PostgreSQL Monitoring
- Pigsty:
pig pgMaintenance Commands
上一节:amcheck 与例行完整性检查 · 返回本章目录 · 下一章:移花接木:逻辑复制、迁移与异构同步 ·
查看全书目录 · 查看索引中心