ITPub博客

首页 > 数据库 > 数据库开发技术 > 子事务滥用的危害

子事务滥用的危害

原创 数据库开发技术 作者:u_a9af4a561c3b 时间:2021-11-29 15:27:50 0 删除 编辑

备库如何知晓子事务可见性

介绍

作为经验丰富的 PostgreSQL 用户,您可能在数据库集群中设置流复制以对数据进行多次备份方面拥有丰富的经验。但是您是否想知道备库如何正确确定从主库发送的元组是否应该对用户可见。在一个事务包含多个子事务的情况下,备库如何确定这种情况下的可见性?你可能会说……好吧,它是PostgreSQL,所以它可以正常工作……这是真的。如果你对 PostgreSQL 如何在内部做某些事情感到好奇和感兴趣,那么这个博客可能对你很有趣,因为我们将讨论正常事务和子事务以及两者如何影响备库确定元组可见性的。

如何确定元组的可见性

PostgreSQL 根据事务ID(又叫做 txid )的概念来确定元组的可见性,它通常作为 xmin 或 xmax 存储在元组的 header 中,其中 xmin 保存插入此元组的事务 txid,t_xmax 保存的事务txid 表示删除或更新了这个元组。如果一个元组没有被删除,它的 t_xmax 是 0。还有另一种称为提交日志(又叫做 clog)的机制,它包含当前活动事务 ID 的信息。当插入的元组具有有效的 t_xmin 和无效的 t_xmax 时,PostgreSQL 必须首先查询 clog 以确定存储在 t_xmin 中的 txid 是否可见,然后将结果存储在元组的关于可见性信息的 hint_bit 字段中,这样它就不必总是去查阅 clog,这可能导致资源消耗。如果元组具有有效的 t_xmax,则无需查看 clog,因为元组必须是不可见的。

这只是关于如何确定可见性的基本想法,但足以让我继续阐述此篇博客。有关可见性的更多信息,您可以参考 The Internals of PostgreSQL

什么是子事务

顾名思义,它是一个较小的事务,存在于常规事务中,可以在BEGIN语句之后使用SAVEPOINT关键字创建它。通常,当你发出BEGIN语句时,后续DML语句(如INSERT或UPDATE)创建的所有元组都将具有与这些元组相关联的txid,而且它们都是相同的。

当在这个事务中发出SAVEPOINT语句时,后续DML语句创建的所有元组将具有不同的txid,该txid通常比主事务的txid大。当再次发出SAVEPOINT语句时,txid再次发生变化。子事务的优点是,在一个非常大的事务中,整个事务不会因为错误而中止。它允许你回滚到前面创建的SAVEPOINT,并继续执行主事务。

子事务和常规事务之间最大的区别是没有与子事务关联的提交概念,并且它不会记录在WAL文件中。但是,如果子事务被中止,它将被记录在WAL文件中,然后被发送到备用服务器。

备库如何在常规事务中确定可见性

下面是一个没有任何子事务的常规事务示例

BEGIN;INSERT INTO test VALUES(111,111111);    => a heap_redo WAL record having t_xmin = 500 is sent to standbyINSERT INTO test VALUES(111,222222);    => a heap_redo WAL record having t_xmin = 500 is sent to standbyINSERT INTO test VALUES(111,333333);    => a heap_redo WAL record having t_xmin = 500 is sent to standbyCOMMIT;   => a XLOG_XACT_COMMIT WAL record having t_xmin = 500 is sent to standby

在一般情况下,上述查询将在流复制期间向备库发送至少4条WAL记录。前3个包含实际的元组信息,其中t_xmin设置为当前事务ID,在本例中为500。换句话说,在发出COMMIT之前,备库应该已经从主库接收了3条WAL日志记录并完成了重做,这意味着3个元组已经存在于备库的内存缓冲区中。但是,它们还不可见,因为备库中不存在事务ID 500。直到在主服务器中发出 COMMIT 之后,才会生成 XLOG_XACT_COMMIT 的WAL记录以发送到备库以通知事务 ID 500 现在已提交且有效。当备库收到 XLOG_XACT_COMMIT WAL 记录时,它最终会更新到clog,以便后续的 SELECT 查询可以进行可见性检查。

备库如何在一个包含子事务的常规事务中处理可见性

现在,让我们使用相同的示例,但添加一些SAVEPOINT来创建子事务

BEGIN;INSERT INTO test VALUES(111,111111);    => a heap_redo WAL record having t_xmin = 500 is sent to standbyINSERT INTO test VALUES(111,222222);    => a heap_redo WAL record having t_xmin = 500 is sent to standbyINSERT INTO test VALUES(111,333333);    => a heap_redo WAL record having t_xmin = 500 is sent to standbySAVEPOINT A;INSERT INTO test VALUES(111,444444);    => a heap_redo WAL record having t_xmin = 501 is sent to standbyINSERT INTO test VALUES(111,555555);    => a heap_redo WAL record having t_xmin = 501 is sent to standbyINSERT INTO test VALUES(111,666666);    => a heap_redo WAL record having t_xmin = 501 is sent to standbySAVEPOINT B;INSERT INTO test VALUES(111,777777);    => a heap_redo WAL record having t_xmin = 502 is sent to standbyINSERT INTO test VALUES(111,888888);    => a heap_redo WAL record having t_xmin = 502 is sent to standbyINSERT INTO test VALUES(111,999999);    => a heap_redo WAL record having t_xmin = 502 is sent to standbyCOMMIT;   => a XLOG_XACT_COMMIT WAL record having t_xmin = 500 is sent to standby

