又一起data corruption,借此案例梳理下分析及处理过程。
相信这个报错大家肯定见过,查询时报错clog不存在。
postgres=# explain analyze select * from user_info where id < 51219961;
ERROR: could not access status of transaction 127045588
DETAIL: Could not open file "pg_xact/0079": No such file or directory.
postgres=# \errverbose
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;
给报错函数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)。

进入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来判断可见性。
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的清理是在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、磁盘故障等)。对于处理方案请仔细斟酌,慎重选择。
本文分享自 PostgreSQL运维之道 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!