pg_squeeze: 比 VACUUM 更好地收缩表

六月 20, 2024

摘要pg_squeeze是 PostgreSQL 的一个扩展,可自动修复表膨胀问题,而无需对表进行大量锁定。

目录

pg_squeeze 简介

它是一个 PostgreSQL 扩展,用于删除表中未使用的空间,并可选择根据特定索引对元组进行排序(就像在常规读/写运行的同时,执行 CLUSTER 命令一样)。事实上,pg_squeeze 试图用来替换 pg_repack 扩展。

虽然提供的功能非常相似,但pg_squeeze采用了不同的方法:

  1. 完全在服务端实现功能。
  2. 利用 PostgreSQL 数据库服务器最新提供的能力。

与同时使用了服务端和客户端代码的 pg_repack 相比,(1) 不仅简化了配置和使用,还可以使用后台工作进程顺利实现自动化处理。

至于 (2),除了使用后台工作进程,一个重要的区别是,我们使用逻辑解码而不是触发器来捕获并发的修改。

登记表进行常规处理

首先,确保您的表具有主键或唯一约束。这是在pg_squeeze工作时处理其他事务可能进行的更改所必需的。

要让pg_squeeze扩展知道该表,需要在squeeze.tables表中插入一条记录。在添加后,它会定期检查表上的统计信息。只要表满足"收缩"标准,就会向队列中添加一个"任务"。任务会按照创建的顺序依次处理。

最简单的"登记"是这样的:

INSERT INTO squeeze.tables (tabschema, tabname, schedule)
VALUES ('public', 'foo', ('{30}', '{22}', NULL, NULL, '{3, 5}'));

还可以选择指定其他的列,例如:

INSERT INTO squeeze.tables (
    tabschema,
    tabname,
    schedule,
    free_space_extra,
    vacuum_max_age,
    max_retry
)
VALUES (
    'public',
    'bar',
    ('{30}', '{22}', NULL, NULL, '{3, 5}'),
    30,
    '2 hours',
    2
);

以下是表的元数据的完整描述。

  • tabschematabname分别是模式名和表名。

  • schedule列会指定什么时候应该对表进行检查,并可能进行收缩。schedule 由下面的复合数据类型的值来描述,类似于一个 crontab 条目:

    CREATE TYPE schedule AS (
        minutes       minute[],
        hours         hour[],
        days_of_month dom[],
        months        month[],
        days_of_week  dow[]
    );
    

    在这里,minutes(0-59)和hours(0-23)指定了一天内的检查时间,而days_of_month(1-31)、months(1-12)和days_of_week(0-7,其中 0 和 7 都代表星期日)则决定了检查的日期。

    如果minutehourmonth都与当前时间戳相匹配,则会执行检查,而 NULL 值分别表示任意分钟、小时和月份。至于days_of_monthdays_of_week,需要至少有一个与当前时间戳匹配,或者两个都为 NULL,才能进行检查。

    例如,上面的条目告诉我们,应在每周三和周五的 22:30 检查表public.bar

  • free_space_extra是触发表处理所需的extra free space的最小百分比。extra形容词指的是,从fillfactor得到的空闲空间,并不会触发表的收缩。

    例如,如果fillfactor等于 60,那么在正常运行期间,每个页面至少应有 40% 的空间是可用的。如果要让 pg_squeeze 处理到表的可用空间能达到 70%,可将free_space_extra设置为 30(即 70% 的可用空间减去因fillfactor而产生的 40% 可用空间)。

    free_space_extra的默认值为 50。

  • min_size是表必须占用的最小磁盘空间(以 MB 为单位)。默认值为 8。

  • vacuum_max_age是自上一次 VACUUM 完成后,认为空闲空间映射表(FSM)已清理的最长时间。一旦过了这个时间间隔,死元组的比例可能会很大,因此需要花费比简单检查 FSM 更多的精力,来评估pg_squeeze的潜在影响 。默认值为 1 小时。

  • max_retry是在相应任务第一次处理失败后,额外尝试收缩表的最大次数。重试处理的典型原因是在收缩表时,表定义发生了更改。如果达到重试次数,则认为表的处理已经完成。一旦到达下一个预定时间,就会立即创建下一个任务。

    max_retry的默认值为 0(即不重试)。

  • clustering_index是处理过的表的现有索引。处理完成后,表中的元组将根据该索引的键进行物理排序。

  • rel_tablespace是表应移动到的现有表空间。NULL 表示表应留在原处。

  • ind_tablespaces是一个二维数组,其中每一行都指定了索引的表空间映射。第一列和第二列分别代表索引名称和表空间名称。所有未指定映射的索引都将保留在原来的表空间中。

    关于表空间,有一种特殊情况值得一提:如果为表指定了表空间,但没有为索引指定表空间,则表会被移动到该表空间,但索引会保留在原来的表空间(也就是说,表的表空间不是索引的默认表空间)。

  • skip_analyze表示在表处理之后不应执行 ANALYZE 命令。默认值为false,表示默认执行 ANALYZE。

squeeze.table 是用户唯一应该修改的表。如果您想修改其他表,请确保您完全明白自己在做什么。

对任何表进行临时处理

