十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从 VACUUM FULL 到 REPACK:PostgreSQL 表重写技术的进化史

从 VACUUM FULL 到 REPACK:PostgreSQL 表重写技术的进化史 本文作者付超Oracle ACE 与 PostgreSQL ACE 双认证专家PG 分会西安用户组核心成员。长期从事数据库运维与技术布道工作具备 Oracle OCM、Kubernetes CKS 等多项专业认证。每个 DBA 都经历过这样的场景一张业务大表因为频繁更新膨胀到实际数据量的好几倍查询越来越慢。你决定用VACUUM FULL把它压缩回正常体积却发现命令一执行这张表的读写全部被锁死——业务侧立刻报警你只能灰溜溜地挑一个凌晨窗口再跑。这是 PostgreSQL 表维护里最经典的矛盾重写表需要空间上的大动干戈而业务不允许片刻停摆。围绕这个矛盾社区和内核团队先后给出了三代解决方案。这篇文章就沿着这条演进线讲讲VACUUM FULL、pg_repack和 PG19 的REPACK各自的思路、代价与取舍。一、表为什么要重写要理解三代方案先得明白重写rewrite和清理vacuum是两回事。PostgreSQL 基于 MVCC 实现多版本并发控制一条记录被 UPDATE 后旧版本不会立即删除而是留在原页面里成为死元组等 VACUUM 来回收。问题在于VACUUM 只是把页面内部的死元组标记为可复用并不会把空间还给操作系统——页面还在文件还在表瘦不下来。这就是所谓的膨胀bloat。要让表真正瘦身唯一的办法是重写把表中还活着的行原样拷贝到一个全新的物理文件中然后把旧文件整个丢弃。新文件里没有死元组留下的空洞磁盘空间自然就还给了操作系统。重写还有一个附加能力物理排序。如果把行按某个索引的顺序写入新文件那么范围查询时符合条件的数据会集中在相邻页面磁盘 IO 大幅减少——这就是聚簇CLUSTER的价值。所以三代方案其实都在做同一件事重写表。区别只在于一个核心问题——重写期间表上的并发变更插入、更新、删除怎么处理二、第一代锁住一切——VACUUM FULL 与 CLUSTER最朴素的做法也是最古老的做法重写期间给表加 ACCESS EXCLUSIVE 锁。VACUUM FULL把整张表重写到新文件只回收空间、不排序CLUSTER在重写的同时按指定索引排好物理顺序。两者的执行期间其他事务对这张表的一切读写都被阻塞直到重写完成。这套方案的优点是无可挑剔的简单内核自带、零依赖、没有任何前置条件数据安全上也是最保守的。对于中小表、或者能接受维护窗口的系统它至今依然是可靠的选择——PG19 也继续保留这两个命令的兼容。实际用起来也很直白-- 回收表空间不排序可指定单表或整库VACUUMFULLorders;VACUUMFULL;-- 当前库所有表-- 按索引物理排序聚簇CLUSTER ordersUSINGorders_pkey;-- 聚簇索引管理先配置默认聚簇索引再按它执行最后刷新统计信息ALTERTABLEorders CLUSTERONorders_pkey;CLUSTER orders;ANALYZEorders;-- 重写后必须刷新统计信息但它的天花板也很明显大表上不可行。一张 TB 级的表重写可能要几十分钟甚至更久期间业务完全停摆。在7×24 小时在线成为标配的今天这几乎等于宣判了第一代方案在大表场景下的死刑。社区因此开始思考能不能让重写在线进行三、第二代触发器与日志表——pg_repack 的巧劲既然重写期间业务不可能停下来那就让变更记下来事后补。这是 pg_repack 的核心思路也是它作为第二代方案的灵魂。这个工具 2011 年从更早的 pg_reorg 项目更名而来至今迭代到 1.5.3支持 PostgreSQL 9.5 到 18。它的执行过程可以讲成一个七步故事先建一张日志表给原表挂上一个 AFTER 触发器此后这张表上发生的每一次插入、更新、删除都会被同步记进日志表——这就是它的增量捕获机制与此同时pg_repack 把原表里的全部存活行拷贝到一张新表里可选按索引或指定列排序在新表上重建所有索引日志表里已经累积的变更被一批批回放到新表上新表追上进度后短暂加锁通过系统目录把新旧表连同索引和 TOAST 表瞬间交换删掉旧表收工。聪明的读者已经看到了关键ACCESS EXCLUSIVE 锁只在开头建日志表、结尾换文件这两个瞬间出现中间的漫长拷贝阶段只持 SHARE UPDATE EXCLUSIVE 锁——普通 DML 照常执行只有 DDL 会被挡住。锁窗口从全程压缩到了毫秒级的两头这就是在线重写的全部秘密。看一组实际用法# 安装与启用二进制编译安装 库内扩展两者版本必须匹配makesudomakeinstallpsql-dyour_db-cCREATE EXTENSION pg_repack# 在线回收空间--no-order 即等价 VACUUM FULLpg_repack --no-order-torders your_db# 在线聚簇默认按表已配置的聚簇索引排序pg_repack-torders your_db# 按任意列排序重写pg_repack --order-bycreated_at-torders your_db# 在线搬迁表空间--moveidx 连索引一起搬pg_repack-dyour_db-torders-sfast_tbs--moveidx# 先试跑预览再正式执行pg_repack-dyour_db-N代价当然也有而且不小必须有主键或 NOT NULL 唯一索引。触发器回放变更时需要靠主键定位行没有主键的表直接被拒之门外触发器带来写放大。重写期间每一次 DML 都要额外写一条日志表记录对写入密集的表是不小的负担极端情况下日志积压甚至会追不上1.5.x 提供了--switch-threshold、--apply-count等参数缓解UNLOGGED 表、临时表不支持也不能按 GiST 索引聚簇声明式分区父表无法整体处理作为第三方扩展它要求客户端二进制与库内扩展版本严格匹配故障后还可能残留临时对象需要手工清理。即便如此pg_repack 依然是 PG18 及以下版本做在线重写的事实标准。它的局限也很清晰所有能力都建立在触发器 日志表这个外部机制上——这像是一套加装的脚手架能用但始终不是数据库自己的一部分。四、第三代内核的逻辑解码方案——REPACKPG19 带来的REPACK把这件事从加装脚手架变成了原生能力。先说命名。VACUUM FULL和CLUSTER这两个命令其实做的是同一件事——重写表但一个名字让人误以为是 VACUUM 的加强版另一个借用了商业数据库聚簇的模糊概念功能重叠又互相混淆。PG19 干脆把它们统一成一个语义清晰的命令REPACK——重写表以回收磁盘空间。旧命令保留兼容但新名字承载了新机制。REPACK的普通模式与第一代无异全程独占锁真正亮眼的是CONCURRENTLY选项——它把 pg_repack 的增量捕获机制换成了内核自带的逻辑解码创建临时逻辑复制槽开始把表数据忽略死元组拷贝到新文件拷贝期间业务 DML 完全不阻塞发生的变更通过逻辑解码被暂存到临时文件数据拷贝完成后把暂存的增量变更回放到新文件上此时才请求 ACCESS EXCLUSIVE 锁——只用于交换新旧表与索引文件交换完成锁释放。和 pg_repack 相比最大的变化在于不再需要触发器也就没有 DML 写放大锁窗口理论上被压缩到交换文件的毫秒级。如果重写期间表的变更量巨大回放阶段仍会持锁锁时间可能从毫秒级拉长到分钟级——这是它唯一的软肋但可以通过错峰执行规避。实际用法如下-- 普通模式等价 VACUUM FULLREPACK orders;-- 聚簇等价 CLUSTERREPACK ordersUSINGINDEXorders_pkey;-- 无锁重写生产首选完成后自动 ANALYZEREPACK(CONCURRENTLY,ANALYZE,VERBOSE)orders;-- 全库当前用户有 MAINTAIN 权限的所有表不能在事务块内执行REPACK;-- 实时查看重写进度与阶段catch-up 回放变更swapping relation files 锁窗口SELECTpid,relid::regclassAStbl,command,phase,heap_blks_scanned,heap_blks_totalFROMpg_stat_progress_repack;作为内核公民它带来了一整套配套能力权限模型不再是 superuser/owner 特判而是标准的MAINTAIN权限符合 PG15 以来的细粒度权限体系监控内建pg_stat_progress_repack视图实时报告进度且能看到阶段流转——catch-up回放并发变更、swapping relation files锁窗口等锁是否异常一目了然参数配套max_repack_replication_slots默认 5仅启动时可调限定并发 CONCURRENTLY 的数量每个并发重写占一个逻辑复制槽。当然新机制也带来新的边界CONCURRENTLY 不支持 UNLOGGED 表、分区表、没有主键或索引型 replica identity 的表也不能在事务块内执行——逻辑解码需要行标识也需要复制槽这是机制本身的物理约束。五、给 DBA 的迁移建议PG18 及以下没有 REPACK 可选能停窗就用VACUUM FULL/CLUSTER需要在线就用 pg_repack前提是表有主键、非 UNLOGGED、重写期间无 DDL。PG19默认切换到REPACK (CONCURRENTLY)内核原生、无扩展依赖、无触发器开销、监控内建锁窗口最小。升级后建议先拿一张大表冒烟对比它和 pg_repack 的耗时与锁时间。仍要保留 pg_repack 的场景表空间在线迁移--tablespace、按任意列排序--order-by这两项 REPACK 目前没有等价选项另外 CONCURRENTLY 要求表已配置 replica identity未配置的表只能用普通模式。两边都不支持的UNLOGGED 表的在线重写、声明式分区父表的整体在线重写——这类场景只能接受停窗或对每个叶子分区逐个处理。别忘了max_repack_replication_slots只在启动时生效升级前就要在配置里定好并发预算。结语回看这条演进线其实是一个把重写变成一等公民的过程第一代用锁解决并发简单但粗暴第二代用触发器把锁窗口挤到两端却背上了主键约束和写放大第三代用内核逻辑解码把在线重写做成了数据库的原生能力。锁粒度越来越小、能力越来越内建——这是 PostgreSQL 表维护工具过去十几年的演化主线也是 REPACK 值得每一个 DBA 在 PG19 升级计划里留一个位置的原因。
返回列表