记录一个openguass(mogdb)checkpoint不推进的问题

最近在基于opengauss的mogdb v508上遇到了一个备库 流复制(Streaming Replication)环境中,备机(standby)的 restart point 无法推进,停留在1个月前的一天,该库并未重启或断电,只是在前一晚有对库里大量表做DDL,加长字段等应用层加密更新数据,目前所有的备库一直是应用日志,而2个从库都有该问题,且重启物理备库后问题依旧存在,影响在下次重启时需要从checkpoint的位置读取归档日志,并且日志中出现如下警告:

Waring: coluld not record restart point at 6F04/98156BB8 because ther are unresolved references to invalid pages 
Context : xlog redo checkpoint: redo 6F04/98156bb8; len 120;next_csn 3551220522; rencent_global_xmin 3621846951; tli 1; fpw false; xid 362859671; oid 3791478784; multi 180; offset 357; oldest xid 13017 in DB 15935; oldest running xid 3621859671; oldest xid with epoch having undo 0; online at Wed JAN 14 16:02:04 2026  remove_seg 0/6E8799
Waring: could not fork new process for connection due to PMstate PM_RECOVERY
Log: not ready to start snapshot capturer.

这个问题通常与 WAL 日志中的无效页面引用(invalid page references) 有关。

什么是 Restart Point?

在 PostgreSQL 的 standby 节点上,restart point 类似于主库上的 checkpoint。它用于:

  • 清理不再需要的 WAL 文件;
  • 减少崩溃恢复时需重放的 WAL 量;
  • 释放旧的 WAL 段以节省磁盘空间。

如果 restart point 无法推进,会导致:

  • 备机保留大量 WAL 文件(即使主库已清理);
  • 磁盘空间可能被耗尽;
  • 恢复时间变长。

checkpoint作用

使用pg_controldata查看控制谁的,检查latest checkpoint xx location是否长时间未变。或数据库内检查

-- 在 standby 上执行
SELECT * FROM pg_stat_wal_receiver;
-- 看是否 replay_lsn 正常推进(应该接近主库的 flushed_lsn)

-- 在主库上
SELECT * FROM pg_stat_replication;

错误问题分析

比较匹配主库执行了 VACUUM FULLCLUSTERTRUNCATE 或 DDL 操作(如 DROP TABLE)的场景,

  • 这些操作会释放或重写大量页面。
  • 在 standby 回放时,如果这些操作涉及的页面尚未完全写入(或被标记为“待清理”),而后续 WAL 又引用了这些“即将失效”的页面,就会触发此警告。

当然还有断电或存储层、文件系统导致 导致主库的数据文件损坏原因。网上在postgresql 9版本较多,opengauss正式基于pg这个版本,也有说是因为full_page_write=off的原因,但也有pg在FPW开启的情况下一样在PG9遇到。这套数据库使用的enable_double_write, 禁用了full_page_writes.

分析思路

1,可以在备库配置日志级别backtrace_min_message设置到debug1, 打印堆栈。

2, 配置报错的日志位置,做xlogdump,查到对应的对象page

3, 使用pagehack离线工具分析page(mogdb官方提供toolkit中包含)

日志

GaussDB中gs_xlogdump_lsn函数测试

gauss=# SHOW data_directory;
          data_directory
-----------------------------------
 /home/gauss/Euler2.10_arm_64/data
(1 row)

gauss=# show xlog_file_path;
 xlog_file_path
----------------


gauss=# SELECT * FROM pg_ls_waldir();
           name           |   size   |      modification
--------------------------+----------+------------------------
 000000010000000800000004 | 16777216 | 2026-01-14 14:57:39+08
 000000010000000800000007 | 16777216 | 2026-01-14 21:27:16+08
 000000010000000800000008 | 16777216 | 2026-01-15 02:57:46+08
 00000001000000080000000B | 16777216 | 2026-01-15 09:23:19+08
 00000001000000080000000C | 16777216 | 2026-01-15 14:57:52+08
 000000010000000800000010 | 16777216 | 2026-01-16 02:57:59+08
 000000010000000800000011 | 16777216 | 2026-01-16 07:58:04+08
 000000010000000800000012 | 16777216 | 2026-01-16 08:58:04+08
 000000010000000800000013 | 16777216 | 2026-01-16 09:00:35+08
 000000010000000800000014 | 16777216 | 2026-01-16 14:55:53+08
 000000010000000800000015 | 16777216 | 2026-01-16 18:02:25+08