可以看到,在SAVEPOINT创建之后,tuple的 t_xmin值会发生变化,不再等于当前的事务ID,示例中为500。在发出 COMMIT 之前,备用数据库应该收到 9 条 WAL 记录,其中 3 个具有 t_xmin = 500,3 个具有 t_xmin = 501 和 3 个具有 t_xmin = 502。它们在备库中尚不可见,因为这些事务 ID 尚不存在。当主节点发出 COMMIT 语句时,它将生成一个 XLOG_XACT_COMMIT 的WAL 记录并发送到备节点,记录中仅包含当前事务ID 500而没有501和502。

提交后,备库能够看到从主库复制的所有 9 条元组。现在,您可能会问...... 为什么?为什么备库知道 txid = 501 和 502 也是可见的,即使主库从未发送 XLOG_XACT_COMMIT 的WAL记录来告诉备库 501 和 502 也已提交。继续阅读以找出答案。

使用类似的示例,如果回滚到其中一个savepoint,主服务器将发送一个XLOG_XACT_ABORT 的WAL记录,通知备库事务ID已经无效。

BEGIN;INSERT INTO test VALUES(111,111111);    => a heap_redo WAL record having t_xmin = 500 is sent to standbyINSERT INTO test VALUES(111,222222);    => a heap_redo WAL record having t_xmin = 500 is sent to standbyINSERT INTO test VALUES(111,333333);    => a heap_redo WAL record having t_xmin = 500 is sent to standbySAVEPOINT A;INSERT INTO test VALUES(111,444444);    => a heap_redo WAL record having t_xmin = 501 is sent to standbyINSERT INTO test VALUES(111,555555);    => a heap_redo WAL record having t_xmin = 501 is sent to standbyINSERT INTO test VALUES(111,666666);    => a heap_redo WAL record having t_xmin = 501 is sent to standbySAVEPOINT B;INSERT INTO test VALUES(111,777777);    => a heap_redo WAL record having t_xmin = 502 is sent to standbyINSERT INTO test VALUES(111,888888);    => a heap_redo WAL record having t_xmin = 502 is sent to standbyINSERT INTO test VALUES(111,999999);    => a heap_redo WAL record having t_xmin = 502 is sent to standbyROLLBACK TO SAVEPOINT B;  => a XLOG_XACT_ABORT WAL record having t_xmin = 502 is sent to standbyCOMMIT;   => a XLOG_XACT_COMMIT WAL record having t_xmin = 500 is sent to standby

在这个例子中,当回滚到SAVEPOINT B时,XLOG_XACT_ABORT 的WAL记录将被发送到备库,以使事务ID 502无效。因此,当主库提交时,txid=502的记录将不可见,因为主库已经通知。

备库的KnownAssignedTransactionIds

除了处理XLOG_XACT_COMMIT和XLOG_XACT_ABORT的WAL记录来确定txid是否有效之外,它实际上还管理一个名为KnownAssignedTransactionIds的全局txid列表。当一个备库接收到一个heap_redo的WAL记录时,它会将它的xmin保存到KnownAssignedTransactionIds。在这里再次使用与上面相同的例子

BEGIN;INSERT INTO test VALUES(111,111111);    => a heap_redo WAL record having t_xmin = 500 is sent to standby, and standby puts 500 in KnownAssignedTransactionIdsINSERT INTO test VALUES(111,222222);    => a heap_redo WAL record having t_xmin = 500 is sent to standby, and standby puts 500 in KnownAssignedTransactionIdsINSERT INTO test VALUES(111,333333);    => a heap_redo WAL record having t_xmin = 500 is sent to standby, and standby puts 500 in KnownAssignedTransactionIdsSAVEPOINT A;INSERT INTO test VALUES(111,444444);    => a heap_redo WAL record having t_xmin = 501 is sent to standby, and standby puts 501 in KnownAssignedTransactionIdsINSERT INTO test VALUES(111,555555);    => a heap_redo WAL record having t_xmin = 501 is sent to standby, and standby puts 501 in KnownAssignedTransactionIdsINSERT INTO test VALUES(111,666666);    => a heap_redo WAL record having t_xmin = 501 is sent to standby, and standby puts 501 in KnownAssignedTransactionIdsSAVEPOINT B;INSERT INTO test VALUES(111,777777);    => a heap_redo WAL record having t_xmin = 502 is sent to standby, and standby puts 502 in KnownAssignedTransactionIdsINSERT INTO test VALUES(111,888888);    => a heap_redo WAL record having t_xmin = 502 is sent to standby, and standby puts 502 in KnownAssignedTransactionIdsINSERT INTO test VALUES(111,999999);    => a heap_redo WAL record having t_xmin = 502 is sent to standby, and standby puts 502 in KnownAssignedTransactionIdsCOMMIT;   => a XLOG_XACT_COMMIT WAL record having t_xmin = 500 is sent to standby and standby submits 500, 501 and 502 to clog as valid.

