二月 19, 2025
摘要:在本教程中,您将学习如何在 PostgreSQL 中处理 “found xmin … from before relfrozenxid …” 的错误。
目录
什么是 xmin 和 relfrozenxid?
xmin 和 xmax
PostgreSQL 表中的每个条目(“元组”)都有系统列 xmin 和 xmax。它们包含了创建元组所表示的行版本和使该行版本失效(更新或删除)的事务的事务 ID。每个 SQL 语句都有一个快照,用于确定它可以看到哪些事务 ID。如果一个语句可以看到元组的xmin
,并且xmax
无效(值为 0)、不可见或属于尚未提交的事务,则该版本的行对语句可见。
事务 ID 回卷和冻结
PostgreSQL 多版本控制架构的一个问题是,事务 ID 是从一个 4 字节的无符号整型计数器生成的。一旦计数器达到其最大值,它将 “回卷” 为 3 (从 0 到 2 的值具有特殊含义)。因此,事务 ID 会随着时间的推移而改变其含义:一个属于已提交事务的事务 ID,现在可能属于一个未来的事务或已回滚的事务。这意味着可见的行可能会突然变得不可见,从而导致数据损坏。如果您想了解这一点,您应该阅读有关事务 ID 回卷的教程。
为了防止这种数据损坏的发生,VACUUM
(通常由 autovacuum 触发)会冻结旧的可见表行:它在行上设置一个标志,指示读取方应忽略xmin
和xmax
。该标志将行标记为无条件可见。一旦 PostgreSQL 冻结了数据库中的所有旧的可见行,事务 ID 就可以安全地回卷。
relfrozenxid 的含义
冻结旧行对于 PostgreSQL 数据库的运行状况至关重要。因此,系统会跟踪冻结的进度。系统表 pg_class 中有一列 “relfrozenxid
”。这样保证了xmin
或xmax
等于或早于relfrozenxid
的所有表条目都将被冻结。PostgreSQL 使用该列来触发 “反回卷” 的 autovacuum 运行:如果一个表的relfrozenxid
过去的事务数超过了 autovacuum_freeze_max_age,则 autovacuum 启动进程会启动一个反回卷的 vacuum 工作进程。这样的工作进程会坚持访问表中可能包含尚未冻结的条目的所有页面。在反回卷的 autovacuum 运行完成后,PostgreSQL 就可以将表的relfrozenxid
往前推进。
早于 relfrozenxid 的 xmin 有什么问题?
从上面可以清楚地看出,任何未冻结的元组都不应包含早于表的relfrozenxid
的xmin
。如果我们找到这样一个未冻结的元组,那就是出现了数据损坏的情况。事实上,我们正在研究的错误消息的 SQLSTATE 是XX001
。所有以XX
开头的错误消息都表示数据已损坏。
请注意,该错误发生在执行VACUUM
的过程中。该错误会终止操作,因此VACUUM
无法完成对表的处理。特别是,它不会推进relfrozenxid
。如果没有人检测到并解决问题,系统最终将会接近于数据丢失。在这种情况下,PostgreSQL 将停止处理任何新事务。如果发生这种情况,您的系统将面临停机,直到有人可以使用单用户模式来解决问题。您不会希望发生这种情况,因此您应该监控 PostgreSQL 日志文件中是否存在数据损坏错误!
什么会导致 xmin 早于 relfrozenxid 的元组?
最大的问题是,为什么人们会不断收到这条错误消息?当然,所有的这些用户都有可能出现硬件问题,这是数据损坏的最常见原因。但也有可能是,PostgreSQL 中的某个地方存在尚未发现的问题。似乎在VACUUM
与其他事情同时运行时,可能会出现一个未冻结的元组,其xmin
早于relfrozenxid
。
通过修改 relfrozenxid 来触发错误
为了弄清楚在错误发生时,应该如何处理,我们可以人为地触发该错误。正如上面所述,还想不出通过正常的数据修改,来触发问题的方法。但是,如果我们愿意手动修改系统表,则很容易导致问题发生。这是一个未受支持的操作,可能会破坏您的系统,因此让我们创建一个新数据库,这样可以删除该数据库以消除数据损坏:
CREATE DATABASE scratch;
\connect scratch
You are now connected to database "scratch" as user "postgres".
CREATE TABLE boom (id integer);
INSERT INTO boom VALUES (1);
UPDATE pg_class
SET relfrozenxid = pg_current_xact_id()::xid
WHERE relname = 'boom';
SELECT * FROM boom;
id
----
1
(1 row)
VACUUM boom;
ERROR: found xmin 31676653 from before relfrozenxid 31676654
CONTEXT: while scanning block 0 offset 1 of relation "public.boom"
请注意,SELECT
语句不会触发错误消息。只有VACUUM
对数据进行足够彻底的检查,才会将该状况报告为错误。
如何修复该错误?
有几种方法可以解决这个问题:
转储和恢复表
也许修复这个问题的最简单、最不危险的方法是,使用pg_dump
导出表 。请记住 — 查询表不会触发错误。然后,您可以删除表,并恢复转储:
pg_dump -U postgres -F c -t boom -f dumpfile scratch
psql -U postgres -d scratch -c 'DROP TABLE boom'
pg_restore -U postgres -d scratch dumpfile
虽然这种方法很简单,但它也有缺点:
- 如果表很大,则导出和导入它可能需要很长时间
- 如果有引用表的外键,您也必须删除并重新创建这些外键
这种方法的一大优势是,除了简单之外,还在于导出和导入是确保您修复所有数据损坏的唯一方法。因此,您应该尽可能使用此方法。
更新 pg_class 系统表中的 relfrozenxid
另一个方法是,手动更新表在pg_class
中的条目的relfrozenxid
。
UPDATE pg_class SET relfrozenxid = '31676653'
WHERE relname = 'boom';
这种技术速度很快,但也有一个缺点:修改一个系统表不受支持而且很危险。如果为relfrozenxid
选择了一个错误的值,则最终可能会碰到更糟糕的问题,例如
ERROR: could not access status of transaction 43350785
DETAIL: Could not open file "pg_xact/0029": No such file or directory.
对损坏的表条目使用 pg_surgery
也许处理该错误最优雅的方法是,使用 pg_surgery 扩展。使用该扩展,一旦我们知道元组的物理地址(ctid
),我们就可以显式地冻结元组:
CREATE EXTENSION pg_surgery;
-- search only in block 0
SELECT ctid FROM boom
WHERE ctid > '(0,0)'
AND ctid < '(0,32000)'
AND xmin = '31676653';
ctid
-------
(0,1)
(1 row)
SELECT heap_force_freeze('boom', '{(0,1)}');
heap_force_freeze
-------------------
(1 row)
使用 pg_surgery 也不是没有风险:它允许您冻结或删掉任意表条目,这可能会导致数据不一致。扩展的名称应该也为您提供了一个提示:除非您知道自己在做什么,否则不要使用手术刀!
更新损坏的行
如果您对损坏的行执行UPDATE
,PostgreSQL 将创建一个新的正确的行版本。
UPDATE boom SET id = id WHERE id = 1;
之后,可以对表进行 vacuum 操作而不会出现错误,这将删除损坏的数据。注意:会修改表的很大一部分的批量更新,将导致大量的数据膨胀。所以只应处理损坏的行。
结论
经常有用户报告错误 “found xmin … from before relfrozenxid …”,也许在 PostgreSQL 中可能存在实时数据损坏的错误。我们已经了解了该错误的含义,并且向您展示了三种处理该问题的方法。这些方法都不是没有缺点的,因此请谨慎选择您的方法。