...
$ pwd
/home/gauss/Euler2.10_arm_64/data/pg_xlog

$ ls -lrt
total 507904
drwx------ 2 gauss gauss        6 Apr 29  2025 archive_status
-rw------- 1 gauss gauss 16777216 Jan 12 21:57 000000010000000800000016
-rw------- 1 gauss gauss 16777216 Jan 13 03:18 000000010000000800000017
-rw------- 1 gauss gauss 16777216 Jan 13 08:57 000000010000000800000018
-rw------- 1 gauss gauss 16777216 Jan 13 08:57 000000010000000800000019
-rw------- 1 gauss gauss 16777216 Jan 13 09:57 00000001000000080000001A
-rw------- 1 gauss gauss 16777216 Jan 13 14:59 00000001000000080000001B
-rw------- 1 gauss gauss 16777216 Jan 13 20:57 00000001000000080000001C
-rw------- 1 gauss gauss 16777216 Jan 13 20:57 00000001000000080000001D
...

gauss=# select pg_current_xlog_location();
 pg_current_xlog_location
--------------------------
 8/15BA45F0
(1 row)

gauss=# create table test(id int);
CREATE TABLE
gauss=# insert into test values(1);
INSERT 0 1
gauss=# update test set id=2;
UPDATE 1
gauss=# delete test where id=1;
DELETE 0
gauss=# vacuum;
VACUUM

gauss=# SELECT pg_walfile_name('8/15BA45F0');
ERROR:  Function pg_walfile_name(unknown) does not exist.

gauss=# \df *file_name
                                                  List of functions
   Schema   |       Name       | Result data type | Argument data types |  Type  | fencedmode | propackage | prokind
------------+------------------+------------------+---------------------+--------+------------+------------+---------
 pg_catalog | pg_xlogfile_name | text             | text                | normal | f          | f          | f
(1 row)

gauss=# select pg_xlogfile_name('8/15BA45F0');
     pg_xlogfile_name
--------------------------
 000000010000000800000015

gauss=# \df gs_xlogdump_lsn
                                                                   List of functions
   Schema   |      Name       | Result data type |                  Argument data types                   |  Type  | fencedmode | propackage | prokind
------------+-----------------+------------------+--------------------------------------------------------+--------+------------+------------+---------
 pg_catalog | gs_xlogdump_lsn | text             | start_lsn text, end_lsn text, OUT output_filepath text | normal | f          | f          | f
(1 row)

gauss=# select gs_xlogdump_lsn('8/15BA45F0','8/15C237D0');
                     gs_xlogdump_lsn
----------------------------------------------------------
 /home/gauss/Euler2.10_arm_64/data/15ba45f0_15c237d0.xlog
(1 row)

Note: 文件内容和xxx_xlogdump差不多。里面主要的数据是对象id, op type.

确认对象

是xlogdump日志中的”rel xxxx/xxxx/xxxx” 查找代码

