最近在基于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 FULL、CLUSTER、TRUNCATE 或 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_pages 选项:该选项可以避免在发现损坏的分页时报错,从而防止进程崩溃,但错误仍然存在,并且某些数据可能会损坏。
-- SET zero_damaged_pages on;
vacuum full <table_name>
VACUUM 机制会重建表,从而清除损坏的数据。这会占用现有表空间,需要创建一个新表,因此您需要预留 2 倍的可用空间来进行此处理。将 zero_damaged_pages 设置为 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 重放期间被初始化)。未初始化的页面都是连续的,那就更容易让人相信。
主库解决后,再重建所有备库。