问题现象
相信这个报错大家肯定见过,查询时报错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、磁盘故障等)。对于处理方案请仔细斟酌,慎重选择。
往期链接:
IndexScan比SeqScan返回的结果更少,索引损坏?
让backtrace打印更便捷--从backtrace_functions到backtrace_errors
postgres_fdw优化案例--GUC控制join pushdown
从disable_cost到disabled_nodes,最小代价预估质的飞跃