0
点赞
收藏
分享

微信扫一扫

pg_repack你需要知道的坑

花明 2022-11-03 阅读 119

理解pg_repack:怎样会出错以及如何避免

pg_repack 是最古老和广泛使用的 PostgreSQL 扩展之一,它十分受欢迎,以至于即使是 DBaaS 服务提供商也需要它。它是 DBA 手中处理表膨胀/碎片化的"强力工具"。我无法想象生产部署中没有该扩展的情况。它神奇地用一个全新的、完全紧实的表替换了膨胀、碎片化的表,而且在其处理过程中不会对表持有排他锁。这个扩展使得 PostgreSQL 的内置命令如 VACUUM FULL 和 CLUSTER 几乎失去了作用。

不过短暂的排它锁还是需要的,请看下文的讨论。

但不幸的是,经常和重复使用 pg_repack 增加了每个人(包括我自己)的舒适度。因此,它被随机推荐为解决一切问题的灵丹妙药。但是我最近开始遇到一些情况,例如对每个表定时执行 pg_repack。这一次它敲响了警钟,感觉像是过度使用抗生素。所以我想为一个不想做详细研究的普通用户写一篇关于 pg_repack 是如何工作的博文,以及它在运行时是如何影响你的系统的,因为更好的理解有助于帮助我们在哪里使用它做出更明智的决定。

pg_repack 有很多功能(选项)。在这篇博文中,我的目的是只讨论它如何对碎片化/膨胀的表进行基本的重组。

预检查并确保齐全

这是为了确保pg_repack安装正确,并且在数据库中是可用的,我们以超级用户的身份运行 pg_repack 。它为清理 pg_repack 创建的临时对象做好准备。它会收集关于表和相关对象(如索引、TOAST、触发器等)的元数据,因为跟踪表和相关对象非常重要。同时还会检查失效的索引、冲突的触发器等对象。在此我们不会详细讨论这个问题。

实际的处理过程会先从获取表的OID上的咨询锁开始,以确保没有其他 pg_repack 也在处理该表。对于全表重组,这不是太大的问题,主要是为了仅对索引的重组不会相互干扰,也不会影响全表的重组。如果还有其他 pg_repaks 正在运行,重组可能会收到如下消息并退出:

ERROR: Another pg_repack command may be running on the table. Please try again later.

在表上持有 AccessExclusive锁的同时创建临时对象

是的没错,pg_repack 需要获取很重的 AccessExclusive 锁,不过是短暂性的,pg_repack 尝试通过执行如下语句来获得表的 AccessExclusive锁:

LOCK TABLE <tablename> IN AccessExclusiveMODE

AccessExclusive 锁要求没有其他会话正在访问该表,甚至是 SELECT 查询也不行。pg_repack 会等待 "wait-time" 这么久(默认为 60 秒)。"wait-time" 可以使用可选参数 --wait-timeout 调整。一旦这个等待时间结束,pg_repack 开始尝试取消掉冲突的语句。对于每次尝试,用户可能会看到如下消息。

WARNING: canceling conflicted backends
WARNING: canceling conflicted backends
...

所以需要注意的是:

当表上有很多并发的活动时,避免运行 pg_repack。因为允许新会话同时获得冲突的锁,例如 ACCESSSHARE,而等待 AccessExclusive锁的会话可能需要无限期地等待。我们应该为 pg_repack 选择一个适当的低活动时间窗口

这些尝试取消的动作会持续另一轮的 "wait-time" 这么久。但是即使在尝试获取 AccessExclusive锁第二轮后仍超时的话,如果仍没有获得 ACCESS EXCLUSIVE锁,它会升级,杀掉每个冲突的会话。因此,如果总等待时间超过了 "wait-time" 的两倍,pg_repack 会杀掉会话。对于突然终止的应用程序连接,是有很大问题的。如果应用层面没有妥善处理,这可能会导致中断。

pg_repack可能会发出如下消息:

WARNING: terminating conflicted backends

在PostgreSQL日志中,每个被杀死的会话都会有如下的条目:

2021-06-17 06:28:46.317 UTC [2761] FATAL: terminating connection due to administrator command
2021-06-17 06:28:46.317 UTC [2758] FATAL: terminating connection due to administrator command

