Postgres long-运行 事务在父分区 table 上持有锁

Postgres long-running transaction holding lock on parent partitioned table

TL;DR:我们有 long-运行 导入,它似乎在父分区 table 上持有锁,即使没有直接引用父 table.

背景

在我们的系统中,我们有 inventoriesinventory_items。库存往往有 200k 左右的项目,我们的访问模式使用本机分区将 inventory_items table 分区为 inventory_id 是有意义的(我们在 Postgres 12 上)。换句话说,每个库存都有自己的分区 table of inventory_items。这是通过以下 DDL 完成的:

CREATE TABLE public.inventory_items (
  inventory_id integer NOT NULL,
  /* ... */
)
PARTITION BY LIST (inventory_id);

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

CREATE TABLE IF NOT EXISTS inventory_items_#{inventory_id}
  PARTITION OF inventory_items
  FOR VALUES IN (#{inventory_id});

长时间导入作业阻止创建新库存

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

我们注意到,虽然这些长导入是 运行,但无法创建新的清单,因为如上所述,创建清单意味着创建分区子项 inventory_items table,并且在 long-运行 导入和在 Web 仪表板中创建清单之间存在一些锁争用,这很糟糕:我们不能仅仅因为发生了完全不相关的导入就阻止用户创建清单。

导入时尝试创建库存时的事件/锁定顺序 运行

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

select pid, relname, mode
from pg_locks l
join pg_class t on l.relation = t.oid
where t.relkind = 'r';

本次查询returns成功obtained/held锁定;它不会显示正在 等待 获取锁的 pid(因为其他 pid 持有它)。对于那些,您必须查看 postgres 日志。

开始缓慢导入

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

 pid  |        relname     |       mode
------+--------------------+------------------
 9029 | inventory_items_16 | AccessShareLock
 9029 | inventory_items_16 | RowExclusiveLock

我们要导入的清单的 ID 为 16,因此持有的锁在属于该清单的 inventory_items 分区子 table 上。请注意,父 inventory_items table.

上似乎没有任何锁

尝试在 Web 仪表板中创建清单

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

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

 9029 | inventory_items_16 | AccessShareLock
 9029 | inventory_items_16 | RowExclusiveLock

PID 7089 是网络服务器。它成功地获取了库存(INSERT INTO inventories)上的 RowExclusiveLock,但是查看 postgres 日志,它正在尝试并未能获取 119795 上的 AccessExclusiveLock,它是父级 inventory_items table:

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);

我认为在创建子分区时父 table 需要 AccessExclusiveLock 的原因是因为 postgres 需要将一些内部模式元数据写入父 table 以便它可以将 inventory_id=16 的行路由到这个新的 table,这对我来说很有意义。

但是,根据我的 pg_locks 查询判断,我不明白锁争用的来源。 Web 服务器需要父级 table 上的 AccessExclusiveLock,但 pg_locks 显示唯一持有的锁是子级 inventory_items_16 table.

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

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

最后的小花絮

在极少数情况下,活动导入的存在不会阻止网络工作者。 90% 的时间它会,但有时不会。因此,在这种混合中的某个地方有一点点不确定性,它混淆了一切。

使用 CREATE TABLE ... PARTITION OF ... 创建分区需要对分区 table 进行 ACCESS EXCLUSIVE 锁定,这将与对分区 table.[=18= 的所有访问冲突]

另一方面,插入分区需要在分区 table 上的 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);
    

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

  2. 使用服务器准备语句将 INSERT 放入分区,并确保在 开始长 运行 之前准备语句 ] 加载数据的事务。您可以为此使用 PostgreSQL 的 PREPAREEXECUTE 语句,或者使用您的 API 的工具。