PolarDB PostgreSQL 版是一款阿里云自主研发的云原生关系型数据库产品,100% 兼容 PostgreSQL,高度兼容Oracle语法;采用基于 Shared-Storage 的存储计算分离架构,具有极致弹性、毫秒级延迟、HTAP 、Ganos全空间数据处理能力和高可靠、高可用、弹性扩展等企业级数据库特性。同时,PolarDB PostgreSQL 版具有大规模并行计算能力,可以应对 OLTP 与 OLAP 混合负载。
背景
PolarDB PostgreSQL 版支持pg_repack插件,使用方法与原生pg_repack插件基本相同。pg_repack 字面意思是“重新包装”,它可以回收碎片化的存储空间,解决表和索引的存储空间膨胀问题。它的作用类似于 PG 数据库内核自带的VACUUM FULL和CLUSTER功能,但是和执行期间完全阻塞业务读写请求,而pg_repack对读写请求的阻塞时间比和更短,对业务影响更小。
本文将结合pg_repack社区源代码来介绍pg_repack的大致原理。
代码结构
pg_repack 代码主要分为两个目录,一个客户端代码目录, 一个服务端代码目录:
- pg_repack/bin/ 目录下的文件最终会编译生成 pg_repack 客户端工具。
- pg_repack/lib/ 目录下的文件最终会编译生成 pg_repack.so 和 pg_repack.control,在服务端通过创建插件的方式操作来进行加载。
repack索引
仅对索引进行 repack,而不操作表本身的数据,可以理解为索引重建,适用于索引空间膨胀的场景。使用方法如下:
- 对具体的索引执行 repack 操作,用参数指定索引名,支持一个操作多个索引。(这里给出的pg_repack 命令经过了简化,去掉了用户名、数据库、密码等参数,下同)
pg_repack --index=<index_name1> --index=<index_name2> --index=<index_name3>
pg_repack -i <index_name1> -i <index_name2> -i <index_name3>
- 对表上的所有索引执行 repack 操作,用参数指定表名,再用参数说明只操作索引。
pg_repack --table=<table_name> --only-indexes
pg_repack --table=<table_name> -x
repack 索引相关的函数调用:,其中 函数执行单个表上的操作,关键逻辑就在该函数中,如果有多个表需要操作,会多次调用它。函数大概分为三步:
- 使用CREATE INDEX CONCURRENTLY并发创建一个新的临时索引,不阻塞读写,新索引名为index_<oid>,其中 oid 为它对应的原索引的 oid。
- 调用 pg_repack 插件实现的函数交换新旧索引的元数据,操作过程需要对表持有排它锁,短暂阻塞读写。其中函数的原理是把新旧索引在 系统表中保存的 relfilenode、reltablespace、reltoastrelid 等元数据对调,这样原索引的元数据就指向了新索引的存储,它更为紧凑,而新索引的元数据则指向了原索引之前的那份存储,它往往空间膨胀率比较高。
- 使用DROP INDEX CONCURRENTLY并发删除新索引,丢弃那份空间膨胀率较高的存储,不阻塞读写。
以上三步操作的作用实际上等同于执行一次REINDEX CONCURRENTLY,因此:
- 对于 PG11 及更早的版本(比如 PolarDB-PG11),由于数据库内核不支持,则可以借助 pg_repack 来实现在线索引重建;
- 对于 PG12 及之后的版本(比如 PolarDB-PG14)没有必要使用 pg_repack 插件来重建索引,直接使用 PG 内核自带的来重建索引即可。
repack普通表
repack 普通表,重建表,并重建它上面的索引,作用类似于或,适用于表空间膨胀的场景。使用方法如下:
- 使用参数指定表名,即可对一个表进行 repack。
pg_repack --table=<table_name>
pg_repack -t <table_name>
- repack 表的过程中需要重建表上的索引,如果表上有多个索引,则可以使用参数设置重建索引的并发度,这样重建速度更快。
pg_repack --table=<table_name> --jobs=<number>
- pg_repack 默认对该表上之前执行过的列进行排序,还可以使用选项对指定的列排序。
pg_repack --table=<table_name> --order-by=<column_name>
pg_repack --table=<table_name> -o <column_name>
- pg_repack 默认为模式,可以使用选项来执行模式。
pg_repack --table=<table_name> --no-order
pg_repack --table=<table_name> -n
repack 普通表相关的函数调用:,其中 是关键函数,它的主要流程如下:
- 初始化
- 对表加意向锁,防止该表上有多个 pg_repack 任务并发执行;
- 对表加排它锁,阻塞读写;
- 创建日志表,用于保存 repack 过程中的增量数据;
- 在原表上创建触发器,用于将原表上的增量数据插入日志表;
- 从排它锁降级到共享锁,不再阻塞读写,后续的 repack 过程多数时间内都持有共享锁,在允许业务读写请求访问表的同时,又可以防止 DDL 操作修改表结构。
- 全量数据同步
- 创建一个空的新表:;
- 将原表数据全量同步到新表:;
- 索引重建:调用函数在新表上创建索引,可以开启多个并发,并发数量取决于参数。
- 增量数据同步:反复调用函数将日志表中的增量数据应用到新表,直到日志表中没有数据为止。如果原表一直在产生增量数据,则同步过程可能要持续很久。
- 元数据交换
- 从共享锁升级为排它锁,阻塞读写,不允许继续产生增量数据;
- 由于加排它锁之前的短暂空当可能有并发 DML 产生增量数据,所以再次调用函数同步增量数据;
- 调用函数交换新表和旧表的元数据,主要是把系统表中保存的 relfilenode、reltablespace、reltoastrelid 等元数据对调,让原表的元数据指向新表的存储,它更为紧凑,而新表的元数据则指向原表之前的那份空间膨胀率较高的存储;
- 释放排它锁,此时 repack 已经基本完成,不再需要锁来进行保护。
- 删除旧表
- 对表加排它锁;
- 调用函数删除旧表以释放膨胀的存储空间,此外还需要删除日志表、触发器等;
- 释放排它锁。
repack分区表
使用方法如下,通过参数指定分区表的父表,pg_repack 会对该父表以及它的所有继承表都执行 repack 操作。其他所有的参数都与普通表相同,比如支持使用重建分区表上的索引,使用设置重建索引的并发度等。
pg_repack --parent-table=<partitioned_table_name>
pg_repack -I <partitioned_table_name>
repack 分区表相关的函数调用为:。其中函数会获取父表的所有继承表/分区,然后对所有的表依次调用。这样做的原因是每个继承表/分区都有自己独立的存储,因此可以独立执行repack。假如其中某个继承表/分区的 repack 发生错误,通常可以忽略该分区,继续执行下一个分区。
获取所有继承表的原理是调用函数,其作用等价于从系统表查出所有的继承表。
至于操作单个表的原理,在前面 repack 普通表的部分已经介绍过了。
repack多个对象
除了前面介绍的对单个表和索引进行操作之外,pg_repack 还支持操作整个数据库或者模式。这些操作都比较激进,一次操作大量的表和索引,对业务的影响相对较大,因此不建议对整个模式和数据库执行 pg_repack,而更推荐对单个表或索引执行 pg_repack。
为了降低 pg_repack 带来的风险,PolarDB PostgreSQL 版不支持模式级别和数据库级别的操作,执行 pg_repack 时必须通过指定具体的表或索引。考虑到实际情况下整个模式或整个数据库中所有表和索引的空间膨胀率都很高的情况很少,更多的是某个具体的表或索引发生空间膨胀,因此禁用模式级别和数据库级别的操作基本不影响 pg_repack 的使用。
不过考虑到模式级别和数据库级别的 repack 是社区 pg_repack 的一项能力,还是继续解读一下它的原理。
repack整个模式
使用方法如下,通过参数执行模式,pg_repack 就会对该模式中的所有表和索引执行操作。
pg_repack --schema=<schema_name>
pg_repack --c <schema_name>
repack 整个模式相关的函数调用:,其中会找出该模式下所有的表,对每个表都执行。的原理已经在之前 repack 单表的部分介绍过了。
repack整个数据库
使用方法如下,通过参数指定数据库,但是不指定具体的表和索引名,pg_repack 就会对该数据库中的所有用户表和索引执行操作。
pg_repack --dbname=<db_name>
pg_repack -d <db_name>
repack 单个数据库相关的函数调用:,其中会查出该数据库下的所有表,并对每个表的调用函数去执行操作。至于的原理,在前面 repack 普通表的部分已经介绍过了。
repack所有数据库
使用方法如下,通过参数表示对该实例的所有数据库中的所有表和索引都执行 repack 操作。
pg_repack --all
pg_repack -a
repack 所有数据库相关的函数调用:,其中会获取实例中的所有数据库,并对每个数据库都调用去执行单个数据库的 repack 操作。的原理在上一步已经介绍过了。
repack修改表空间
使用方法如下,通过指定一个新的表空间,即可将 repack 之后的新表移动到一个新的表空间,如果使用参数,还可以将 repack 之后的新索引也移动到新的表空间。
pg_repack --tablespace=<new_tablespace> --moveidx
pg_repack -s <new_tablespace> --moveidx
它的实现原理很简单,就是在函数创建新表或函数创建新索引的过程中指定表空间,CREATE TABLE和CREATE INDEX天然支持该能力。
PolarDB PostgreSQL 版不支持该操作,因为 PolarDB PostgreSQL 版的数据都保存在默认的表空间,且不允许用户创建额外的表空间。




