pg_repack 是 PostgreSQL 数据库生态的一款第三方插件,本文将结合 pg_repack 的 源代码 来介绍其原理,而不会介绍如何使用它。如果想了解 pg_repack 的具体用法,可以参考 pg_repack 的 官方文档 或 PolarDB PostgreSQL 版的 pg_repack 文档。
简介
pg_repack 的字面意思是“重新包装”,可以 回收碎片化的存储空间,解决表和索引的存储空间膨胀问题。
PostgreSQL 内核自带的 VACUUM FULL 和 CLUSTER 功能同样可以重写表并解决存储空间膨胀问题,为何还要开发一个 pg_repack 插件来做同样的事?这是因为 VACUUM FULL 和 CLUSTER 需要锁表,可能导致业务长时间无法进行数据读写,而 pg_repack 对读写请求的阻塞时间很短,对业务影响更小,这就是它相比 VACUUM FULL 和 CLUSTER 最大的优势。
pg_repack 以安装在 PostgreSQL 数据库侧的 pg_repack 插件作为服务端,并提供 pg_repack 客户端给用户,两者需要搭配使用,用户执行一条形如 pg_repack --table=my_table 的 shell 命令,客户端会连接到服务端去执行表重写的操作。
为什么需要一个单独的客户端,而不能让用户直接连接到数据库去执行类似 SELECT repack_table('my_table') 之类的函数调用来完成表重写?这是因为 pg_repack 的操作涉及到全量数据同步、增量数据同步等多个阶段,其中还有锁级别的切换,包含多个事务,无法封装在一个函数中。为了让用户能够一键完成表重写,因此 pg_repack 选择将以上步骤封装到客户端中。
代码结构
pg_repack 代码主要分为两个目录,一个是服务端代码目录 pg_repack/lib/,一个是客户端代码目录 pg_repack/bin/:
pg_repack/lib/目录下的文件最终会编译生成 pg_repack.so,在数据库服务端通过CREATE EXTENSION创建插件的方式操作来进行加载。pg_repack/bin/目录下的文件最终会编译生成 pg_repack 客户端工具。
repack 普通表
首先介绍 repack 普通表,这是最常见、最重要的 pg_repack 操作。该操作会重写表,并重建表上的索引,作用类似于 VACUUM FULL 或 CLUSTER,适用于表空间膨胀的场景。大致用法如下:
- 使用
--table/-t参数指定表名。 - 如果表上有多个索引,则可以使用
--jobs/-j参数设置重建索引的并发度,这样重建速度更快。 - 默认为
CLUSTER模式,重写过程中对该表上之前执行过CLUSTER的列进行排序,还可以使用--order-by/-o选项对指定的列排序。可以使用--no-order/-n选项来执行VACUUM FULL模式。
VACUUM FULL 操作会对表加排它锁,阻塞一切读写操作,将表中数据读出并写到一份新的存储,新的存储中的数据排列很紧密,用于代替之前的碎片化的旧存储。VACUUM FULL 的逻辑之所以相对简单,是因为它阻塞了读写操作,不需要考虑并发读写场景。
然而 pg_repack 允许操作过程中有并发读写,因此需要考虑并发 DML 产生的增量数据,总体上分为 全量数据同步 + 增量数据同步 两个阶段。其中增量数据用触发器捕获,保存到单独的日志表中,最后将日志表的数据应用的新表。
相关的函数调用链为 main->repack_one_database->repack_one_table,其中 repack_one_table 是关键函数,它的主要流程如下:
- 初始化
- 对表加意向锁,防止该表上有多个 pg_repack 任务并发执行;
- 对表加排它锁,阻塞读写;
- 创建日志表,用于保存 repack 过程中的增量数据;
- 在原表上创建触发器,用于将原表上的增量数据插入日志表;
- 从排它锁降级到共享锁,不再阻塞读写,后续的 repack 过程多数时间内都持有共享锁,在允许业务读写请求访问表的同时,又可以防止 DDL 操作修改表结构。
- 全量数据同步
- 创建一个空的新表:
CREATE TABLE new_table AS SELECT * FROM old_table WITH NO DATA; - 将原表数据全量同步到新表:
INSERT INTO new_table SELECT * FROM old_table;
- 创建一个空的新表:
- 索引重建:调用
rebuild_indexes函数在新表上创建索引,可以开启多个并发,并发数量取决于--jobs参数。 - 增量数据同步:反复调用
apply_log函数将日志表中的增量数据应用到新表,直到日志表中没有数据为止。如果原表一直在产生增量数据,则同步过程可能要持续很久。 - 元数据交换
- 从共享锁升级为排它锁,阻塞读写,不允许继续产生增量数据;
- 由于加排它锁之前的短暂空当可能有并发 DML 产生增量数据,所以再次调用
apply_log函数同步增量数据; - 调用
repack_swap函数交换新表和旧表的元数据,主要是把pg_catalog.pg_class系统表中保存的 relfilenode、reltablespace、reltoastrelid 等元数据对调,让原表的元数据指向新表的存储,它更为紧凑,而新表的元数据则指向原表之前的那份空间膨胀率较高的存储; - 释放排它锁,此时 repack 已经基本完成,不再需要锁来进行保护。
- 删除旧表
- 对表加排它锁;
- 调用
repack_drop函数删除旧表以释放膨胀的存储空间,此外还需要删除日志表、触发器等; - 释放排它锁。




