暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

PolarDB-PG调优-pg_repack 插件原理解读(一)

ZzzMickey 2024-12-26
241

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/

  1. pg_repack/lib/ 目录下的文件最终会编译生成 pg_repack.so,在数据库服务端通过 CREATE EXTENSION 创建插件的方式操作来进行加载。
  2. pg_repack/bin/ 目录下的文件最终会编译生成 pg_repack 客户端工具。

repack 普通表

首先介绍 repack 普通表,这是最常见、最重要的 pg_repack 操作。该操作会重写表,并重建表上的索引,作用类似于 VACUUM FULL 或 CLUSTER,适用于表空间膨胀的场景。大致用法如下:

  1. 使用 --table/-t 参数指定表名。
  2. 如果表上有多个索引,则可以使用 --jobs/-j 参数设置重建索引的并发度,这样重建速度更快。
  3. 默认为 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 是关键函数,它的主要流程如下:

  1. 初始化
    • 对表加意向锁,防止该表上有多个 pg_repack 任务并发执行;
    • 对表加排它锁,阻塞读写;
    • 创建日志表,用于保存 repack 过程中的增量数据;
    • 在原表上创建触发器,用于将原表上的增量数据插入日志表;
    • 从排它锁降级到共享锁,不再阻塞读写,后续的 repack 过程多数时间内都持有共享锁,在允许业务读写请求访问表的同时,又可以防止 DDL 操作修改表结构。
  2. 全量数据同步
    • 创建一个空的新表:CREATE TABLE new_table AS SELECT * FROM old_table WITH NO DATA
    • 将原表数据全量同步到新表:INSERT INTO new_table SELECT * FROM old_table
  3. 索引重建:调用 rebuild_indexes 函数在新表上创建索引,可以开启多个并发,并发数量取决于 --jobs 参数。
  4. 增量数据同步:反复调用 apply_log 函数将日志表中的增量数据应用到新表,直到日志表中没有数据为止。如果原表一直在产生增量数据,则同步过程可能要持续很久。
  5. 元数据交换
    • 从共享锁升级为排它锁,阻塞读写,不允许继续产生增量数据;
    • 由于加排它锁之前的短暂空当可能有并发 DML 产生增量数据,所以再次调用 apply_log 函数同步增量数据;
    • 调用 repack_swap 函数交换新表和旧表的元数据,主要是把 pg_catalog.pg_class 系统表中保存的 relfilenode、reltablespace、reltoastrelid 等元数据对调,让原表的元数据指向新表的存储,它更为紧凑,而新表的元数据则指向原表之前的那份空间膨胀率较高的存储;
    • 释放排它锁,此时 repack 已经基本完成,不再需要锁来进行保护。
  6. 删除旧表
    • 对表加排它锁;
    • 调用 repack_drop 函数删除旧表以释放膨胀的存储空间,此外还需要删除日志表、触发器等;
    • 释放排它锁。
文章转载自ZzzMickey,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论