我已经看到标题中报告的错误多次了。如果你不熟悉 PostgreSQL 的内部结构,这条消息会令你困惑: xminrelfrozenxid 是什么意思?为什么一个在另一个之前会成问题?因此,我认为写一篇文章关于这个问题以及错误的意义是值得的。我还会向你展示如何应对这种情况并修复错误。

什么是 xminrelfrozenxid

xminxmax

每个 PostgreSQL 表记录(“tuple”)都有系统列 xminxmax 。它们包含创建和无效(更新或删除)元组所表示的行版本事务的事务 ID。每个 SQL 语句都有一个快照,它决定了可以查看哪些事务 ID。如果一个语句可以看到tuple的 xmin ,并且 xmax 无效(值为 0),不可见或属于尚未提交的事务,那么该版本的行对该语句是可见的。(有关 xmax 的更多详细信息,请参阅此文章。)

事务 ID 回环和冻结

PostgreSQL 的多版本架构问题之一是事务 ID 由一个 4 字节无符号整数计数器生成。一旦计数器达到最大值,它将“回绕”到 3(0 到 2 的值有特殊含义)。因此,随着时间的推移,事务 ID 的含义会发生变化:原本属于已提交事务的事务 ID 现在可能属于未来的事务或已回滚的事务。这意味着可见的行可能会突然变得不可见,从而导致数据损坏。如果您想了解这一点是如何发生的,请阅读我关于事务 ID 回绕的文章

为防止此类数据损坏发生, VACUUM (通常由 autovacuum 触发)冻结旧的可见表记录:它在行上设置一个标志,指示读取器应忽略 xminxmax 。该标志将行标记为无条件可见。一旦 PostgreSQL 已冻结数据库中所有旧的可视行,事务 ID 就可以安全地回绕。

relfrozenxid 的含义

冻结旧行对于 PostgreSQL 数据库的健康至关重要。因此,系统会跟踪冻结的进度。系统表 pg_class 有一个列“ relfrozenxid ”。所有具有 xminxmax 等于或早于 relfrozenxid 的表记录都保证被冻结。PostgreSQL 使用该列来触发“反回绕”的autovacuum运行:如果表的 relfrozenxid 在过去超过 autovacuum_freeze_max_age 事务,autovacuum将启动一个反回绕的vacuum进程进行清理工作。这样的工作进程坚持访问可能包含尚未冻结记录的所有表页。在反回绕autovacuum运行完成后,PostgreSQL 可以推进表的 relfrozenxid

xminrelfrozenxid 更老,有什么问题?

从上述内容可以清楚地看出,任何未冻结的tuple都不应包含比表的 relfrozenxid 更旧的 xmin 。如果我们发现这样的未冻结tuple,那就是数据损坏的情况。实际上,我们正在研究错误信息的 SQLSTATE 是 XX001 。所有以 XX 开头的错误信息都表明数据损坏。

请注意,错误发生在执行 VACUUM 期间。错误终止了操作,因此 VACUUM 无法完成对表的处理。特别是,它不会推进表的 relfrozenxid 。如果没有人检测并修复这个问题,系统最终会接近数据丢失。在这种情况下,PostgreSQL 将停止处理任何新的事务。如果发生这种情况,您的系统将面临停机,直到有人能够使用单用户模式修复问题。您不希望发生这种情况,因此应监控 PostgreSQL 日志文件以查找数据损坏错误!

什么会导致tuple中的 xminrelfrozenxid 旧?

主要问题是为什么人们总是收到这个错误信息。当然,所有这些人都有硬件问题是有可能的,这是数据损坏最常见的原因。但我认为更有可能的是,PostgreSQL 中某个地方存在一个尚未发现的 bug。似乎如果 VACUUM 与其他某个进程同时运行,结果可能是一个比 relfrozenxid 更旧的 xmin 未冻结的tuple。

在这个阶段,我想向您寻求帮助。如果您能找到重现错误的方法,请报告您的发现。另外,如果您能想象出可能导致此类数据损坏的并发操作情况,那将很有帮助。这将有助于改进 PostgreSQL。一份好的错误报告是对项目的宝贵贡献!

如何通过修改 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_classrelfrozenxid 条目。

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,这将删除损坏的数据。注意:大批量的更新会导致大量膨胀。因此,只处理损坏的行。

结论

人们经常报告错误“从 relfrozenxid 之前找到 xmin...”,这让我认为 PostgreSQL 可能存在一个数据损坏的 bug。我们已经看到了这个错误的含义,并且我已经向你展示了三种处理问题的方法。这些方法都不是没有缺点,所以请仔细选择你的方法。

翻译自:
https://www.cybertec-postgresql.com/en/postgresql-found-xmin-from-before-relfrozenxid/

❤️ 转载文章请注明出处,谢谢!❤️