Ale*_*eer 8 database postgresql database-partitioning database-locking postgresql-12
TL;DR:我们有长时间运行的导入,它们似乎在父分区表上持有锁,即使没有任何东西直接引用父表。
在我们的系统中,我们有inventories和inventory_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。
当我尝试在仪表板中创建清单时,请求由于 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% 的情况下是这样,但有时却不是。因此,在这种混合中的某个地方存在着一点点不确定性,它混淆了一切。
创建分区CREATE TABLE ... PARTITION OF ...需要ACCESS EXCLUSIVE对分区表加锁,这会与对分区表的所有访问发生冲突。
另一方面,插入分区需要在计划插入语句时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);
Run Code Online (Sandbox Code Playgroud)
这只需要SHARE UPDATE EXCLUSIVE分区表上的锁(从 PostgreSQL v12 开始),它与并发插入兼容。
使用服务器准备好的语句进入INSERT分区,并确保在启动加载数据的长时间运行事务之前准备好该语句。您可以使用 PostgreSQL 的PREPAREandEXECUTE语句来实现此目的,或者使用您的 API 设施。
| 归档时间: |
|
| 查看次数: |
4300 次 |
| 最近记录: |