data corruption分析与处理


问题现象

相信这个报错大家肯定见过,查询时报错clog不存在。

postgres=
ERROR:  could not access status of transaction 127045588
DETAIL:  Could not open file "pg_xact/0079": No such file or directory.
postgres=
ERROR:  58P01: could not access status of transaction 127045588
DETAIL:  Could not open file "pg_xact/0079": No such file or directory.
LOCATION:  SlruReportIOError, slru.c:938
postgres=

先说说危害:首先报错表肯定只能操作一部分数据;另外会影响到autovacuum,导致实例事务id回收慢。

保守或者稳妥的处理方案就是用备份恢复,如果可以接受放弃一部分数据那也可以手动补齐clog解决报错。

问题分析

在处理这种问题时,我们会去找损坏的起始位置。可以用一些函数,或者数据量少时可以手动折半查找去定位。 但是耗时会比较久。

DO $$
DECLARE
  rec record;
BEGIN
FOR rec in SELECT ctid,* FROM tablename LOOP
  raise notice 'Parameter is: %', rec.ctid;
  raise notice 'Parameter is: %', rec;
END LOOP; 
END;
$$
LANGUAGE plpgsql;
  • 怎么快速定位到报错的ctid呢?

给报错函数SlruReportIOError设置断点, 进入HeapTupleSatisfiesVisibility函数,tup->t_self = {ip_blkid = {bi_hi = 48, bi_lo = 53101}, ip_posid = 2}

那么48 << 16 | 53101 为3198829:ip_blkid是BlockId,由高16位和低16位组成,因此这里将48左移16位再或运算53101,就能得出当前页面的blockid。ip_posid是在当前页面中的offset,所以当前tuple的ctid为(3198829,2)。


  • ctid=(3198829,2)这条元组是否可见?和事务id 127045588的关系?

进入 HeapTupleSatisfiesMVCC函数:

HeapTupleHeaderXminCommitted(tuple)返回false,即tuple->t_infomask & HEAP_XMIN_COMMITTED为0,也就是 状态不是committed

tuple->t_infomask & HEAP_MOVED_OFF为0,tuple->t_infomask & HEAP_MOVED_IN为0,说明这条不 在vacuum full操作中

TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetRawXmin(tuple)))为false,说明tuple ->t_choice.t_heap.t_xmin即127045588 不是当前事务

XidInMVCCSnapshot(HeapTupleHeaderGetRawXmin(tuple), snapshot)为false, 说明127045588不在活跃snapshot中,可以看到snapshot的xmin = 984291081, xmax = 984291081,xip = 0x0。

那么可以看到判断一个元组是否可见,是先通过t_infomask标志位进行的,若元组对应infomask无committed标志,并且未进行vacuum full,也非当前事务,不在活跃事务快照中。 说明当前tuple对应的xmin是一个过去的事务,且可见性未知,那么就需要查看clog来判断可见性。


  • 事务id 127045588和pg_xact/0079的关系?

TransactionIdDidCommit函数中:

TransactionLogFetch(transactionId)通过transactionId来匹配clog文件并读取内容。

经过计算127045588对应的clog为0079(127045588/8192/4/32转16进制),clog文件大小为256K,也就是32个block(8192K)构成一个文件。那么clog文件名为:xid/(BLCKSZ * CLOG_XACTS_PER_BYTE)/32 = xid/(8192 * 4)/32,这个文件确实当前不存在。


  • clog保留策略是怎样的?

clog的清理是在vacuum/autovacuum的时候进行的,大体的过程如下:

┌─────────────────┐
│   do_autovacuum  │
│      /         │
│    vacuum       │
└─────────┬───────┘
          │
          ▼
┌─────────────────┐
│ vac_update_     │
│ datfrozenxid    │
└─────────┬───────┘
          │
          ▼
┌─────────────────┐
│ vac_truncate_   │
│ clog            │
└─────────┬───────┘
          │
          ▼
┌─────────────────┐
│ TruncateCLOG    │
└─────────┬───────┘
          │
          ▼
┌─────────────────┐
│ SimpleLruTruncate│
└─────────┬───────┘
          │
          ▼
┌─────────────────┐
│ SlruScanDirectory│
└─────────┬───────┘
          │
          ▼
┌─────────────────┐    ┌─────────────────────────┐
│ (callback)      │───▶│ SlruScanDirCbDeleteCutoff│
│ SlruScanDirCb   │    └─────────┬───────────────┘
│ DeleteCutoff    │              │
└─────────────────┘              ▼
                           ┌─────────────────────────┐
                           │ SlruInternalDeleteSegment│
                           └─────────┬───────────────┘
                                     │
                                     ▼
                           ┌─────────────────┐
                           │    unlink       │
                           │ (删除文件)       │
                           └─────────────────┘

TruncateCLOG(frozenXID, oldestxid_datoid)这个函数可以看做vacuum清理clog的入口,会根据frozenXID和oldestxid_datoid 即目前实例所有库中最老的xid(datfrozenxid)来进行清理,计算出该xid所在的clog,将之前的clog使用unlink删除。

当前实例frozenXID = 335599317

postgres=# SELECT min(datfrozenxid::int) FROM pg_database;
    min   
 -----------
 335599317
(1 row)
postgres=#

经过计算开始保留的clog文件为0140,所以在这之前的文件都会被清理。

(gdb) p/x 335599317 / 8192 / 4 / 32
$14 = 0x140
(gdb)
  • 为什么会出现这样的场景?

可能BUG导致(老版本存在类似的BUG,可以bug-list中检索具体查看),可能硬件故障或实例异常崩溃crash导致。

处理方案

如果有备份,建议用备份恢复。 如果能接受部分数据丢失,则可以手动补齐缺失的clog,然后vacuum full table。

--生产环境请慎重操作,注意路径
dd if=/dev/zero of=$PGDATA/pg_xact/0079 bs=256k count=1
vacuum full user_info;



小结

本篇记录了一起PostgreSQL数据损坏案例,现象是查询时报错“clog不存在”,导致部分数据无法访问并影响autovacuum。通过分析报错信息,定位到损坏的数据行(ctid),并解释了元组可见性判断逻辑,事务ID与clog文件的对应关系、clog的清理机制,以及可能的原因(如BUG、磁盘故障等)。对于处理方案请仔细斟酌,慎重选择。


往期链接:

错误结果?警惕mysql_fdw数据黑洞

IndexScan比SeqScan返回的结果更少,索引损坏?

2025年终总结

执行计划跑偏,你了解背后的根因吗?

Toast是insert性能杀手?

杭州PCC2025有感

一条SQL引发的惨案,内存耗尽?

openHalo兼容mysql原理和实践

为IvorySQL增添PACKAGE语法帮助

AI4DB试玩-Bao/Balsa适配PG18

OpenTenBase V5尝鲜记:让数据库"学会"思考

让backtrace打印更便捷--从backtrace_functions到backtrace_errors

postgres_fdw优化案例--GUC控制join pushdown

50G的数据,1TB的日志,凶手居然是...

从disable_cost到disabled_nodes,最小代价预估质的飞跃

深入理解max_locks_per_transaction

PITR玩砸了,看DBA如何删库跑路

提升vacuum执行时长观测能力





请使用浏览器的分享功能分享到微信等