在主库上发出 COMMIT 之前,备库的KnownAssignedTransactionIds中已经存在 txid 500、501 和 502。当备库收到 XLOG_XACT_COMMIT 的WAL记录时,它实际上会将来自 WAL 的 txid 500 加上来自KnownAssignedTransactionIds的 501 和 502 提交给clog,以便随后的SELECT 查询clog时会说 txid 500、501 和 502 是可见的。如果主库发送 XLOG_XACT_ABORT回滚到之前的 SAVEPOINT 的结果,那么无效的 txid 将从KnownAssignedTransactionIds中删除,因此当事务提交时,无效的 txid 将不会提交到clog。

小结

正常情况下,事务里面报错之后,就无法再进一步操作了。

postgres=# begin;BEGINpostgres=*# select 1/0;ERROR:  division by zeropostgres=!# select 1 from t1 limit 1;ERROR:  current transaction is aborted, commands ignored until end of transaction blockpostgres=!# rollback ;ROLLBACK

当然,你也可以通过on_error_rollback这个内置变量来实现事务内报错仍可以继续运行

postgres=# \set ON_ERROR_ROLLBACK onpostgres=# begin;BEGINpostgres=*# select 1/0;ERROR:  division by zeropostgres=*# select 1 from t1 limit 1; ?column? ----------        1(1 row) postgres=*# commit ;COMMIT

但是本质上也是通过savepoint来实现的

postgres=# create table t2(id int);CREATE TABLEpostgres=# begin;BEGINpostgres=*# select txid_current(); txid_current --------------          946(1 row) postgres=*# select 1/0;ERROR:  division by zeropostgres=*# insert into t2 values(1);INSERT 0 1postgres=*# commit ;COMMITpostgres=# select xmin,xmax,id from t2; xmin | xmax | id ------+------+----  948 |    0 |  1(1 row) postgres=# begin;BEGINpostgres=*# select txid_current(); txid_current --------------          949(1 row) postgres=*# select 1/0;                      ---950ERROR:  division by zeropostgres=*# select 1/0;                      ---951ERROR:  division by zeropostgres=*# insert into t2 values(2);        ---952INSERT 0 1postgres=*# commit;COMMITpostgres=# select xmin,xmax,id from t2; xmin | xmax | id ------+------+----  948 |    0 |  1  952 |    0 |  2(2 rows)

可以看到,会导致事务ID的消耗进一步加剧。

参照digoal的 PostgreSQL savepoint 的内存开销以及子事务溢出问题以及 SUBTRANSACTIONS AND PERFORMANCE IN POSTGRESQL

1.每个savepoint都会消耗一个txid2.每个savepoint消耗8K的会话本地内存(CurTransactionContext)3.当有并发事务时, 建议每个事务的子事务不要超过64个, 否则会引入SubtransControlLock等待事件, 可能影响性能。

SubtransControlLock:Waiting to read or update sub-transaction information.

快照通过查询进程数组(process array)信息来进行初始化,process array 保存在共享内存中并包含有当前所有正在运行的进程的相关信息。当前,它也包含进程的当前事务ID,并且每个会话最多可以容纳64个未中止的子事务。如果有超过64个这样的子事务,那么快照被标记为子事务溢出(suboverflowed)。

子事务溢出(suboverflowed)快照不包含确定可见性所需的所有数据,因此PostgreSQL有时不得不求助于 pg_subtrans。这些页面缓存在共享缓冲区中,但您可以在 perf 的输出中,看到SimpleLruReadPage_ReadOnly 的开销。其他事务必须更新 pg_subtrans 以注册子事务,您可以在 perf 输出中看到它们如何与读进程争夺轻量级锁。

之前同事遇到过一起实际案例,也是大量子事务带来的性能问题。源端OGG同步过来,最开始是6000条提交一次,也就意味着

start transaction;insert into xxxsavepoint 1 insert into xxxsavepoint 2 insert into xxxsavepoint 3...

总共6000个savepoint,大量的SubtransControlLock锁的竞争。后续更改为50条就提交一次了之后,问题得以解决。

前文译自 https://www.highgo.ca/2021/03/03/how-postgresql-handles-sub-transaction-visibility-in-streaming-replication/

原文作者:PostgreSQL学徒;如有侵权,请联系管理员删除。

下一篇: 罕见的索引失效
请登录后发表评论 登录
全部评论

注册时间:2020-07-14

  • 博文量
    306
  • 访问量
    139319