Postgres long-运行 事务在父分区 table 上持有锁
Postgres long-running transaction holding lock on parent partitioned table
TL;DR:我们有 long-运行 导入,它似乎在父分区 table 上持有锁,即使没有直接引用父 table.
背景
在我们的系统中,我们有 inventories
和 inventory_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
锁,同时正在计划插入语句。这会导致锁冲突。
我看到两条出路:
分两步创建新分区:
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 开始),它与并发插入兼容。
使用服务器准备语句将 INSERT
放入分区,并确保在 开始长 运行 之前准备语句 ] 加载数据的事务。您可以为此使用 PostgreSQL 的 PREPARE
和 EXECUTE
语句,或者使用您的 API 的工具。
TL;DR:我们有 long-运行 导入,它似乎在父分区 table 上持有锁,即使没有直接引用父 table.
背景
在我们的系统中,我们有 inventories
和 inventory_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
锁,同时正在计划插入语句。这会导致锁冲突。
我看到两条出路:
分两步创建新分区:
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 开始),它与并发插入兼容。使用服务器准备语句将
INSERT
放入分区,并确保在 开始长 运行 之前准备语句 ] 加载数据的事务。您可以为此使用 PostgreSQL 的PREPARE
和EXECUTE
语句,或者使用您的 API 的工具。