Postgres 长时间运行的事务在父分区表上持有锁

Ale*_*eer 8 database postgresql database-partitioning database-locking postgresql-12

TL;DR:我们有长时间运行的导入,它们似乎在父分区表上持有锁,即使没有任何东西直接引用父表。

背景

在我们的系统中,我们有inventoriesinventory_items。库存往往有 200k 左右的物品,对于我们的访问模式来说,使用本机分区对inventory_items表进行分区是有意义的inventory_id(我们使用的是 Postgres 12)。换句话说,每个库存都有自己的 inventory_items 分区表。这是通过以下 DDL 完成的:

CREATE TABLE public.inventory_items (
  inventory_id integer NOT NULL,
  /* ... */
)
PARTITION BY LIST (inventory_id);
Run Code Online (Sandbox Code Playgroud)

在我们的应用程序代码中,当通过 Web 仪表板创建库存时,我们会通过以下方式自动创建分区子 inventory_items 表:

CREATE TABLE IF NOT EXISTS inventory_items_#{inventory_id}
  PARTITION OF inventory_items
  FOR VALUES IN (#{inventory_id});
Run Code Online (Sandbox Code Playgroud)

长期进口工作阻碍创造新库存

这些库存通常每天通过 CSV 或其他方式完全重新加载/重新导入一次,并且这些导入任务有时可能需要一段时间。

我们注意到,当这些长时间导入正在运行时,不可能创建新的清单,因为如上所述,创建清单意味着创建分区子表inventory_items,并且长时间运行的导入和创建分区子表之间存在一些锁争用。网络仪表板中的库存,这很糟糕:我们不能仅仅因为发生了完全不相关的导入就阻止用户创建库存。

导入运行时尝试创建清单时的事件/锁定序列

我在 psql 中使用以下查询来确定谁持有哪些锁:

select pid, relname, mode
from pg_locks l
join pg_class t on l.relation = t.oid
where t.relkind = 'r';
Run Code Online (Sandbox Code Playgroud)

该查询返回成功获取/持有的锁;它不会显示正在等待获取锁的 pid(因为其他一些 pid 持有该锁)。对于这些,你必须查看 postgres 日志。

开始缓慢导入

导入开始后,工作进程(pid 9029)将获取以下锁

 pid  |        relname     |       mode
------+--------------------+------------------
 9029 | inventory_items_16 | AccessShareLock
 9029 | inventory_items_16 | RowExclusiveLock
Run Code Online (Sandbox Code Playgroud)

我们要导入的库存的 ID 为 16,因此持有的锁位于属于该库存的 inventory_items 分区子表上。请注意,父表上似乎没有任何锁inventory_items

尝试在 Web 仪表板中创建库存

当我尝试在仪表板中创建清单时,请求由于 30 秒的 SQL 语句超时而停止并超时。在超时之前,锁看起来像这样:

 pid  |        relname     |       mode
------+--------------------+------------------
 7089 | inventories        | RowExclusiveLock

 9029 | inventory_items_16 | AccessShareLock
 9029 | inventory_items_16 | RowExclusiveLock
Run Code Online (Sandbox Code Playgroud)

PID 7089 是 Web 服务器。它成功地获取了库存(the INSERT INTO inventories)上的 RowExclusiveLock,但是查看 postgres 日志,它尝试获取 119795(即父inventory_items表)上的 AccessExclusiveLock,但失败了:

postgres.7089 [RED] [29-1]  sql_error_code = 00000 LOG:  statement: CREATE TABLE IF NOT EXISTS inventory_items_16
postgres.7089 [RED] [29-2]    PARTITION OF inventory_items
postgres.7089 [RED] [29-3]    FOR VALUES IN (16);
postgres.7089 [RED] [29-4]
postgres.7089 [RED] [30-1]  sql_error_code = 00000 LOG:  process 7089 still waiting for AccessExclusiveLock on relation 119795 of database 16402 after 1000.176 ms
postgres.7089 [RED] [30-2]  sql_error_code = 00000 DETAIL:  Process holding the lock: 9029. Wait queue: 7089.
postgres.7089 [RED] [30-3]  sql_error_code = 00000 STATEMENT:  CREATE TABLE IF NOT EXISTS inventory_items_16
postgres.7089 [RED] [30-4]    PARTITION OF inventory_items
postgres.7089 [RED] [30-5]    FOR VALUES IN (16);
Run Code Online (Sandbox Code Playgroud)

我认为创建子分区时父表上需要 AccessExclusiveLock 的原因是因为 postgres 需要将一些内部 schema-y 元数据写入父表,以便它可以将 inventory_id=16 的行路由到这个新表,这使得对我来说有感觉。

但是,从我的 pg_locks 查询来看,我不明白锁争用来自哪里。Web 服务器需要父表上的 AccessExclusiveLock,但 pg_locks 显示唯一持有的锁位于子inventory_items_16表上。

那么,这里可能发生了什么?子表上的锁是否会在父表上的锁中“扩展”,或者以其他方式与父表上的锁竞争?

还有其他方法可以解决这个问题吗?我们对对这些表进行分区的决定非常有信心,但是这种意外的锁争用正在导致真正的问题,因此我们正在寻找一种干净的、最少维护的方法来保持这种基本架构。

最后的小花絮

在极少数情况下,活动导入的存在不会阻止 Web Worker。90% 的情况下是这样,但有时却不是。因此,在这种混合中的某个地方存在着一点点不确定性,它混淆了一切。

Lau*_*lbe 9

创建分区CREATE TABLE ... PARTITION OF ...需要ACCESS EXCLUSIVE对分区表加锁,这会与对分区表的所有访问发生冲突。

另一方面,插入分区需要在计划插入语句时ACCESS SHARE锁定分区表。这会导致锁冲突。

我看到两条出路:

  1. 分两步创建新分区:

    CREATE TABLE inventory_items_42 (
       LIKE inventory_items INCLUDING DEFAULTS INCLUDING CONSTRAINTS
    );
    ALTER TABLE inventory_items
       ATTACH PARTITION inventory_items_42 FOR VALUES IN (42);
    
    Run Code Online (Sandbox Code Playgroud)

    这只需要SHARE UPDATE EXCLUSIVE分区表上的锁(从 PostgreSQL v12 开始),它与并发插入兼容。

  2. 使用服务器准备好的语句进入INSERT分区,并确保在启动加载数据的长时间运行事务之前准备好该语句。您可以使用 PostgreSQL 的PREPAREandEXECUTE语句来实现此目的,或者使用您的 API 设施。