也可以在不登记的情况下手动收缩表(即无需在squeeze.tables中插入相应的记录),并且无需事先检查实际膨胀的情况。

函数签名:

squeeze.squeeze_table(
    tabchema name,
    tabname name,
    clustering_index name,
    rel_tablespace name,
    ind_tablespaces name[]
)

执行样例:

SELECT squeeze.squeeze_table('public', 'pgbench_accounts');

启用/禁用表的处理

要处理膨胀的表,请以超级用户运行此语句:

SELECT squeeze.start_worker();

该函数会启动一个后台工作进程(scheduler worker),定期检查有哪些已登记的表需要进行膨胀检查,并为每个表创建一个任务。只要特定数据库存在任务,就会启动另一个工作进程(squeeze worker)。

如果当前数据库已在运行scheduler worker,函数不会报错,但新的工作进程会立即退出。

如果有工作进程正在当前数据库中运行,可以使用下面语句停止它们:

SELECT squeeze.stop_worker();

只有本文档中提到的函数才应视为用户接口。如果您想调用其他函数,请确保您完全明白自己在做什么。

如果希望后台工作进程在整个 PostgreSQL 实例启动时自动启动,请在postgresql.conf文件中添加如下条目:

squeeze.worker_autostart = 'my_database your_database'
squeeze.worker_role = postgres

下次启动实例时,将为my_database启动两个或多个工作进程(即一个scheduler worker,和一个或多个squeeze workers),并为your_database启动同样的工作进程。请注意,如果您采用这种方法并满足下面任一条件,任何工作进程都会拒绝启动,或者停止而不做任何工作:

  1. pg_squeeze扩展不存在于数据库中,或

  2. squeeze.worker_role参数指定了不具有超级用户权限的角色。

虽然实际上有两个工作进程,但上述函数/配置变量使用了单数形式的worker。这是因为在以前的 pg_squeeze 版本中只有一个工作进程,它同时负责任务的调度和执行。在升级过程中,强制所有用户调整配置文件可能并不值得。

控制对其他后端进程的影响

虽然被收缩的表在大部分时间里都可以被其他事务读写,但它在完成处理时需要获取排他锁。如果 pg_squeeze 偶尔看上去阻塞了对表的访问,可以考虑设置 GUC 参数squeeze.max_xlock_time。例如:

SET squeeze.max_xlock_time TO 100;

设置排他锁的保持时间不应超过 0.1 秒(100 毫秒)。如果最后阶段需要更多时间,pg_squeeze 会释放排他锁,处理中间其他事务提交的更改,并再次尝试最后阶段。如果再次超过锁的持续时间,就会报错。如果出现这种情况,要么增加设置,要么将处理问题表的时间安排在写入活动较少的另一天。

每个数据库运行多个工作进程

如果认为单个收缩工作进程无法应付负载,可考虑将squeeze.workers_per_database配置变量的值设置为大于 1。这样,pg_squeeze扩展就能同时处理多个表,每个收缩工作进程处理一个表。不过,请注意这一设置会影响所有使用pg_squeeze扩展的数据库。实例中所有收缩工作进程(包括"调度工作进程")的总数,不能超过内核配置变量max_worker_processes

监控

  • squeeze.log表中,每个成功收缩的表都会包含一个条目。

    tabschematabname这两列标识处理的表。startedfinished这两列标识处理的开始和完成时间。ins_initial是在 “初始加载阶段 “插入到新表存储中的元组数,即处理开始前表中存在的元组数。另一方面,insupddel是在表处理过程中由应用程序插入、更新和删除的元组数。(这些 “并发的数据更改” 也必须合入收缩的表,否则就会丢失数据)。

  • squeeze.errors表中包含了收缩过程中发生的错误。此处报告的一个常见问题是,有人更改了正在进行处理的表的定义(如添加或删除列)。

  • squeeze.get_active_workers()函数返回一个收缩工作进程的表,这些收缩工作进程正在当前数据库中处理表。

    pid列包含了工作进程的系统 PID。其他列的含义与squeeze.log表中的对应列相同。squeeze.log表只显示已完成的收缩操作信息,而squeeze.get_active_workers()函数则可让您检查处理过程中的进度。

取消登记表

如果不再需要对特定表进行定期收缩,只需从squeeze.tables表中删除相应行即可。

尽管后台工作进程会定期取消登记不存在的表,但取消登记要删除的表也是一种好的做法。

并发性

  1. 该扩展在处理的某些阶段并不阻止其他事务更改表。如果一个 “破坏性命令”(即ALTER TABLE,VACUUM FULL,CLUSTERTRUNCATE)在收缩完成前提交,squeeze_table()函数就会中止,对表的所有更改都会回滚。squeeze.tables表的max_retry列决定了收缩工作进程重试的次数。此外,更改调度时间也可以避免中断。

  2. pg_repack 一样,pg_squeeze也会改变行的可见性,从而允许出现 MVCC 提醒 第一段中描述的 MVCC 不安全行为。

磁盘空间要求

执行全表收缩所需的可用磁盘空间,大约是目标表及其索引的两倍。例如,如果要收缩的表和索引的总大小为 1GB,则需要额外的 2GB 磁盘空间。