【GaussDB】内核事务异步提交(延迟清理)相关问题记录
背景
【GaussDB】会话里出现大量idle in transaction状态的问题排查
【GaussDB】排查一起ustore表在存储过程里膨胀的问题
谁能联想到,上面这两个问题竟然存在相关性。
近期发现有好几个问题似乎都与GaussDB的异步事务提交功能相关,(注意这里的异步提交并非指的传统意义上的fsync=off或synchronous_commit=off,而是对于"只读事务",在事务结束时,将资源清理等动作做异步处理,不阻塞客户端,或者叫做延迟清理。)(该特性在GaussDB506版本引入)
但目前(20260720),华为研发并未完全确认这些问题是否都与异步事务提交功能相关。这里我仅以个人视角来进行一下分析。
GaussDB内核版本号为506.0.0SPC0500
问题现象
- pg_stat_activity.state 在pbe自动提交情况下,如果不显式执行commit,任意SQL执行后,会显示为idle in transaction状态,但xact_start为空。(稳定复现)https://www.darkathena.top/archives/gaussdb-idle-in-transaction-bug-analyze
- pg_stat_all_tables里的增删改计数器在PBE自动提交时,存储过程里带dml+commit,执行存储过程,如果不在外面再执行一次commit,计数器不变。(稳定复现,我没单独写文章)
- ustore的表,在pbe自动提交情况下,存储过程里delete+insert+commit,循环执行这个存储过程,操作大量数据且磁盘性能较差时,大概率insert完全无法复用delete释放的空间,造成表随执行次数正比例膨胀。(概率性复现)https://www.darkathena.top/archives/gaussdb-an-ustore-table-bloat-in-plsql
其实可能还有一些其他问题,但复现条件比较严苛,我暂时不好复现,因此仅作为怀疑点。
分析
由于没有GaussDB的源码,这里参考一下openGauss(虽然openGauss上没有复现相关问题),对应的pr :
https://gitcode.com/opengauss/openGauss-server/pull/8094
修改前:
if (u_sess->xact_cxt.pbe_execute_complete == true) {
finish_xact_command();
} else {
u_sess->xact_cxt.pbe_execute_complete = true;
}
修改后:
if (u_sess->xact_cxt.pbe_execute_complete == true) {
if (u_sess->attr.attr_common.enable_beta_features && GetTopTransactionIdIfAny() == InvalidTransactionId) {
u_sess->xact_cxt.commit_pending = true;
} else {
finish_xact_command();
u_sess->xact_cxt.commit_pending = false;
}
} else {
u_sess->xact_cxt.pbe_execute_complete = true;
}
这段代码是 S报文(exec_sync_message)里处理的 ,Sync(同步)报文是前端(客户端)在扩展查询协议中,用于结束一个消息序列并建立错误恢复点的核心控制消息。
也就是原本pbe下都会执行到 finish_xact_command(),但优化后,GetTopTransactionIdIfAny() == InvalidTransactionId这个场景就不执行finish_xact_command();了。
GetTopTransactionIdIfAny是当前会话里顶层事务的xid ,而InvalidTransactionId 是0。
什么时候GetTopTransactionIdIfAny会是0呢?一般是所谓的只读查询时会是0。但是,在GaussDB里,如果是存储过程里有提交,且提交后没有新的dml,这个事务结束后的顶层xid也会变成0。(做事务的和做PLSQL的研发没有统一原则?)
在pg_stat_activity.top_xid (GaussDB有这个字段,openGauss没有) 中可以实时看到顶层xid的情况。通过加上适当的pg_sleep,在存储过程执行期间,可以看到执行dml时,top_xid>0,但当存储过程执行到commit后,top_xid就变成0了,也就是最终这个存储过程执行完后top_xid变为0,但是实际上有数据修改,也就是说,这个事务并不是一个只读事务,所以GetTopTransactionIdIfAny() == InvalidTransactionId这个判断条件有问题!
所以,既然已知了问题发生点,那么这个问题到底会影响哪些东西,关键就是看没被执行的finish_xact_command()里到底处理些什么东西了。
由于同样还是没有GaussDB源码,依旧参考openGauss(再次说明,openGauss上我并未测出类似问题),
User: 查一下这个函数#sym:finish_xact_command 会做些什么事情
GitHub Copilot:
finish_xact_command是 openGauss 中的一个便捷函数,用于完成当前事务命令的提交过程。它与start_xact_command配对使用,管理事务的生命周期。主要功能:
检查事务状态:仅在
xact_started为 true 时执行操作取消语句超时:调用
disable_sig_alarm(true)取消任何活动的语句超时计时器提交事务:调用
CommitTransactionCommand()提交当前事务内存检查(调试模式):
如果启用了
MEMORY_CONTEXT_CHECKING,检查所有未在提交过程中释放的内存上下文对于线程池工作线程,还会检查会话内存上下文
内存统计(调试模式):如果启用了
SHOW_MEMORY_STATS,打印内存统计信息用于泄漏追踪重置状态:将
t_thrd.postgres_cxt.xact_started设置为 false典型调用场景:
- 事务控制语句(COMMIT、ROLLBACK)执行后
- 查询执行完成后
- 在批处理操作之间
- 在处理完客户端请求后
这个函数是 PostgreSQL 事务管理的一部分,确保事务正确提交并释放相关资源。
User: CommitTransactionCommand() 里具体做了些什么呢?
GitHub Copilot:
现在让我再查看
CommitTransaction()的实现,它是最核心的提交逻辑:
CommitTransactionCommand()是 openGauss 事务管理的核心函数之一,位于 xact.cpp 第 4234 行。它根据当前事务块状态 (TBlockState) 采取不同行为,本质上是一个状态机。核心逻辑(switch 状态机)
状态 处理 TBLOCK_STARTED(不在事务块中) 调用 CommitTransaction()提交,回到 DEFAULT 空闲状态TBLOCK_BEGIN(刚执行完 BEGIN) 将状态转为 TBLOCK_INPROGRESS(不提交,仅推进状态) TBLOCK_INPROGRESS(事务块进行中) 仅 CommandCounterIncrement()(命令计数器 +1,使当前命令的修改对后续命令可见,不提交)TBLOCK_END(执行完 COMMIT) 调用 CommitTransaction(),回到 DEFAULTTBLOCK_ABORT / TBLOCK_SUBABORT(已中止) 什么都不做,等待 ROLLBACK TBLOCK_ABORT_END(执行完 ROLLBACK) CleanupTransaction()清理后回到 DEFAULTTBLOCK_ABORT_PENDING(需要中止) AbortTransaction()→CleanupTransaction()TBLOCK_PREPARE(两阶段提交) PrepareTransaction()TBLOCK_SUBBEGIN / TBLOCK_SUBRELEASE / TBLOCK_SUBCOMMIT 管理子事务(savepoint)的提交或回滚 TBLOCK_SUBABORT_PENDING / TBLOCK_SUBABORT_END 先中止再清理子事务,递归调用自身 最关键的 TBLOCK_STARTED → CommitTransaction()
当不在显式事务块中(
TBLOCK_STARTED),会调用真正的提交函数CommitTransaction(),它做以下事情:
- 触发器处理:循环执行
AfterTriggerFireDeferred(),直到所有延迟触发器完成- 资源清理:
ResourceOwnerRelease()释放资源- 存储管理器提交:
smgrDoPendingSyncs()同步文件- 缓冲区刷写:
FlushBufferPool()/BufmgrCommit()- 记录提交:
RecordTransactionCommit()写 XLOG commit 记录- 异步通知:
AtCommit_Notify()发送 LISTEN/NOTIFY- 缓存失效:
AtEOXact_Inval()处理共享缓存失效消息- 释放锁:
LockReleaseAll()释放事务持有的锁- 统计更新:
pgstat_xact_commit()报告统计信息- 清理内存上下文、重置全局状态
总结
CommitTransactionCommand()的本质是一个调度器:
- 在隐式事务(autocommit)中:直接完成真正的提交
- 在显式事务块中:仅做命令计数器递增,不提交
- 在事务结束(COMMIT)时:完成真正的提交
- 在事务中止(ROLLBACK)时:执行清理
在你选中的第 5925 行上下文中,它在
exec_simple_query的最后阶段被调用,用于确保该查询的事务命令被正确完成。
这个问题似乎比预想的要严重,在pbe的autocommit情况下,没有"真正提交"(主要是状态更新和资源清理,暂不讨论数据可见性的问题,因为目前暂未发现数据可见性方面有上报问题),这篇文章我都不敢继续写下去了......不过这毕竟是看的openGauss源码,GaussDB里这一块会需要处理哪些东西,有没有减少或者新增,甚至执行流程可能全变了,都是不确定的。
写到这里,暂不发布本文,等一手华为的消息。
20260825更新
华为有了回复,他们内部拉了很多板块的内核研发人员一起进行了讨论分析研究,最终得出结论:
ustore表膨胀是因为统计信息上报的过程没有执行(此处统计信息不是指pg_statistic/pg_class,而是指pg_stat_all_tables),这个问题也会导致pg_stat_all_tables没被更新,这就是撞上了之前异步事务提交(延迟清理功能)发现的这个BUG。
具体就是因为这种场景下,统计信息没有上报,导致依赖于统计信息上报过程里执行的动作都没做,而新版本针对统计信息没有上报的问题做了修复,所以依赖它的问题都解决了。
但是华为研发认为异步事务提交(已改口叫延迟清理功能)的这个BUG目前只会导致这三个已知问题。然后再我再三追问下,华为研发说autovacuum和autoanalyze也会受影响,可能会影响ubtree索引大小以及表统计信息不准,进而导致SQL性能或执行计划受影响。
但是我仍然有疑问,新版本的修复逻辑还是会导致含dml+commit的存储过程走到异步事务提交(延迟清理)的逻辑,只是避免了异步任务的遗漏(是否会在极小的延迟处理期间内,其他线程获取到的不是最新的状态或统计信息?)。
异步事务提交(延迟清理)的这个BUG,从首次发现idle_in_transaction状态不对的问题开始,已经持续跟踪了3个多月,我没有GaussDB的源码,用openGauss的源码无法形成对GaussB的绝对质疑。不过和华为的沟通过程中,我发现GaussDB里上报统计信息这段逻辑和openGauss有所区别,所以华为给的最终答复应该是可信的。
我当前这个驻场项目合同也快到期了,后续无法在此客户处继续监控该问题是否存在其他影响。深思熟虑后,我还是打算把这篇文章发出来,供大家参考。

