由 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 中的可见性由xmin
和xmax
系统列决定,它们包含插入和删除该行的事务 ID。如果存储的事务 ID 是子事务的 ID,PostgreSQL 还必须查询所属(子)事务的状态,以确定事务 ID 是否有效。
为了确定语句可以看到哪些元组,PostgreSQL 会在语句(或事务)的开头创建一个数据库的快照。这样的快照包括了:
- 最大事务 ID:从该事务开始后的所有内容都是不可见的
- 创建快照时处于活动状态的事务和子事务列表
- 当前(子)事务中最早可见命令的命令编号
快照信息是通过查看进程数组来进行初始化的,该数组存储在共享内存中,它包含了所有当前正在运行的后端的信息。当然,这也包含了后端的当前事务 ID,并且每个会话最多可缓存 64 个未中止的子事务。如果此类子事务超过 64 个,则快照会标记为 “子事务缓存溢出”。
测试结果分析
子事务溢出时的快照,不包含确定可见性所需的所有数据,因此 PostgreSQL 偶尔不得不访问pg_subtrans
中的页面。这些页面被缓存在共享缓冲区中,但是您可以看到,在perf
输出中,SimpleLruReadPage_ReadOnly
的调用开销处于高位。其他事务必须更新pg_subtrans
以注册子事务,您可以在perf
输出中看到它们如何与读取者争夺轻量级锁。