再唠唠子事务
1前言
这两天又碰到了一起子事务的问题,赶巧最近在油管上也看了一个类似案例——PGConf India 2023 -Rare but extremely challenging Postgres Performance Problems by Dilip Kumar (EDB),由于是印度哥们的演进,语速过快,听起来着实费力,所以就参照 PPT 进行总结分享吧。
2子事务
其实关于子事务的危害,之前已经翻译了不少篇幅
感兴趣的老铁回顾一下。让我们回到原话题:
第一个案例如上👆🏻:使用 pgbench prepare statement 的方式进行压测,右侧是压测的结果,可以看到,当创建了子事务之后 ( create subtransaction overflow ),TPS 下降了一半之多 ( 3W → 1W5 ),而左边是等待事件,可以看到有大量的 LWLock——SubtransSLRU、SubtransBuffer。之后又开启了一个空闲长事务,随着压测时间越来越久,TPS 下降到了只有 200 多了,性能暴跌!
以上是问题现象,要分析这个问题的原因,我们需要先回顾一下关于子事务 ( savepoint ) 的前置知识:
每当事务或子事务修改数据时,都会为其分配一个永久事务 ID。PostgreSQL 会在 CLOG 中跟踪这些事务,相关事务 ID 被持久化到 pg_xact 子目录中
提交子事务不需要刷新WAL
每个数据库会话只能有一个事务,但可以有多个子事务
每个 savepoint 会消耗 8KB 本地内存
每个会话最多容纳 64 个子事务 ( #define PGPROC_MAX_CACHED_SUBXIDS 64 ),超过了就会被标记为 suboverflowed,对应的等待事件是 SubtransSLRU ( 老版本是 SubtransControlLock )
/*
* Each backend advertises up to PGPROC_MAX_CACHED_SUBXIDS TransactionIds
* for non-aborted subtransactions of its current top transaction. These
* have to be treated as running XIDs by other backends.
*
* We also keep track of whether the cache overflowed (ie, the transaction has
* generated at least one subtransaction that didn't fit in the cache).
* If none of the caches have overflowed, we can assume that an XID that's not
* listed anywhere in the PGPROC array is not a running transaction. Else we
* have to look at pg_subtrans.
*/
#define PGPROC_MAX_CACHED_SUBXIDS 64 /* XXX guessed-at value */
每个 savepoint 都会消耗一个事务 ID
当我们判断可见性时,需要获取一致性快照,快照中包括 xmin/xmax/xip_list,xip_list 包括在获取快照时处于活动状态的事务和子事务列表
有关子事务与父事务的映射关系存储在 pg_subtrans 目录中,子事务的缓存默认是 32 个页面 ( NUM_SUBTRANS_BUFFERS ),无法配置,因此默认情况下总共可以缓存 65536 子事务,当分配了一个子事务之后,那么我们就需要去更新 pg_subtrans,需要获取SubTransCtl Lock,该锁全局唯一,顺序依次获取释放。
因此,当我们需要去判断可见性的时候,如果含有子事务,并且又超过了 64 个,那么这么一个被标记为 suboverflowed 的快照由于没有包括可见性判断需要的所有数据,因此就会涉及到额外 IO,需要查看 pg_subtrans 目录
pg_subtrans is used to check whether the transaction in question is still
running --- the main Xid of a transaction is recorded in ProcGlobal->xids[],
with a copy in PGPROC->xid, but since we allow arbitrary nesting of
subtransactions, we can't fit all Xids in shared memory, so we have to store
them on disk. Note, however, that for each transaction we keep a "cache" of
Xids that are known to be part of the transaction tree, so we can skip looking
at pg_subtrans unless we know the cache has been overflowed. See
storage/ipc/procarray.c for the gory details.slru.c is the supporting mechanism for both pg_xact and pg_subtrans. It
implements the LRU policy for in-memory buffer pages. The high-level routines
for pg_xact are implemented in transam.c, while the low-level functions are in
clog.c. pg_subtrans is contained completely in subtrans.c.
并且由于子事务是"嵌套"的,需要遍历来查找。
试想一下,假如这个时候又来了一个长事务会怎样?毕竟是 DBkiller
当开启了一个长事务之后,xmin → xmax 快照的跨度就会很大,就需要更多地访问子事务缓存 SLRU,也意味着势必会产生更多的 cache miss,于是恶性循环,需要更多去访问磁盘上 pg_subtrans 里的文件,竞争 LWLock。
另外我们知道子事务 savepoint 我们可以选择 rollback,也可以选择 release,这二者也有些许差异
回滚的话,可以恢复子事务的影响,并且立即在 PG_XACT 中标记为 aborted,并且从 64 个 slots 中释放;释放的话,可以减少子事务递归树的深度,但是不能释放 slot。
3小结
所以小结一下,正如之前我反复提到的子事务的危害:
子事务慎用,会急剧消耗事务 ID,进而导致事务 ID 回卷 少量的子事务不会增加显著的开销,但是不要超过 64 尽可能使用 release savepoint 及时释放子事务的深度,使子事务的数量不超过 PGPROC_MAX_CACHED_SUBXIDS,这样即使在事务中创建了 100 个子事务,也不会有性能下降 超过了 64,那么会被标记为 overflowed,那么就需要查找 pg_subtrans 目录以查找父事务和子事务的可见性关系 当同时存在长事务和子事务时,要格外小心,系统的性能很容易骤降,备库的查询会瘫痪
OK。以上是此篇分享的上半部分,下半部分留到下篇文章继续分析。
4参考
PGConf India 2023 -Rare but extremely challenging Postgres Performance Problems by Dilip Kumar (EDB)