因此,需要注意的一点是:

pg_repack 可以杀掉那些阻止 pg_repack 获取 AccessExclusive锁的会话,这可能会让应用程序发生中断或意料之外的行为。

如果超过了两倍的 "wait-time",pg_repack会继续杀掉会话。但是同样,这种行为也可以使用一个参数来控制,--no-kill-backend。如果指定了此参数, pg_repack 会尊重所有并发的会话并取消掉它自己,而不是试图取消或杀掉其他会话, pg_repack 可能会发出如下消息:

WARNING: timed out, do not cancel conflicting backends
INFO: Skipping repack public.t1 due to timeout

我相信这在生产系统中更可取。

所以需要注意的是:

在处理关键的应用系统时,始终记得指定 --no-kill-backend

当获取到 AccessExclusive 锁时,pg_repack 会创建所有临时对象,包括具有相同结构的替换表。

它会根据原始表创建一个主键TYPE。例如,如果我们正在重组某个单字段"id"主键的表,主键类型定义如下所示:

CREATE TYPE repack.pk_16423 AS (id integer)

这种类型的定义很有用,因为可以有复合主键。下面是一个处理含有复合键的表的示例。

CREATE TYPE repack.pk_16824 AS (id integer, id1 integer, id2 timestamp without time zone);

然后它会继续创建一个日志表来捕获 pg_repack 重组期间的所有的数据更改 (CDC-Change data capture) 。这个表含有BIGINT类型的主键,以及上面步骤中创建的原始表的主键类型和我们正在重组的表的行数据类型 (row type)。这一行可以保存我们重组的表的整个元组信息。

由于TYPE的定义和行类型,这个日志表的定义很容易。下面是一个例子:

CREATE TABLE repack.log_16423 (id bigserial PRIMARY KEY, pk repack.pk_16423, row public.t1)

现在 pg_repack 在要重组的表上创建一个触发器,这样只要表上有DML,就需要将相应的信息捕获到上面创建的日志表中。这是使用AFTER INSERT OR DELETE OR UPDATE触发器完成的。例如

CREATE TRIGGER repack_trigger AFTER INSERT OR DELETE OR UPDATE ON public.t1 FOR EACH ROW EXECUTE PROCEDURE repack.repack_trigger('INSERT INTO repack.log_16423(pk, row) VALUES( CASE WHEN $1 IS NULL THEN NULL ELSE (ROW($1.id)::repack.pk_16423) END, $2)')

由于 pg_repack 在这个阶段持有  AccessExclusive 锁,在这个阶段不会有任何并发的DML,这会在接下来的阶段改变。

在释放 AccessExclusive 锁之前,pg_repack 在这个阶段使用了一个重要的技巧,源代码中的注释说

/* While we are still holding an AccessExclusive lock on the table, submit
* the request for an AccessShare lock asynchronously from conn2.
* We want to submit this query in conn2 while connection's
* transaction still holds its lock, so that no DDL may sneak in
* between the time that connection commits and conn2 gets its lock.
*/

是的,pg_repack 使用另一个连接到数据库,并通过它发送一个 AccessShare 的锁请求。这可以防止在切换到 AccessShare 锁的过程中出现任何DDL。当主连接提交时,AccessShare 锁将被授予第二个连接。所以 pg_repack 使用两个数据库连接来完成工作。但仍然有可能会有DDL干扰。所以 pg_repack 默认会杀死所有的并发DDL。

一旦这个阶段完成,pg_repack 可以继续在第一个连接上通过提交事务释放 AccessExclusive 锁。这样,第二个连接的 AccessShare 锁请求会被授予。这个COMMIT非常特殊,该表上的所有日志表和触发器都将被提交,因此从此时起整个系统都可以使用它。

将行/元组复制到临时表

将元组从表复制到新表会使用SERIALIZABLE的隔离级别。因为在数据进入日志表和pg_repack将要创建的临时替代表之间不应该有任何的不一致。

BEGIN ISOLATION LEVEL SERIALIZABLE

由于 AccessExclusive 锁被移除,并发会话可以继续处理它们的 DML 和 SELECT查询。只有 DDL 会被阻塞。所以我们可以说这个表对事务和查询是可用的。

