作者介绍 Laurenz Albe:Cybertec的高级顾问和支持工程师。自2006年以来,一直与PostgreSQL合作并为其做出贡献。 译者简介 陈雁飞:开源PostgreSQL爱好者,一直从事PostgreSQL数据库运维工作 最近,在排查PostgreSQL性能问题的时候,两次遇到子事务相关问题。所以,我想这个话题非常适合作为博客内容。
什么是子事务?
每个人都了解数据库事务。在PostgreSQL中,事务是默认工作在自动提交模式下,多语句情况下,需要显示调用BEGIN或者START TRANSACTION来开启一个事务,最后使用END或者COMMIT结束它。如果用ROLLBACK中断一个事务(或者数据库会话结束的时候没有执行提交操作),那么在事务中的操作将成为没有完成的。
现在子事务允许你回滚部分已经在事务中完成的工作。可以使用下面的标准语句在一个事务中开启子事务:
SAVEPOINT name;
“name”表示一个子事务的标识符(没有单引号!)。不能在SQL中提交一个子事务(将和包含它的事务一起自动提交),但是可以使用下面的命令回滚:
ROLLBACK TO SAVEPOINT name;
子事务的使用
子事务在长事务中有非常大的作用。在PostgreSQL中,事务中任何一个错误都会中断整个事务:
test=> BEGIN;BEGINtest=*> SELECT 'Some work is done'; ?column? ------------------- Some work is done(1 row) test=*> SELECT 12 / (factorial(0) - 1);ERROR: division by zerotest=!> SELECT 'try to do more work';ERROR: current transaction is aborted, commands ignored until end of transaction blocktest=!> COMMIT;ROLLBACK
test=> BEGIN;
BEGIN
test=*> SELECT 'Some work is done';
?column?
-------------------
Some work is done
(1 row)
test=*> SELECT 12 / (factorial(0) - 1);
ERROR: division by zero
test=!> SELECT 'try to do more work';
ERROR: current transaction is aborted, commands ignored until end of transaction block
test=!> COMMIT;
ROLLBACK
对于一个做了很多工作的事务来说,这是非常烦人的,因为这意味着失去到目前为止完成的所有工作。子事务可以帮助我们从这种情况中进行恢复
test=> BEGIN;BEGINtest=*> SELECT 'Some work is done'; ?column? ------------------- Some work is done(1 row) test=*> SAVEPOINT a;SAVEPOINTtest=*> SELECT 12 / (factorial(0) - 1);ERROR: division by zerotest=!> ROLLBACK TO SAVEPOINT a;ROLLBACKtest=*> SELECT 'try to do more work'; ?column? --------------------- try to do more work(1 row) test=*> COMMIT;COMMIT
test=> BEGIN;
BEGIN
test=*> SELECT 'Some work is done';
?column?
-------------------
Some work is done
(1 row)
test=*> SAVEPOINT a;
SAVEPOINT
test=*> SELECT 12 / (factorial(0) - 1);
ERROR: division by zero
test=!> ROLLBACK TO SAVEPOINT a;
ROLLBACK
test=*> SELECT 'try to do more work';
?column?
---------------------
try to do more work
(1 row)
test=*> COMMIT;
COMMIT
注意ROLLBACK TO SAVEPOINT回滚一个旧事务a的时候,会重新开始一个新的子事务。
PL/pgSQL中子事务
即使你从来没有使用过SAVEPOINT语句,但是你可能遇到过子事务。在PL/pgSQL中,上面的代码类似下面
BEGIN PERFORM 'Some work is done'; BEGIN -- a block inside a block PERFORM 12 / (factorial(0) - 1); EXCEPTION WHEN division_by_zero THEN NULL; -- ignore the error END; PERFORM 'try to do more work';END;
每次输入带有
EXCEPTION
子句的语句块时,都会开启一个新的子事务。当离开这个块的时候会提交该子事务,进入异常处理分支的时候表示回滚。
数据库之间兼容性
其它数据库处理事务中错误的方式不尽相同。不会中止完整的事务,而是仅仅回滚导致错误的语句,从而使事务本身处于活动状态。
当从这样的数据库迁移或移植到PostgreSQL中时,你可能需要在子事务中包装每个语句,以模拟上面的行为。
PostgreSQL JDBC驱动程序中有一个连接参数“autosave”,如果将其设置为“always”,就会在每条语句之前自动设置一个保存点,方便在失败的时候回滚。
如下所示,这种转换技巧存在严重的性能瓶颈。
性能测试用例
为了说明由于过度使用子事务导致性能问题,创建下面的测试用例表
CREATE UNLOGGED TABLE contend ( id integer PRIMARY KEY, val integer NOT NULL)WITH (fillfactor='50'); INSERT INTO contend (id, val)SELECT i, 0FROM generate_series(1, 10000) AS i; VACUUM (ANALYZE) contend;
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;
这个表数据量很少、不记录日志以及低的填充因子,这些都是为了尽可能降低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;
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;
第一组测试用例将设置60个子事务,第二组测试用例将设置90个子事务。通过使用预备语句方式尽可能减少查询解析的影响。
在每个数据库会话中,pgbench将:client_id替换成一个唯一的数字。所以只要没有不超过10个客户端,每个客户端的更新操作不会产生冲突,但是会查询其他客户端产生的数据行。
性能测试
由于机器只有8核,因此在测试中将使用6个并发客户端运行十分钟。 为了让“perf top”能查看到重要的信息,需要安装PostgreSQL调试符号信息。这在生产系统上也是推荐的。 TEST 1(60个子事务)
pgbench -f subtrans.sql -n -c 6 -T 600 transaction type: subtrans.sqlscaling factor: 1query mode: simplenumber of clients: 6number of threads: 1duration: 600 snumber of transactions actually processed: 100434latency average = 35.846 mstps = 167.382164 (including connections establishing)tps = 167.383187 (excluding connections establishing)
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_value1.75% [.] AllocSetAlloc+ 1.36% [.] pg_qsort+ 1.12% [.] base_yyparse+ 1.10% [.] TransactionIdIsCurrentTransactionId+ 0.96% [.] heap_hot_search_buffer+ 0.96% [.] LWLockAttemptLock+ 0.85% [.] HeapTupleSatisfiesVisibility+ 0.82% [.] heap_page_prune+ 0.81% [.] ExecInterpExpr+ 0.80% [.] SearchCatCache1+ 0.79% [.] BitmapHeapNext+ 0.64% [.] LWLockRelease+ 0.62% [.] MemoryContextAllocZeroAligned+ 0.55% [.]_bt_checkkeys 0.54% [.] hash_any+ 0.52% [.] _bt_compare 0.51% [.] ExecScan
+ 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
+ 0.96% [.] heap_hot_search_buffer
+ 0.96% [.] LWLockAttemptLock
+ 0.85% [.] HeapTupleSatisfiesVisibility
+ 0.82% [.] heap_page_prune
+ 0.81% [.] ExecInterpExpr
+ 0.80% [.] SearchCatCache1
+ 0.79% [.] BitmapHeapNext
+ 0.64% [.] LWLockRelease
+ 0.62% [.] MemoryContextAllocZeroAligned
+ 0.55% [.]_bt_checkkeys
0.54% [.] hash_any
+ 0.52% [.] _bt_compare
0.51% [.] ExecScan
Test2(90个子事务)
pgbench -f subtrans.sql -n -c 6 -T 600 transaction type: subtrans.sqlscaling factor: 1query mode: simplenumber of clients: 6number of threads: 1duration: 600 snumber of transactions actually processed: 41400latency average = 86.965 mstps = 68.993634 (including connections establishing)tps = 68.993993 (excluding connections establishing)
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+ 1.26% [.] tbm_iterate+ 1.22% [.] hash_search_with_hash_value+ 1.08% [.] AllocSetAlloc+ 0.77% [.] heap_hot_search_buffer+ 0.72% [.] pg_qsort+ 0.72% [.] base_yyparse+ 0.66% [.] SubTransGetParent+ 0.62% [.] HeapTupleSatisfiesVisibility+ 0.54% [.] ExecInterpExpr+ 0.51% [.] SearchCatCache1
+ 10.59% [.] LWLockAttemptLock
+ 7.12% [.] LWLockRelease
+ 2.70% [.] LWLockAcquire
+ 2.40% [.] SimpleLruReadPage_ReadOnly
+ 1.30% [.] TransactionIdIsCurrentTransactionId
+ 1.26% [.] tbm_iterate
+ 1.22% [.] hash_search_with_hash_value
+ 1.08% [.] AllocSetAlloc
+ 0.77% [.] heap_hot_search_buffer
+ 0.72% [.] pg_qsort
+ 0.72% [.] base_yyparse
+ 0.66% [.] SubTransGetParent
+ 0.62% [.] HeapTupleSatisfiesVisibility
+ 0.54% [.] ExecInterpExpr
+ 0.51% [.] SearchCatCache1
即使考虑到test2都是长事务,与test1相比,仍然有60%性能差距。
子事务实现
要了解发生什么,我们需要了解事务和子事务实现方式。
当一个事务或者子事务中修改了数据后,会为该事务分配一个事务ID(transaction ID)。PostgreSQL在提交日志(commit log)中跟踪这些事务ID信息,日志信息持久化存储在数据目录下pg_xact子目录中。
但是,事务和子事务之间有下面几点差异:
l. 每个子事务包含一个事务或者子事务(“父亲”)
l. 提交子事务不会刷新WAL
l. 一个数据库会话中有且只能有一个事务,但是可以有多个子事务
存储给定子事务的父信息相关的(子)事务信息持久化存储在数据目录下的pg_subtrans子目录。由于这些信息随着包含事务结束后立即变成过去时,因此不必在关闭或者崩溃期间保留这些数据。
子事务和可见性
PostgreSQL中行级版本(元组)可见性由xmin和xmax系统列决定的,分别表示创建和删除事务的事务ID。如果存储的事务ID是子事务信息,那么PostgreSQL还必须查询包含(子)事务的状态,以确定对该事务ID是否可见。
为了确定语句可以看到哪些元组,PostgreSQL在语句(或事务)开始的地方首先获取数据库的快照信息。快照主要包含如下信息:
l. 最大事务ID:任何超过该事务ID都是不可见的
l. 获取快照的时候处于活跃状态的事务和子事务
l. 当前(子)事务中可见的最早命令号(commnad number)
快照通过查询进程数组(process array)信息来进行初始化,进程数组保存在共享内存中并包含有当前运行进程的相关信息。当前,它也包含后端进程的当前事务ID,并且每个会话最多可以容纳64个未中止的子事务。如果有超过64个这样的子事务,那么快照被标记为子事务溢出(suboverflowed)。
结果分析
一个子溢出的快照不会包含检测可见性的所有数据信息,所以PostgreSQL有时将不得不求助于pg_subtrans。这些页缓存在共享内存中,但是在perf中可以看到SimpleLruReadPage_ReadOnly函数排在前面输出。其它事务必须更新pg_subtrans后才能注册子事务,可以在perf输出中看到如何与读进程争夺轻量级锁。
分析子事务太多问题
除了查看”perf top”,还有其它指向该问题方向的可疑点:
l. 运行单个进程的时候负载表现很好,但是并发多个数据库会话后会变高
l. 在pg_stat_activity视图中经常看到等待实践“SubtransControlLock”
l. 如果使用“pg_export_snapshot()”函数导出快照信息,数据目录下的pg_snapshots 子目录保存的结果文件中包含有“sof:1”信息,其表示子事务数组溢出
结论
子事务是一个很好的工具,但是需要合理使用它。如果需要并发,每个事务不要启动超过64个子事务。
本文中提供的分析方法应该可以帮助你确定是否存在类似问题。 找到问题的根因可能很棘手。例如:对于SQL语句的每个结果行(可能在触发器中)调用的带有异常处理程序的函数,启动新的子事务不会那么明显。