PostgreSQL 子事务问题: 子事务缓存溢出

John Doe 五月 21, 2024

摘要:在本文中,我们将了解 PostgreSQL 中的子事务问题:子事务缓存溢出。

目录

测试准备工作

为了演示过度使用子事务导致的问题,我们来创建一个测试表:

CREATE UNLOGGED TABLE contend (
    id integer PRIMARY KEY,
    val integer NOT NULL
)
WITH (fillfactor='50');

INSERT INTO contend (id, val)
SELECT i, 0
FROM generate_series(1, 10000) AS i;

VACUUM (ANALYZE) contend;

该表很小,关闭了 WAL 日志记录,并设置了一个较低的填充因子,以尽可能减少所需的 I/O。这样,就可以更好地观察子事务的效果。

这里将使用 pgbench(PostgreSQL 附带的基准测试工具),来运行以下自定义 SQL 脚本:

BEGIN;
PREPARE sel(integer) AS
   SELECT count(*)
   FROM contend
   WHERE id BETWEEN $1 AND $1 + 100;
PREPARE upd(integer) AS
   UPDATE contend SET val = val + 1
   WHERE id IN ($1, $1 + 10, $1 + 20, $1 + 30);

SAVEPOINT a;
set rnd random(1,990)
EXECUTE sel(10 * :rnd + :client_id + 1);
EXECUTE upd(10 * :rnd + :client_id);

SAVEPOINT a;
set rnd random(1,990)
EXECUTE sel(10 * :rnd + :client_id + 1);
EXECUTE upd(10 * :rnd + :client_id);

...

SAVEPOINT a;
set rnd random(1,990)
EXECUTE sel(10 * :rnd + :client_id + 1);
EXECUTE upd(10 * :rnd + :client_id);

DEALLOCATE ALL;
COMMIT;

该脚本将为 1 号测试用例设置 60 个保存点,为 2 号测试用例设置 90 个保存点。它使用预备语句来最大程度地减少查询分析的开销。

pgbench将会用唯一的数据库会话编号替换:client_id。因此,只要不超过 10 个客户端,每个客户端的UPDATE不会与其他客户端的UPDATE冲突,但它们会SELECT彼此的行。

性能测试

由于测试机器有 8 个核,这里将使用 6 个并发客户端运行测试十分钟。

要使用 “perf top“ 查看有意义的信息,您需要安装 PostgreSQL 调试用的符号信息。推荐在生产系统上进行安装。

测试 1(60 个子事务)

$ pgbench -f subtrans.sql -n -c 6 -T 600

transaction type: subtrans.sql
scaling factor: 1
query mode: simple
number of clients: 6
number of threads: 1
duration: 600 s
number of transactions actually processed: 100434
latency average = 35.846 ms
tps = 167.382164 (including connections establishing)
tps = 167.383187 (excluding connections establishing)

这是在测试运行时,执行 “perf top --no-children --call-graph=fp --dsos=/usr/pgsql-12/bin/postgres“ 显示的内容:

+    1.86%  [.] tbm_iterate
+    1.77%  [.] hash_search_with_hash_value
     1.75%  [.] AllocSetAlloc
+    1.36%  [.] pg_qsort
+    1.12%  [.] base_yyparse
+    1.10%  [.] TransactionIdIsCurrentTransactionId

测试 2(90 个子事务)

$ pgbench -f subtrans.sql -n -c 6 -T 600

transaction type: subtrans.sql
scaling factor: 1
query mode: simple
number of clients: 6
number of threads: 1
duration: 600 s
number of transactions actually processed: 41400
latency average = 86.965 ms
tps = 68.993634 (including connections establishing)
tps = 68.993993 (excluding connections establishing)

这是 “perf top --no-children --call-graph=fp --dsos=/usr/pgsql-12/bin/postgres” 显示的内容:

+   10.59%  [.] LWLockAttemptLock
+    7.12%  [.] LWLockRelease
+    2.70%  [.] LWLockAcquire
+    2.40%  [.] SimpleLruReadPage_ReadOnly
+    1.30%  [.] TransactionIdIsCurrentTransactionId

即使我们考虑到 2 号测试中的事务执行了更多的操作,与 1 号测试相比,仍然有 60% 的性能衰退

子事务的实现

要了解发生了什么事情,我们必须了解事务和子事务是如何实现的。

每当一个事务或子事务修改数据时,它都会被分配一个事务 ID。PostgreSQL 在提交日志中跟踪这些事务 ID,该日志的内容存放在数据目录的pg_xact目录中。

但是,事务和子事务之间存在一些差异:

  • 每个子事务都有一个所属的事务或子事务(“父事务”)
  • 提交一个子事务不需要刷写 WAL 日志
  • 在任意时刻,每个数据库会话只能有一个事务,但可有许多子事务

哪个(子)事务是给定子事务的父事务的信息,存放在数据目录的pg_subtrans目录中。因为一旦所属的事务结束,这些信息就会过时,所以在关闭或崩溃时不必保留这些数据。

子事务和可见性

一个行版本(“元组”)在 PostgreSQL 中的可见性由xminxmax系统列决定,它们包含插入和删除该行的事务 ID。如果存储的事务 ID 是子事务的 ID,PostgreSQL 还必须查询所属(子)事务的状态,以确定事务 ID 是否有效。

为了确定语句可以看到哪些元组,PostgreSQL 会在语句(或事务)的开头创建一个数据库的快照。这样的快照包括了:

  • 最大事务 ID:从该事务开始后的所有内容都是不可见的
  • 创建快照时处于活动状态的事务和子事务列表
  • 当前(子)事务中最早可见命令的命令编号

快照信息是通过查看进程数组来进行初始化的,该数组存储在共享内存中,它包含了所有当前正在运行的后端的信息。当然,这也包含了后端的当前事务 ID,并且每个会话最多可缓存 64 个未中止的子事务。如果此类子事务超过 64 个,则快照会标记为 “子事务缓存溢出”。

测试结果分析

子事务溢出时的快照,不包含确定可见性所需的所有数据,因此 PostgreSQL 偶尔不得不访问pg_subtrans中的页面。这些页面被缓存在共享缓冲区中,但是您可以看到,在perf输出中,SimpleLruReadPage_ReadOnly的调用开销处于高位。其他事务必须更新pg_subtrans以注册子事务,您可以在perf输出中看到它们如何与读取者争夺轻量级锁。