在复制数据之前,日志表会被截断。因为 pg_repack 只需要从数据拷贝开始时捕获的日志数据。

DELETE FROM repack.log_16423

同样,pg_repack 会试图杀死任何可能正在等待做 DDL 的会话,并获得表上的AcessShare锁。由于另一个连接已经持有AccessShare锁,因此可以毫不费力地获得This。由于另一个连接已经持有 AccessShare 锁,因此可以毫不费力地获得。

在这个阶段,pg_repack 会创建一个替代表,和原始表的结构完全相同,用于后续替换原始表,但没有任何数据。它是一个CTAS语句,类似于

CREATE TABLE repack.table_16423... AS SELECT col1,col2,col3... FROM ONLY public.t1 WITH NO DATA

接着就是数据的复制:

INSERT INTO repack.table_16423 SELECT col1,col2,co3... FROM ONLY public.t1

一旦数据复制结束,事务将被提交。根据负载和WAL日志的生成量来说,重组期间的最重的一个阶段完成了。

在此阶段,将在这个临时的替代表上创建索引和键。

将 CDC 日志应用到临时表

请记住,pg_repack 的主连接在此阶段没有持有表上任何的锁 (除了第二个连接的 AccessShare 锁) 。所以在这个阶段没有任何东西阻塞事务 (DML)。根据上一阶段数据复制所花费的时间,以及数据复制期间的并发事务,日志文件中可能会有很多 CDC (变更数据捕获) 条目。这需要复制到新的临时/替代表。

这个逻辑在 pg_repack 中使用 C 函数实现。你可以参考 repack_apply 的源代码。它从日志表中读取所有数据并处理 INSERTS、UPDATES 和 DELETES。为了加快重复操作的速度,使用了Prepared Statements。最后,所有处理过的日志表中的数据都将从日志表中删除。

用临时表交换原始表

这是由第二个连接执行的,因为它已经持有 AccessShare 锁,但是它会把锁升级为 AccessExclusive 锁。

LOCK TABLE public.t1 IN AccessExclusiveMODE

在持有表上的 AccessExclusive 锁期间,将再次执行CDC回放 (和"repack_apply"相同) 。所以如果日志表中出现了任何新的条目,它也会被处理。

原始表和临时表通过执行repack_swap 函数进行交换:

SELECT repack.repack_swap('16423');

这是 pg_repack 最漂亮强大的部分。这是在C函数 repack_swap 中实现的。不仅表被交换,所有权、关联的TOAST (表和索引) 、索引和依赖项也被交换。另外,OID也会被交换,这样即使在 pg_repack 后,表的oid仍然保持不变。交换的工作遵循COMMIT。

最后阶段的清理

pg_repack 使用它内置的C函数 repack_drop 来清理所有临时对象。为了防止任何并发会话获取表上可能阻止清理的锁,在清理之前需要获取 AccessExclusive 锁。这是第三次在表上放置 AccessExcluive 锁。

总结

pg_repack是一个最强大流行的扩展。我们鼓励在适当的监督下使用。但请避免过度使用。正如我试图解释的,我们应该预料到原始表到临时表之间的大量数据移动、触发器到日志表的写入、从日志表到临时表的数据复制,等等。所以我们也应该对更高的WAL日志产生量有所预期。考虑到所有的影响,pg_repack需要在低活动时间窗口执行,以避免不良后果。

最终需要重申的一些要点是:

  1. pg_repack 需要多次获取重量级的 AccessExclusive 锁,不过是短暂性的。
  2. 在高并发的情况下,几乎不可能拿到AccessExclusive锁。
  3. 如果 pg_repack 在 wait-time 期间无法获得 AcessExclusive 锁,默认情况下,它会尝试杀掉冲突的语句
  4. 如果总等待时间超过 wait-time 的两倍,它可能会杀掉会话。这可能会导致不良的结果和意外中断。
  5. pg_repack 的默认值可能不适用于关键系统。使用 --no-kill-backend 选项使其更加温和。
  6. 不允许针对正在进行 pg_repack 的表使用 DDL,任何尝试这样做的会话都可能会被杀掉。

原文链接:​​https://www.percona.com/blog/2021/06/24/understanding-pg_repack-what-can-go-wrong-and-how-to-avoid-it/​​


举报

相关推荐

0 条评论