一个20亿次的旧事务把我的数据库击溃了?

B站影视 日本电影 2025-03-10 10:31 1

摘要:要理解这种现象的成因,我们需要深入PostgreSQL事务的内部机制。了解这些机制后,我们才能讨论问题本身以及如何避免它。

一个20亿次的旧事务把我的数据库击溃了?!

要理解这种现象的成因,我们需要深入PostgreSQL事务的内部机制。了解这些机制后,我们才能讨论问题本身以及如何避免它。

一、元组、MVCC与事务ID

如果你之前接触过关系型数据库,应该已经知道:事务(Transaction)是一组数据库操作的逻辑单元(Unit of Work, UoW),这些操作要么全部提交,要么全部回滚。

用户对数据库的任何操作(显式或隐式)都发生在一个事务中。

为了实现隔离性(ACID中的I),PostgreSQL采用了**多版本并发控制(Multi-Version Concurrency Control, MVCC)**模型。顾名思义,该模型通过维护数据对象的多个版本来确保事务间的数据隔离性。

MVCC的原理同样适用于数据库表的行。PostgreSQL会为同一逻辑行维护多个版本,这些不同版本在PostgreSQL术语中被称为「元组(tuples)」

元组是行的某个具体状态;每次更新行都会为同一逻辑行生成一个新元组。

作为软件工程师,我们通常不会注意到这一点。但现在既然知道了元组的存在,不妨快速检查一下数据库中「存活元组」和「死亡元组」的数量。

简单来说,存活元组是当前或未来事务可能修改或访问的元组,其余均为死亡元组。

SELECT n_live_tup, n_dead_tup, relname FROM pg_stat_all_tables;

PostgreSQL为每个事务分配两个ID——虚拟事务ID(VXID)和事务ID(XID)。这些ID用于决定哪些元组对事务可见。

事务ID(XID)可以唯一标识所有PostgreSQL数据库中的事务,并用于判断元组的可见性。

每个元组的头部包含xmin和xmax字段。xmin是创建该元组的事务XID,xmax是更新或删除该元组的事务XID。如果某元组是行的最新状态,其xmax值为0,PostgreSQL会忽略它。

*可通过以下SQL查询查看元组的xmin和xmax值:

SELECT xmin, xmax, * FROM ;

注意:返回的数据仅对当前活动事务可见。

PostgreSQL结合xmin、xmax及其他信息,决定某个事务中应显示哪个版本的元组。

到目前为止都理解了吗?很好,我们继续!

二、XID的限制与「猝死」

PostgreSQL的事务ID(XID)是32位无符号整数,理论上有约40亿(2³²)个唯一XID。虽然这看起来很多,但对数据密集型应用来说,可能在几个月内就会耗尽这些XID。

XID按1递增分配。当达到2³²时,XID会从0重新开始循环!这个完整的循环周期称为一个「纪元(epoch)」

实际上,XID 0、1、2被保留用于特殊用途,因此循环实际从XID 3开始。

如前所述,PostgreSQL通过MVCC维护行的多个版本(元组),而这些元组包含xmin和xmax值。但XID循环后,新事务会分配到更小的XID,导致xmin较大的元组突然不可见!

此时,所有数据似乎都凭空消失了!这种现象被称为「猝死(sudden death)」

三、PostgreSQL如何应对「猝死」

理论上,PostgreSQL将2³²个XID分为两半,每半约20亿个。当前事务的XID(例如my_xid)到my_xid + 2³¹属于前一半,其余属于后一半。注意:加法运算基于模2³²。

分割点并非固定,而是随着当前XID动态移动。当前事务的前半部分XID属于「未来」,后半部分属于「过去」。

基于此,元组可见性逻辑如下:

if my_xid my_xid + 2³¹)else: return tuples with (xmin my_xid + 2³¹)

注意:实际实现更复杂,此处仅为简化讨论。

分割XID范围后,当前事务可用的未来XID约为20亿个。但剩下的20亿个旧XID对应的数据会如何?这些数据对当前事务不可见。当启动下一个事务时,循环边界向前移动一位,导致最旧的XID(第20亿个)从「过去」变为「未来」。

过去的XID被提升为未来可用

假设数据库中仍存在对应20亿次旧事务的元组,这些原本可见的元组会突然不可见,仿佛从未被修改过!这将导致严重的数据不一致。

为防止这种情况,PostgreSQL会检查是否存在xmin超过20亿的元组。一旦发现,数据库将立即进入只读模式,禁止写入,直到处理完这些元组。

但如何确保这些元组被及时处理?答案是「清理(Vacuuming)」

PostgreSQL通过「冻结(freezing)」机制解决这一问题,该机制是清理过程的一部分。

当某个元组被认为足够旧(无活跃事务会修改它)时,其xmin会被标记为特殊值,标识为「冻结」。冻结的元组在比较时被视为「远古数据」,始终可见。

清理可通过手动执行或自动清理守护进程(auto-vacuum)完成。无论哪种方式,必须确保20亿次以上的旧事务元组被冻结,否则可能导致灾难性后果。

你可能会问:「既然有后台进程自动处理,为何还需要担心?」

虽然后台进程能分担工作,但仍需合理配置自动清理阈值。若数据库负载高,需冻结的元组数量庞大,清理进程可能仅在阈值极高时触发。此时,进程可能无法及时冻结所有元组,最终导致数据库崩溃。

因此,深刻理解PostgreSQL的清理机制和自动清理配置至关重要。否则,20亿次旧交易真的会让你的数据库宕机!

来源:dbaplus社群

相关推荐