
开头先说个我踩过的坑。去年有套核心系统的订单表才 60GB平时查询都在几十毫秒内。结果某个大促活动之后磁盘 IO 突然掉了慢查询一个接一个最夸张的一条SELECT count(*)跑了快三分钟。我上服务器一查表本身才 60GB膨胀的空壳空间占了差不多 45GB再加上索引膨胀整个表实际占盘超过 130GB。我第一反应是VACUUM FULL但这张表是 7x24 小时在写的核心表VACUUM FULL会拿 AccessExclusiveLock直接把业务堵死肯定不行。后来就是靠 pg_repack 在线重建表彻底释放了空间整个过程业务几乎无感。这篇博客我就把 pg_repack 从原理到实战、从安装到避坑的完整经验都写出来想给同样被表膨胀折磨的 DBA 和运维同学一个可以直接照着操作的参考。如果你手头有 PostgreSQL 数据库如果你被表越来越胖、查询越来越慢、磁盘一直告警这类问题困扰或者你正准备把大表重新整理一遍但不敢离线操作那这篇文章就是写给你的。1. 为什么说表膨胀是PostgreSQL最磨人的问题1.1 膨胀的根子MVCC 机制PostgreSQL 用多版本并发控制MVCC来实现高并发读写。每次UPDATE不会原地修改数据而是插入一个新版本的行旧版本的行继续留在页面里等待事务结束后被清理。DELETE也不是物理删除只是打上删除标记。这些旧版本行就是膨胀的来源。等你高频更新完一批数据表里可能已经堆积了大量死掉的旧版本。它们对业务查询是透明的但文件实实在在占着磁盘。日常的VACUUM能清理死元组、把页面重新标记为可复用但有一个大前提空间只是内部腾出来给了后续写入复用文件大小本身不会缩小。只有在表尾部的空页面被释放时文件才会变小大多数情况下堆表文件一直是涨上去就很难缩回来。我经常用一句话跟同事解释VACUUM 是把书里废弃的章节标记成空白页书的总页数不变pg_repack 是重新抄写一遍把空白页彻底丢掉。核心表的更新频率高膨胀速度远大于 autovacuum 的清理速度到了高峰期就扛不住了。1.2 膨胀带来的连锁反应膨胀不只是多占一块磁盘这么简单它会在三层同时暴雷磁盘空间最直接一个 100GB 的业务表膨胀 50%背后就是 50GB 额外开销存储成本翻倍。IO 和缓存全表扫描时膨胀表需要读入更多页面数据页不在 shared_buffers 里的概率变大磁盘 IO 和 WAL 写入量同步上升。索引效率索引也会膨胀索引页碎片化B-Tree 高度增加回表次数变多查询计划器对代价的估算直接失真本来该走索引的查询可能被优化成顺序扫描。更隐蔽的问题是主备切换场景。主库 WAL 很多情况下是由更新产生的膨胀表让同样的业务更新量产生了更多 WAL备库回放压力跟着上涨延迟一旦拉大高可用切换时丢数据的窗口就变得不可控。1.3 为什么不建议直接上 VACUUM FULLVACUUM FULL是 PostgreSQL 自带的整理工具但它重建表期间要持有 AccessExclusiveLock整个表对业务完全不可用。一张几百 GB 的大表VACUUM FULL一跑就是半小时甚至几小时业务侧等于直接停服。所以在线场景下pg_repack 几乎是唯一的选择。它通过影子表 数据重放的方式能在 DML 不中断的情况下完成重建只对目标表短暂加锁。这个短暂锁在绝大多数业务里是可以接受的。2. pg_repack 的原理与版本差异搞清楚机制才不会用错2.1 新版本机制日志表 触发器pg_repack 的核心思路是先建一张和原表结构一样的影子表把数据从原表灌进影子表同时建一个触发器记录这段时间内的增量修改INSERT/UPDATE/DELETE数据灌完后把增量回放到影子表最后在极短的时间内切换表名、重建索引、回收原表空间。新版本1.4.x 及之后的版本这里以常见发行版的 1.4.6、1.4.7 为例的关键流程如下在目标表上创建日志表记录 repack 开始后的所有增量变更。在目标表上创建触发器将 DML 操作写入日志表。创建一张与目标表结构一致的空影子表同时创建必要的约束和索引。执行全量数据回放把原表的数据批量插入到影子表。回放日志表里的增量数据让影子表跟上原表的最新状态。在很短的重放窗口内做一次ACCESS EXCLUSIVE锁切换把原表和影子表的名字互换重建索引和约束最后删掉旧表。这个流程有两点对大规模生产环境特别友好全量复制阶段走的是COPY加批量插入不阻塞读写触发器只记录增量数据量远小于全量所以整个窗口期很短。整体对业务影响小这也是它进入开源 DBA 工具箱之后迅速普及的原因。2.2 老版本机制差异使用 pg_repack 前一定要先区分版本机制。1.4 之前的老版本采用的是另一种策略在切换旧表与影子表时用ACCESS EXCLUSIVE锁把整个表锁住然后通过文件硬链接和索引交换来完成切换。这种方式不需要日志表和触发器但锁窗口相对更长而且对表空间、并发写入的容忍度差一些。新老版本的命令参数基本兼容但如果你在很老的机器上还在用旧版本建议至少升到 1.4.6 或更新的版本修复了不少边界问题和稳定性缺陷。我在生产环境里只认一个原则能用新版本就不要用老版本。2.3 新旧机制对比速查对比项1.4日志表触发器1.4 之前锁切换记录增量方式触发器写入日志表无触发器依赖短锁全量复制期间写阻塞否否切换窗口期锁短相对较长对高并发大表的友好度高中建议优先使用尽快升级有了这个认知往后看各种为什么锁了这么久为什么报触发器错误的问题基本都能顺藤摸瓜。3. 安装姿势与版本匹配最容易被忽略的第一步很多人在 pg_repack 上栽的第一个跟头不是命令用错而是安装阶段就埋了雷。因为 pg_repack 包含一个数据库扩展.so文件和一台命令行客户端两者版本必须和 PostgreSQL 服务端兼容。服务端是 PostgreSQL 14你装了个为 PostgreSQL 12 编译的扩展大概率直接报错。3.1 如何在 Linux 上安装最常见的发行版安装方式RHEL/CentOS/Rocky PGDG 仓库yum install pg_repack # 如果你装了多个 PG 大版本请使用带版本号的包例如 # yum install pg_repack_14Ubuntu/Debian PGDG 仓库apt install postgresql-14-repack # 版本号根据你实际的 PG 大版本调整源码编译安装适合没有现成 RPM/DEB 的环境git clone gitgithub.com:reorg/pg_repack.git cd pg_repack make make install源码编译前请先确保pg_config指向的目标是和你要操作的 PostgreSQL 实例同版本的二进制否则扩展.so加载时会报错。可以用pg_config --version验证。3.2 装完必须做的两个检查第一确认扩展文件路径正确并且能正常被 PostgreSQL 读取。可以先在库里执行CREATE EXTENSION pg_repack;如果返回成功说明扩展没问题。注意CREATE EXTENSION需要超级用户权限而且这一步只能在你要操作的数据库里做一次不是每个库都自动可用。第二确认客户端版本pg_repack --version我遇到过一次挺奇怪的现象扩展现有版本正常但运行pg_repack客户端时报extension is not available。最后查下来是pg_repack二进制链接到了另一个版本的 libpq认错了服务端版本的元数据。所以生产环境里客户端、扩展、服务端三个组件一定要保持同一系列版本别混装。3.3 授权准备pg_repack 使用超级用户权限是最省心的因为它要创建触发器、创建日志表、交换表名等。生产环境如果出于安全考虑不给普通业务账号超级权限也需要授予SUPERUSER或者至少授予pg_repack必要的权限。注意频繁操作的表最好由专属账号统一维护脚本里不要硬编码密码用.pgpass或密钥认证更稳妥。4. 日常运维怎么用全库、单表、索引重建的实战命令4.1 先评估哪些表值得 repack不建议上来就全库一把梭。我的习惯是先用一条 SQL 查看表大小、膨胀比例、活跃元组数量排序后挑投入产出比最高的表。SELECT schemaname, relname, pg_size_pretty(pg_total_relation_size(c.oid)) AS total_size, n_live_tup, n_dead_tup, CASE WHEN n_live_tup 0 THEN round(n_dead_tup::numeric / n_live_tup * 100, 2) ELSE 0 END AS dead_pct FROM pg_stat_user_tables t JOIN pg_class c ON c.oid t.relid WHERE schemaname NOT IN (pg_catalog, information_schema) ORDER BY pg_total_relation_size(c.oid) DESC LIMIT 20;看dead_pct超过 40%、表总大小超过 10GB 的基本都值得跑一轮。空间占比大但活跃行很少的表优先级最高因为回收效果立竿见影。4.2 单表 repack 命令与参数解析最常见的操作是单表重建pg_repack --host 127.0.0.1 --port 5432 --dbname mydb \ --table public.orders --no-order --no-superuser-check几个参数的实际含义我说明一下--table指定需要重建的表只需要表名不需要写账号。--no-order表示不按主键或唯一索引排序复制数据通常为了速度可以加上前提是你不需要在复制阶段顺便重排数据。--no-superuser-check是当你的账号不是超级用户但在白名单里时跳过权限检查用的。生产环境如果给了SUPERUSER这个参数可以不加。-k或--no-kill-backend表示不踢掉那些会阻塞 repack 的后端连接。不加时 pg_repack 会尝试终止阻塞连接但生产环境不建议默认加-k容易误杀长事务最好由你手动判断。如果要连索引一起重建可以加上--index参数指定索引或者干脆在 repack 表之后用独立的命令重建索引效果都一样。区别在于单独重建索引的锁窗口比整个表重建短很多对于超大表建议步骤拆开。4.3 先建索引的细节pg_repack 在影子表上创建索引时不会自动沿用原表所有索引的属性和并发策略。默认它会把原表上的索引全部在影子表上重建但此时是在线操作不阻塞写。如果有大索引推荐在命令执行之前先用CREATE INDEX CONCURRENTLY把索引建好然后通过参数让 pg_repack 跳过某些索引的重复创建。比如pg_repack --dbname mydb --table public.orders \ --index public.idx_orders_created_at \ --index public.idx_orders_status这样 repack 时就只重建你指定的索引其余已经通过CONCURRENTLY建好的索引保留不变跑批时间能缩短不少。4.4 全库 repack 的注意事项全库 repack 可以用--all-databases但我强烈建议在生产环境谨慎使用。一张 500GB 的大表和几十张小表混在一起全库跑完等于一次小型的线上整理风暴磁盘空间需要额外预留约等于最大被重建表的容量。所有表的触发器、日志表在同一时段创建IO 压力叠加。一个表失败会连带影响后续表。我更推荐的顺序是先处理 TOP 10 的大表执行完一批、观察一两天确认没有性能回退再处理下一批。按表大小从大到小操作收益最直接。5. 分区表的 repack老版本无能为力的三个替代方案分区表是 pg_repack 又一个容易踩坑的重灾区。PostgreSQL 12 之前的原生分区表老问题很多用 pg_repack 处理子表会遇到各种报错最常见的包括cannot create a trigger on a partition和cannot repack a partitioned table directly。5.1 对子表逐个 repack如果分区键是范围分区或者列表分区最直接的办法是对每个子表单独 repackpg_repack --dbname mydb --table public.orders_2024_01 pg_repack --dbname mydb --table public.orders_2024_02这样做有几个小细节子表名字直接从分区父表查看不要猜。每个子表必须有主键或唯一索引否则 repack 无法确定增量回放的排序键。父表上如果有全局约束或索引repack 子表之后父表层的元数据可能滞后建议 repack 完后用REFRESH相关元数据或直接重建父表索引。如果你用的是 PostgreSQL 12分区表已经在底层支持了很多之前不支持的 DDL但 pg_repack 对某些分区策略依然不能直接处理父表。这时只处理子表不影响整体分区裁剪逻辑。5.2 升级 PG 版本如果分区表非常多逐个子表 repack 太累或者单子表数据量太大导致窗口依然不可接受那我建议认真评估升级到 PostgreSQL 14 或更高版本。新版 PG 对分区表有大量优化配合新版 pg_repack 可以更平滑处理分区场景。当然升级本身是一个完整项目但长远看比在旧版本上反复打补丁式运维要省心得多。5.3 调整分区键这个方案比较激进适合在建表设计阶段就介入的场景。如果你发现某个分区表的写入总是集中在某个新分区上导致该子表膨胀特别快可以考虑更细粒度的时间分区比如从按月改成按周让单个子表的体积和更新频率降下来。这样每个子表 repack 的时间窗口也会相应缩短。6. 踩过的坑与排查全链路从权限报错到超时中断这一章我直接把我自己还有身边同事在生产环境遇到的典型问题按症状-排查-根治的方式写全。6.1 ERROR: must be owner of relation xxx这个报错几乎人人都遇过。表面原因是账号不是表 owner但深层原因往往是运行 pg_repack 的账号没有所以我们常规的权限模型里表 owner 和超级用户是两个概念。排查步骤查看当前连接账号和表的 ownerSELECT current_user; SELECT relname, relowner::regrole FROM pg_class WHERE relname 目标表;如果账号确实是超级用户依然报这个错那就要检查你连接到的数据库是不是目标表所在的数据库。跨库连接时另一库里的表对此库账号来说权限并不继承。根治方案要么把表 owner 改为 repack 专用账号要么直接用超级用户执行。生产环境不建议随意 ALTER TABLE OWNER因为会影响权限审计。更推荐维护一个专用的维护账号授予SUPERUSER或按需授权并在作业调度里固定用这个账号去执行。我自己的经验是权限明确、账号隔离反而比到处用超级用户好排查。6.2 ERROR: permission denied to create trigger创建触发器需要表 owner 或特定权限普通只读账号必然失败。排查时看两处目标表是否在扩展创建的那个数据库里。账号有没有CREATE权限、触发器权限。根治方案也不复杂在运行 pg_repack 之前把维护账号加入该表的权限链给足SELECT、INSERT、UPDATE、DELETE、TRUNCATE、REFERENCES、TRIGGER权限或者直接使用超级用户。注意触发器权限是隐式由表 owner 持有的如果你用非 owner 账号即使别的权限都给了创建触发器可能还是会失败。所以最稳的还是超级用户或者把表 owner 改为维护账号。6.3 卡住不动锁等待和长时间运行的 SQLpg_repack 在开始前会检查目标表上的活动事务。如果有一个长事务一直不提交它可能会等待表现为进程长时间没输出。这时别急着 kill先看SELECT pid, state, wait_event_type, wait_event, query FROM pg_stat_activity WHERE query ILIKE %repack%;如果发现 pg_repack 在等待某个AccessExclusiveLock大概率是表上有一个长时间运行的查询或者一个坏事务没结束。你可以选择等待业务低峰期再执行。终止那个阻塞事务前提是确认它确实可以安全终止。在第 4.2 节提到的--no-kill-backend也是为这种场景准备的。不加--no-kill-backend时pg_repack 会尝试主动终止阻塞它的后端连接加上之后它会老实等待。我个人的建议是在业务低峰期不加这个参数让 pg_repack 自动处理在混跑期加上参数手工控制更稳妥。6.4 repack 完成后磁盘空间没释放这是被问得最多的一个问题。明明 repack 成功了df -h看磁盘一点没少。原因基本是表的旧文件被删除后空间被其他进程或操作系统延迟分配或者表所在表空间还有其他大对象又或者 autovacuum 还没来得及回收。正确验证方式不是看df而是看SELECT pg_size_pretty(pg_total_relation_size(目标表));repack 前后对比这个值通常能看到明显下降。如果df -h一直没变化考虑检查表空间路径下是否还有 WAL 归档、备份残留。一个真实案例某系统当时df没变化排查了半天发现磁盘上有一份旧的 pg_basebackup 备份删掉之后空间立刻回来了。所以 repack 本身没问题是运维直觉误导了判断。6.5 建索引失败导致的整体回滚如果原表上有失效索引或者坏索引pg_repack 会在影子表上尝试复制结构时失败。常见报错是index contains corrupted page或者duplicate key value violates unique constraint。这说明源表本身有数据异常repack 只是把问题暴露出来。处理办法先用REINDEX INDEX CONCURRENTLY修复源表索引。如果是数据重复导致的唯一键冲突先修复数据再跑 pg_repack。我在一次业务大表上遇到过因为应用 bug 写入了重复订单号唯一索引任务构建失败。当时是先临时去掉唯一约束清理重复数据再加回约束最后才跑通 repack。整个过程要谨慎别在数据没修好之前强行 repack否则会一直卡在同样的位置。6.6 主备延迟被拉高repack 会产生大量 WAL备库回放压力会升高。务必要在业务低峰期操作并且操作期间密切观察备库延迟。如果延迟超过阈值优先暂停下一个表的 repack而不是硬顶着继续跑。对超大表可以考虑临时调大max_wal_senders、max_standby_streaming_delay等参数但不要为了追求速度把同步复制模式的备库搞出数据裂缝。6.7 预检清单我把自己的生产环境预检清单贴出来每次操作前逐项确认目标表是否绑定了 CDC 工具阅读 Log 的逻辑复制如果有repack 期间要确认不会冲突。磁盘剩余空间是否大于目标表大小。目标表是否有主键/唯一索引。是否在业务低峰期长事务是否已经结束。账号权限是否满足超级用户最省心。是否已经有备份或备库可以随时接管。是否知道操作失败后如何快速取消并回滚pg_repack 的 CtrlC 与--exit-on-error行为要提前测试。7. 同类工具怎么选VACUUM FULL、pg_squeeze、pg_repack 的对比很多人会问既然有 pg_repack那自带的 VACUUM FULL 还有存在意义吗有但适用场景完全不同。维度VACUUM FULLpg_repackpg_squeeze是否需要长锁是全程 AccessExclusiveLock否切换窗口短锁否设计目标是在线收缩在线 DML 容忍度不支持支持支持观感慢、堵、直观快、在线、复杂一点相对冷门、生态不如 repack适用场景小表、可以停服的离线维护窗口大表、7x24 在线系统对 repack 有顾虑时的一个备选成熟度内置、最稳高、社区活跃中低生产案例相对少从我自己的实践看小表低于几百 MB用 VACUUM FULL 完全没问题锁那么一小会儿无所谓命令简单、出问题概率低。大表优先 pg_repack因为生态成熟、命令直观、遇到问题能搜到答案。pg_squeeze 我也在测试环境验证过功能是有的但要大规模上生产我目前还不太放心毕竟 repack 被验证的场景多得多。如果你管理的是一套几十个库的 PostgreSQL 环境建议把 repack 作为一种常规表维护套餐纳入月度巡检。我一般每个月挑一个周末低峰期把当月膨胀率最高的 top 10 表逐台跑一遍配合监控系统观察磁盘趋势。半年下来因为膨胀导致的故障基本没有再出现过。8. 关于磁盘空间和安全性的最后一个提醒跑 pg_repack 之前务必确认你的 PostgreSQL 数据目录所在文件系统有足够的剩余空间。repack 需要大约一个目标表大小的临时空间如果你在磁盘即将写满的情况下强行操作可能直接导致数据库 crash。我见过真实的教训有同事在磁盘剩余 10GB 的情况下对一张 20GB 的表跑 repack跑到一半磁盘全满WAL 写不进去最后只能临时删掉 repack 的临时文件才缓过来。另一个提醒是计划任务化之后的容错。如果要把 pg_repack 自动化建议每次执行前自动跑一次剩余空间检验脚本空间不足时直接退出并告警而不是硬着头皮重试。这样能避免自动任务反而把库搞挂的尴尬局面。也不要忘了在每次 repack 前记录表大小和运行时间基线这样你能看到优化效果也能在下次膨胀加速时更早感知趋势。维护脚本化、结果可追踪才是线上稳定运行的长久之计。