dbaplus社群 03月10日
一个20亿次的旧事务把我的数据库击溃了?!
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本文深入探讨了PostgreSQL事务ID(XID)环绕可能导致数据库崩溃的问题。由于PostgreSQL使用32位无符号整数作为XID,当XID耗尽并循环时,会导致旧事务的元组突然不可见,即“猝死”。为避免这种情况,PostgreSQL将XID范围分为两半,并通过清理(Vacuuming)和冻结(freezing)机制来处理旧元组。清理进程负责将足够旧的元组的xmin标记为“冻结”,使其始终可见。然而,如果自动清理配置不合理或数据库负载过高,清理进程可能无法及时冻结所有元组,最终导致数据库进入只读模式甚至崩溃。因此,理解PostgreSQL的清理机制和自动清理配置至关重要。

🔢**XID限制与猝死风险:**PostgreSQL的事务ID(XID)是32位无符号整数,存在耗尽并循环的风险。当XID循环后,新事务分配到更小的XID,会导致xmin较大的元组突然不可见,引发数据“猝死”。

🔄**MVCC与元组可见性:**PostgreSQL采用多版本并发控制(MVCC)模型,为同一逻辑行维护多个版本(元组)。每个元组头部包含xmin和xmax字段,分别表示创建和更新/删除该元组的事务XID。PostgreSQL结合xmin、xmax等信息判断元组的可见性。

🧹**清理与冻结机制:**为防止XID环绕导致的数据不一致,PostgreSQL通过清理(Vacuuming)和冻结(freezing)机制来处理旧元组。清理进程将足够旧的元组的xmin标记为特殊值,标识为“冻结”,使其始终可见。

⚙️**自动清理配置的重要性:**虽然PostgreSQL有自动清理守护进程,但仍需合理配置自动清理阈值。若数据库负载高,需冻结的元组数量庞大,清理进程可能无法及时冻结所有元组,最终导致数据库崩溃。

原创 Rishabh Agarwal 2025-03-10 07:15 广东

Postgres的「事务ID环绕」是罪魁祸首?!




一个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 <table_name>;


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


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³²。


来源:Wikibooks


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


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


if my_xid < 2³¹:  return tuples with (xmin < my_xid or xmin > my_xid + 2³¹)else:  return tuples with (xmin < my_xid and 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亿次旧交易真的会让你的数据库宕机!



作者丨Rishabh Agarwal   编译丨Rio

来源丨网址:https://medium.com/javarevisited/how-a-2-billion-old-transaction-can-bring-your-database-down-ddf4db0ceaa1

dbaplus社群欢迎广大技术人员投稿,投稿邮箱:editor@dbaplus.cn


阅读原文

跳转微信打开

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

PostgreSQL 事务ID环绕 MVCC 清理 冻结
相关文章