-- https://gitcode.com/opengauss/openGauss-server/blob/master/contrib/pg_xlogdump/pg_xlogdump.cpp

   /* print block references */
    for (block_id = 0; block_id <= record->max_block_id; block_id++) {
        if (!XLogRecHasBlockRef(record, block_id))
            continue;

        XLogRecGetBlockTag(record, block_id, &rnode, &forknum, &blk);
        XLogRecGetBlockLastLsn(record, block_id, &lsn);

        uint8 seg_fileno;
        BlockNumber seg_blockno;
        XLogRecGetPhysicalBlock(record, block_id, &seg_fileno, &seg_blockno);
        
        // output format: ", blkref #%u: rel %u/%u/%u/%d storage %s fork %s blk %u (phy loc %u/%u) lastlsn %X/%X"
        printf(", blkref #%d: rel %u/%u/%u/%d/%d, forknum:%d", block_id, rnode.spcNode, rnode.dbNode, rnode.relNode,
               rnode.bucketNode, rnode.opt, forknum);

“rel xxxx/xxxx/xxxx” 对应的是tablespace oid/ database oid/ relfilenode ,根据这些信息可以去数据库中查找

SELECT pg_class.relfilenode, pg_namespace.nspname as schema_name, pg_class.relname, pg_class.relkind 
from pg_class
JOIN pg_namespace on pg_class.relnamespace = pg_namespace.oid
ORDER BY pg_class.relfilenode ASC;

操作类型desc

上面在gaussdb 做xlogdump 对应的操作类型比较多,对应的是desc:

$ cat   /home/gauss/Euler2.10_arm_64/data/15ba45f0_15c237d0.xlog|grep desc|awk '{print $1 $2 $3 $4 $5}'|sort |uniq -c
    274 desc:Btree-insertleaf:
      1 desc:Btree-insertupper:
      1 desc:Btree-reuse_page:rel
      1 desc:Btree-splitright:
    147 desc:Btree-vacuum:lastBlockVacuumed
    114 desc:Heap2-clean:remxid
     38 desc:Heap2-cleanupinfo:
   3218 desc:Heap2-visible:cutoff
    210 desc:Heap-delete:off
     87 desc:Heap-inplace:off
      4 desc:Heap-newpage
      2 desc:Heap-XLOG_HEAP_HOT_UPDATEhot_update:
    236 desc:Heap-XLOG_HEAP_INSERTinsert:
      1 desc:Standby-AccessExclusivelocks:
      7 desc:Standby-XLOG_RUNNING_XACTS
      7 desc:Standby-XLOG_STANDBY_CSN
     13 desc:Standby-XLOG_STANDBY_CSN_COMMITTING,xid
      1 desc:Storage-filecreate:
      2 desc:Storage-filetruncate:
      1 desc:Transaction-XLOG_XACT_COMMITcommit:
     12 desc:Transaction-XLOG_XACT_COMMIT_COMPACTcommit:
      1 desc:UHeap2-WAL_UHEAP2_UPDATE:TupInfo:
      1 desc:UHeap-WAL_UHEAP_INSERT(init
      2 desc:UndoLog-DISCARD_UNDO_LOG_LSN:zone
      7 desc:XLOG-checkpoint:redo

而这个案例中报错部分似乎只有desc:Heap2-visible:cutoff 和desc:Heap2-clean

XLOG_HEAP2_CLEAN 执行vacuum会产生此wal记录类型,一个page被清理后,会对这个page的item进行更新

XLOG_HEAP2_VISIBLE 更改某个数据page在VM文件中的映射位时,产生这个wal记录。

备库开启高的日志级别

配置日志级别backtrace_min_message设置到debug1

尝试解决方法

1, 确认硬件无故障

2,修复主库

如果是索引index采用重建的方法

REINDEX INDEX <index_name>;

对于表table,似乎可以使用 Vacuum 命令。此外,还可以使用 zero_damaged_pa​​ges 选项:该选项可以避免在发现损坏的分页时报错,从而防止进程崩溃,但错误仍然存​​在,并且某些数据可能会损坏。

-- SET zero_damaged_pages on;
vacuum full <table_name>

VACUUM 机制会重建表,从而清除损坏的数据。这会占用现有表空间,需要创建一个新表,因此您需要预留 2 倍的可用空间来进行此处理。将 zero_damaged_pa​​ges 设置为 on 实际上是告诉 PostgreSQL 在缓冲区级别将错误清零并继续执行。这样做会导致无效页面上的数据丢失。

我们在做vacuum时日志有提示

WARNING:  relation "my_table" page 3307387 is uninitialized --- fixing
WARNING:  relation "my_table" page 3307388 is uninitialized --- fixing
...
WARNING:  relation "my_table" page 3307401 is uninitialized --- fixing
WARNING:  invalid page in block 6 of relation base/16385/11936; zeroing out page
WARNING:  invalid page in block 4 of relation base/16385/20087; zeroing out page

这意味一些数据文件page已扩展了table, 但是实际上没有与table关联和提交。这些页面可能是分配给新元组的,但崩溃发生在插入事务提交之前(实际上,在任何相关的 WAL 条目写入磁盘之前——否则,这些空页面会在 WAL 重放期间被初始化)。未初始化的页面都是连续的,那就更容易让人相信。

主库解决后,再重建所有备库。