Postgresql源码(153)Redo系列XLOG (RM_XLOG_ID = 0)
1 总结
RM_XLOG_ID处理的是PG元信息类 WAL 记录,包括:
- 检查点 (Checkpoint): 数据库一致性状态的快照标记
- OID 计数器推进 (NextOid): 对象标识符分配的持久化
- WAL 段切换 (Switch): 强制切换到新的 WAL 文件
- 参数变更 (ParameterChange): Hot Standby 关键参数的同步
- 全页镜像 (FPI): 独立的全页写记录
- 恢复点 (RestorePoint): PITR 命名恢复点
- 恢复结束标记 (EndOfRecovery): 备库提升为主库的时间线分界点
- 其他辅助类型
StartupProcessMain()
-> StartupXLOG()
-> PerformWalRecovery()
-> ApplyWalRecord()
-> xlog_redo() <-- 我们分析的函数
2 XLOG_* 类型定义
以下宏定义位于 src/include/catalog/pg_control.h 第 66-80 行:
// 文件: src/include/catalog/pg_control.h:66-80
#define XLOG_CHECKPOINT_SHUTDOWN 0x00 // 关闭检查点 — 主库正常关闭时写入
#define XLOG_CHECKPOINT_ONLINE 0x10 // 在线检查点 — CHECKPOINT 命令或 checkpointer 自动写入
#define XLOG_NOOP 0x20 // 空操作 — 预留类型,几乎从不生成
#define XLOG_NEXTOID 0x30 // OID 计数器推进 — OID 预分配池耗尽时写入
#define XLOG_SWITCH 0x40 // WAL 段切换 — pg_switch_wal() 触发
#define XLOG_BACKUP_END 0x50 // 备份结束 — pg_backup_stop() 写入 (PG15+)
#define XLOG_PARAMETER_CHANGE 0x60 // 参数变更 — 修改 Hot Standby 关键参数后重启
#define XLOG_RESTORE_POINT 0x70 // 恢复点 — pg_create_restore_point() 写入
#define XLOG_FPW_CHANGE 0x80 // full_page_writes 变更 — 动态修改 FPW 参数
#define XLOG_END_OF_RECOVERY 0x90 // 恢复结束 — 备库提升为主库时写入
#define XLOG_FPI_FOR_HINT 0xA0 // hint bit 全页镜像 — checksums/wal_log_hints 开启时
#define XLOG_FPI 0xB0 // 全页镜像 — checkpoint 后首次修改页面
/* 0xC0 在 PostgreSQL 9.5-11 中使用过 */
#define XLOG_OVERWRITE_CONTRECORD 0xD0 // 覆盖跨页续记录 — 时间线切换时的特殊处理
info 值的编码规则:
WAL 记录的 xl_info 字段由资源管理器定义的高 4 位和通用标志位组成。xlog_redo() 函数通过 XLogRecGetInfo(record) & ~XLR_INFO_MASK 去掉通用标志位,只保留资源管理器定义的部分。因此上面的值都是 0x10 的倍数 (高 4 位有效)。
3 关键结构体定义
3.1 CheckPoint 结构体
CHECKPOINT_SHUTDOWN 和 CHECKPOINT_ONLINE 记录的 payload。
// 文件: src/include/catalog/pg_control.h:35-64
typedef struct CheckPoint
{
XLogRecPtr redo; // REDO 起始点: 开始创建此检查点时的下一个可用 RecPtr
// 恢复时从此位置开始重放
TimeLineID ThisTimeLineID; // 当前时间线 ID
TimeLineID PrevTimeLineID; // 上一个时间线 ID(如果此记录开启了新时间线则不同)
bool fullPageWrites; // 当前 full_page_writes 设置
FullTransactionId nextXid; // 下一个空闲事务 ID(64 位完整事务 ID)
Oid nextOid; // 下一个空闲 OID
MultiXactId nextMulti; // 下一个空闲 MultiXactId
MultiXactOffset nextMultiOffset; // 下一个空闲 MultiXact 偏移
TransactionId oldestXid; // 集群范围内最小的 datfrozenxid
Oid oldestXidDB; // 拥有最小 datfrozenxid 的数据库 OID
MultiXactId oldestMulti; // 集群范围内最小的 datminmxid
Oid oldestMultiDB; // 拥有最小 datminmxid 的数据库 OID
pg_time_t time; // 检查点创建的时间戳
TransactionId oldestCommitTsXid; // 最旧的有效提交时间戳对应的事务 ID
TransactionId newestCommitTsXid; // 最新的有效提交时间戳对应的事务 ID
/*
* 最旧的仍在运行的事务 ID。仅在从在线检查点初始化 Hot Standby
* 模式时需要,因此仅在 wal_level = replica 的在线检查点中计算。
* 否则设为 InvalidTransactionId。
*/
TransactionId oldestActiveXid; // 最旧的活跃事务 ID
} CheckPoint;
大小: 88 字节 (与 GDB 中 main_data_len = 88 一致)。
3.2 xl_parameter_change 结构体
PARAMETER_CHANGE 记录的 payload。
// 文件: src/include/access/xlog_internal.h:273-283
typedef struct xl_parameter_change
{
int MaxConnections; // 最大连接数
int max_worker_processes; // 最大后台工作进程数
int max_wal_senders; // 最大 WAL 发送进程数
int max_prepared_xacts; // 最大预备事务数
int max_locks_per_xact; // 每事务最大锁数
int wal_level; // WAL 级别 (0=minimal, 1=replica, 2=logical)
bool wal_log_hints; // 是否记录 hint bit 变更的 WAL
bool track_commit_timestamp; // 是否跟踪提交时间戳
} xl_parameter_change;
大小: 28 字节 (与 GDB 中 main_data_len = 28 一致)。
3.3 xl_restore_point 结构体
RESTORE_POINT 记录的 payload。
// 文件: src/include/access/xlog_internal.h:286-290
typedef struct xl_restore_point
{
TimestampTz rp_time; // 恢复点创建的时间戳
char rp_name[MAXFNAMELEN]; // 恢复点名称 (最长 64 字节)
} xl_restore_point;
大小: 72 字节 (8 字节时间戳 + 64 字节名称,与 GDB 中 main_data_len = 72 一致)。
3.4 xl_overwrite_contrecord 结构体
OVERWRITE_CONTRECORD 记录的 payload。
// 文件: src/include/access/xlog_internal.h:293-297
typedef struct xl_overwrite_contrecord
{
XLogRecPtr overwritten_lsn; // 被覆盖的原始记录的 LSN
TimestampTz overwrite_time; // 覆盖发生的时间戳
} xl_overwrite_contrecord;
3.5 xl_end_of_recovery 结构体
END_OF_RECOVERY 记录的 payload。
// 文件: src/include/access/xlog_internal.h:300-305
typedef struct xl_end_of_recovery
{
TimestampTz end_time; // 恢复结束的时间戳
TimeLineID ThisTimeLineID; // 新时间线 ID
TimeLineID PrevTimeLineID; // 分叉来源的上一个时间线 ID
} xl_end_of_recovery;
4. 完整 xlog_redo() 函数逐行注释
// 文件: src/backend/access/transam/xlog.c:7793-8141
void
xlog_redo(XLogReaderState *record)
{
uint8 info = XLogRecGetInfo(record) & ~XLR_INFO_MASK;
// 取出 WAL 记录的 info 字段,去掉 XLR_INFO_MASK 通用标志位
// info 的值对应不同的 XLOG 子类型(见第 2 节定义)
XLogRecPtr lsn = record->EndRecPtr;
// WAL 记录的结束位置(LSN),用于后续 PageSetLSN 和 minRecoveryPoint 更新
/*
* 在 XLOG 资源管理器中,备份块(Full Page Image)只在 XLOG_FPI 和
* XLOG_FPI_FOR_HINT 记录中使用。其他类型的 XLOG 记录不应该包含块引用。
*/
Assert(info == XLOG_FPI || info == XLOG_FPI_FOR_HINT ||
!XLogRecHasAnyBlockRefs(record));
/* ================================================================
* 分支 1: XLOG_NEXTOID (0x30) — OID 计数器推进
* ================================================================
* 触发条件:
* GetNewObjectId() 函数中 oidCount 减至 0 时,调用
* XLogPutNextOid(nextOid + VAR_OID_PREFETCH)
* 其中 VAR_OID_PREFETCH = 8192
*
* 典型触发场景:
* 大量 CREATE TABLE / CREATE INDEX / CREATE TYPE 等分配 OID 的 DDL
* 注意: 与序列 (SEQUENCE) 无关!序列使用自己的 WAL 记录类型
*
* payload: 仅 4 字节,一个 Oid 值
*/
if (info == XLOG_NEXTOID)
{
Oid nextOid;
/*
* 曾经尝试取 ShmemVariableCache->nextOid 和记录值的最大值,
* 但如果 OID 计数器发生回卷 (wrap around) 会导致失败。
* 由于恢复期间不会有 OID 分配操作,所以直接信任记录中的值即可。
* 仍然持有 OidGenLock 以防万一(防御性编程)。
*/
memcpy(&nextOid, XLogRecGetData(record), sizeof(Oid));
// 从 WAL 记录的 main data 区域读取 4 字节的 Oid 值
LWLockAcquire(OidGenLock, LW_EXCLUSIVE);
// 获取 OID 生成器的排他锁
ShmemVariableCache->nextOid = nextOid;
// 直接设置共享内存中的下一个可用 OID
ShmemVariableCache->oidCount = 0;
// oidCount 清零 — 下次分配时会重新触发 prefetch
LWLockRelease(OidGenLock);
}
/* ================================================================
* 分支 2: XLOG_CHECKPOINT_SHUTDOWN (0x00) — 关闭检查点
* ================================================================
* 触发条件:
* 主库正常关闭 (pg_ctl stop -m smart/fast)
* shutdown 检查点保证:写入时没有任何活跃事务
* 因此所有计数器值都是精确值,可以直接信任
*
* payload: CheckPoint 结构体 (88 字节)
*/
else if (info == XLOG_CHECKPOINT_SHUTDOWN)
{
CheckPoint checkPoint;
TimeLineID replayTLI;
memcpy(&checkPoint, XLogRecGetData(record), sizeof(CheckPoint));
// 从 WAL 记录中读取完整的 CheckPoint 结构体
/* ---- 恢复事务 ID 计数器 ---- */
/* 对于 SHUTDOWN checkpoint,直接信任 XID 计数器的值 */
LWLockAcquire(XidGenLock, LW_EXCLUSIVE);
ShmemVariableCache->nextXid = checkPoint.nextXid;
// 直接设置 nextXid(64 位完整事务 ID)
LWLockRelease(XidGenLock);
/* ---- 恢复 OID 计数器 ---- */
LWLockAcquire(OidGenLock, LW_EXCLUSIVE);
ShmemVariableCache->nextOid = checkPoint.nextOid;
ShmemVariableCache->oidCount = 0;
// 与 NEXTOID 分支相同: 直接设置值,oidCount 清零
LWLockRelease(OidGenLock);
/* ---- 恢复 MultiXact 计数器 ---- */
MultiXactSetNextMXact(checkPoint.nextMulti,
checkPoint.nextMultiOffset);
// 设置下一个 MultiXactId 和偏移(直接设置,因为是 shutdown 检查点)
MultiXactAdvanceOldest(checkPoint.oldestMulti,
checkPoint.oldestMultiDB);
// 推进最旧的 MultiXactId
/*
* 不需要在这里设置 oldestClogXid;如果它在初始化后发生了变化,
* 会在重放 xl_clog_truncate 记录时设置。
*/
/* ---- 设置事务 ID 冻结限制 ---- */
SetTransactionIdLimit(checkPoint.oldestXid, checkPoint.oldestXidDB);
// 设置 oldestXid 限制,用于防止事务 ID 回卷 (wraparound)
/* ---- 在线备份冲突检测 ---- */
/*
* 如果在等待 end-of-backup 记录时遇到了 shutdown checkpoint,
* 说明在线备份已被取消(因为主库关闭了),
* 永远不会收到 end-of-backup 记录。必须 PANIC。
*/
if (ArchiveRecoveryRequested &&
!XLogRecPtrIsInvalid(ControlFile->backupStartPoint) &&
XLogRecPtrIsInvalid(ControlFile->backupEndPoint))
ereport(PANIC,
(errmsg("online backup was canceled, recovery cannot continue")));
/* ---- Hot Standby: 构造空的运行事务快照 ---- */
/*
* ★ 核心逻辑:
* shutdown checkpoint 说明主库关闭时没有活跃事务。
* 构造一个空的(或仅包含 prepared transactions 的)
* RunningTransactions 快照来更新 ProcArray。
* 这使得备库能正确判断事务可见性。
*/
if (standbyState >= STANDBY_INITIALIZED)
{
TransactionId *xids;
int nxids;
TransactionId oldestActiveXID;
TransactionId latestCompletedXid;
RunningTransactionsData running;
oldestActiveXID = PrescanPreparedTransactions(&xids, &nxids);
// 扫描所有 prepared transactions(两阶段提交中的事务)
/* 更新 pg_subtrans 中 prepared transactions 的条目 */
StandbyRecoverPreparedTransactions();
/*
* 构造一个表示"服务器已关闭"的 RunningTransactions 快照,
* 其中只有 prepared transactions 仍然活跃。
* 由于所有子事务都与其父 prepared transaction 一起列出,
* 所以不会存在溢出的情况。
*/
running.xcnt = nxids;
// 活跃事务数(仅 prepared transactions)
running.subxcnt = 0;
// 子事务计数为 0
running.subxid_status = SUBXIDS_IN_SUBTRANS;
// 子事务状态: 都记录在 pg_subtrans 中
running.nextXid = XidFromFullTransactionId(checkPoint.nextXid);
// 下一个事务 ID
running.oldestRunningXid = oldestActiveXID;
// 最旧的运行中事务 ID
latestCompletedXid = XidFromFullTransactionId(checkPoint.nextXid);
TransactionIdRetreat(latestCompletedXid);
// latestCompletedXid = nextXid - 1 (最近完成的事务 ID)
Assert(TransactionIdIsNormal(latestCompletedXid));
running.latestCompletedXid = latestCompletedXid;
running.xids = xids;
ProcArrayApplyRecoveryInfo(&running);
// 将快照应用到 ProcArray,让备库的查询能正确判断事务可见性
}
/* ---- 更新 pg_control ---- */
/* ControlFile->checkPointCopy 始终跟踪最新检查点的 XID */
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
ControlFile->checkPointCopy.nextXid = checkPoint.nextXid;
LWLockRelease(ControlFileLock);
/* ---- 更新共享内存中的检查点 XID ---- */
SpinLockAcquire(&XLogCtl->info_lck);
XLogCtl->ckptFullXid = checkPoint.nextXid;
SpinLockRelease(&XLogCtl->info_lck);
/* ---- 时间线 ID 验证 ---- */
/*
* 在重放此记录之前,应该已经切换到了新的时间线。
* 如果时间线不匹配则 PANIC。
*/
(void) GetCurrentReplayRecPtr(&replayTLI);
if (checkPoint.ThisTimeLineID != replayTLI)
ereport(PANIC,
(errmsg("unexpected timeline ID %u (should be %u) "
"in shutdown checkpoint record",
checkPoint.ThisTimeLineID, replayTLI)));
/* ---- 设置恢复重启点 ---- */
RecoveryRestartPoint(&checkPoint, record);
// 将此检查点设为恢复重启点,使后续崩溃恢复可以从这里开始
}
/* ================================================================
* 分支 3: XLOG_CHECKPOINT_ONLINE (0x10) — 在线检查点
* ================================================================
* 触发条件:
* - SQL: CHECKPOINT;
* - checkpointer 进程定期自动执行
* - 达到 max_wal_size 等触发条件时自动执行
*
* 与 SHUTDOWN 检查点的关键区别:
* - 在线检查点期间可能有活跃事务
* - XID 计数器当作最小值处理(取较大者)
* - ★ 完全忽略 nextOid!OID 跟踪依赖 XLOG_NEXTOID 记录
* - oldestXid 只能前进不能后退
*
* payload: CheckPoint 结构体 (88 字节)
*/
else if (info == XLOG_CHECKPOINT_ONLINE)
{
CheckPoint checkPoint;
TimeLineID replayTLI;
memcpy(&checkPoint, XLogRecGetData(record), sizeof(CheckPoint));
/* ---- 恢复事务 ID 计数器(取最大值) ---- */
/* 在 ONLINE checkpoint 中,将 XID 计数器当作最小值 */
LWLockAcquire(XidGenLock, LW_EXCLUSIVE);
if (FullTransactionIdPrecedes(ShmemVariableCache->nextXid,
checkPoint.nextXid))
ShmemVariableCache->nextXid = checkPoint.nextXid;
// 只在记录值更大时才更新(因为后续 WAL 可能已推进了 XID)
LWLockRelease(XidGenLock);
/*
* ★ 关键区别: ONLINE checkpoint 中完全忽略 nextOid!
*
* 原因: 检查点开始时记录的 nextOid 可能已经过时,
* 因为后续可能有 XLOG_NEXTOID 记录更新了更大的值。
* OID 计数器可能发生回卷,所以取最大值也不安全。
*
* OID 的跟踪完全依赖 XLOG_NEXTOID 记录。
* 使用 nextOid 的代码都会自行避免分配重复 OID。
*/
/* ---- MultiXact 处理(取最大值) ---- */
MultiXactAdvanceNextMXact(checkPoint.nextMulti,
checkPoint.nextMultiOffset);
// AdvanceNextMXact: 只前进不后退
/*
* 注意: 当重放由旧版主库生成的 WAL 时,
* 这里可能会触发 multixact 截断操作。
*/
MultiXactAdvanceOldest(checkPoint.oldestMulti,
checkPoint.oldestMultiDB);
/* ---- oldestXid 只前进不后退 ---- */
if (TransactionIdPrecedes(ShmemVariableCache->oldestXid,
checkPoint.oldestXid))
SetTransactionIdLimit(checkPoint.oldestXid,
checkPoint.oldestXidDB);
/* ---- 更新 pg_control ---- */
/* ControlFile->checkPointCopy 始终跟踪最新检查点的 XID */
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
ControlFile->checkPointCopy.nextXid = checkPoint.nextXid;
LWLockRelease(ControlFileLock);
/* ---- 更新共享内存 ---- */
SpinLockAcquire(&XLogCtl->info_lck);
XLogCtl->ckptFullXid = checkPoint.nextXid;
SpinLockRelease(&XLogCtl->info_lck);
/* ---- 时间线 ID 验证 ---- */
/* 在线检查点中时间线 ID 不应该改变 */
(void) GetCurrentReplayRecPtr(&replayTLI);
if (checkPoint.ThisTimeLineID != replayTLI)
ereport(PANIC,
(errmsg("unexpected timeline ID %u (should be %u) "
"in online checkpoint record",
checkPoint.ThisTimeLineID, replayTLI)));
RecoveryRestartPoint(&checkPoint, record);
}
/* ================================================================
* 分支 4: XLOG_OVERWRITE_CONTRECORD (0xD0) — 覆盖跨页续记录
* ================================================================
* 触发条件:
* 极其罕见,仅在时间线切换时发生。
* 当一条 WAL 记录跨越两个页面(contrecord),而在续记录所在的位置
* 发生了时间线切换,新时间线需要覆盖旧时间线未完成的续记录。
*
* redo 处理: 由 xlogrecovery_redo() 处理,xlog_redo() 中什么都不做
*/
else if (info == XLOG_OVERWRITE_CONTRECORD)
{
/* nothing to do here, handled in xlogrecovery_redo() */
}
/* ================================================================
* 分支 5: XLOG_END_OF_RECOVERY (0x90) — 恢复结束标记
* ================================================================
* 触发条件:
* 备库提升为主库: pg_ctl promote 或 SELECT pg_promote();
* 此记录标记新时间线的开始点
*
* payload: xl_end_of_recovery 结构体
* 包含 end_time, ThisTimeLineID, PrevTimeLineID
*/
else if (info == XLOG_END_OF_RECOVERY)
{
xl_end_of_recovery xlrec;
TimeLineID replayTLI;
memcpy(&xlrec, XLogRecGetData(record), sizeof(xl_end_of_recovery));
/*
* 对于 Hot Standby,可以像处理 Shutdown Checkpoint 那样处理此记录,
* 但这种情况更罕见且更难测试,额外维护成本的收益不大。
*/
/* ---- 时间线 ID 验证 ---- */
/*
* 在重放此记录之前,应该已经切换到了新的时间线。
*/
(void) GetCurrentReplayRecPtr(&replayTLI);
if (xlrec.ThisTimeLineID != replayTLI)
ereport(PANIC,
(errmsg("unexpected timeline ID %u (should be %u) "
"in end-of-recovery record",
xlrec.ThisTimeLineID, replayTLI)));
}
/* ================================================================
* 分支 6: XLOG_NOOP (0x20) — 空操作
* ================================================================
* 触发条件: 几乎从不生成
* 代码中保留用于未来扩展或特殊测试
* 没有已知的正常触发路径
*/
else if (info == XLOG_NOOP)
{
/* nothing to do here */
}
/* ================================================================
* 分支 7: XLOG_SWITCH (0x40) — WAL 段切换
* ================================================================
* 触发条件:
* SQL: SELECT pg_switch_wal();
* 强制切换到新的 WAL 段文件
*
* 特殊行为:
* EndRecPtr 会跳到下一个 WAL 段的起始位置
* (例如从 0x191BB1A0 跳到 0x1A000000)
*
* payload: 无(xl_tot_len = 24,仅 WAL 记录头)
*/
else if (info == XLOG_SWITCH)
{
/* nothing to do here */
}
/* ================================================================
* 分支 8: XLOG_RESTORE_POINT (0x70) — 恢复点
* ================================================================
* 触发条件:
* SQL: SELECT pg_create_restore_point('name');
* 在 WAL 流中标记一个命名恢复点,用于 PITR(时间点恢复)
*
* payload: xl_restore_point 结构体 (72 字节: 8 字节时间戳 + 64 字节名称)
*
* 注意: 实际的恢复目标匹配逻辑在 xlogrecovery.c 中处理
* recovery_target_name = 'name' 参数控制恢复到哪个点
*/
else if (info == XLOG_RESTORE_POINT)
{
/* nothing to do here, handled in xlogrecovery.c */
}
/* ================================================================
* 分支 9: XLOG_FPI (0xB0) / XLOG_FPI_FOR_HINT (0xA0) — 全页镜像
* ================================================================
* XLOG_FPI 触发条件:
* checkpoint 后首次修改某个页面时,由 XLogSaveBufferForHint() 等
* 产生独立的 FPI 记录(当无法与其他 WAL 记录合并时)
*
* XLOG_FPI_FOR_HINT 触发条件:
* 需要 data_checksums = on 或 wal_log_hints = on
* hint bit 更新(如 visibility map 标记)需要 WAL 日志
*
* payload: 仅 block references(块引用),每个包含完整 8KB 页面镜像
* main_data_len = 0
*
* 关键说明:
* - FPI 记录不会产生恢复冲突 (recovery conflict)
* - 如果资源管理器需要产生冲突,必须定义独立的 WAL 记录类型
* - XLOG_FPI 必须包含 full-page image,否则报 ERROR
* - XLOG_FPI_FOR_HINT 在 full_page_writes=off 时可能没有 image(此时跳过)
*/
else if (info == XLOG_FPI || info == XLOG_FPI_FOR_HINT)
{
/*
* XLOG_FPI 记录只包含一个或多个块引用,每个都必须包含全页镜像。
* 即使生成记录时 full_page_writes 是关闭的也是如此 —
* 否则这条记录就没有存在的意义。
*
* XLOG_FPI_FOR_HINT 记录在 hint bit 更新时生成。
* 仅在 checksums 和/或 wal_log_hints 开启时才生成。
* 如果生成时 full_page_writes 是关闭的,可能不包含全页镜像。
* 此时什么都不需要做。
*/
for (uint8 block_id = 0; block_id <= XLogRecMaxBlockId(record); block_id++)
{
Buffer buffer;
if (!XLogRecHasBlockImage(record, block_id))
{
// 检查此 block_id 是否有全页镜像
if (info == XLOG_FPI)
elog(ERROR, "XLOG_FPI record did not contain a full-page image");
// XLOG_FPI 必须有 image,否则是数据损坏
continue;
// XLOG_FPI_FOR_HINT 在 full_page_writes=off 时可以没有 image,跳过
}
if (XLogReadBufferForRedo(record, block_id, &buffer) != BLK_RESTORED)
elog(ERROR, "unexpected XLogReadBufferForRedo result "
"when restoring backup block");
// XLogReadBufferForRedo 恢复完整页面:
// 1. 从 WAL 记录中解压 FPI 数据
// 2. 覆盖目标数据页
// 3. 设置页面 LSN
// 4. 返回 BLK_RESTORED
UnlockReleaseBuffer(buffer);
// 释放 buffer 锁和 pin
}
}
/* ================================================================
* 分支 10: XLOG_BACKUP_END (0x50) — 备份结束标记
* ================================================================
* 触发条件:
* SQL: SELECT pg_backup_stop(false); (PostgreSQL 15+)
* 旧版: SELECT pg_stop_backup();
*
* redo 处理:
* 由 xlogrecovery_redo() 处理(设置 backupEndPoint)
* xlog_redo() 中什么都不做
*
* payload: XLogRecPtr (8 字节) — 备份开始的 LSN
*/
else if (info == XLOG_BACKUP_END)
{
/* nothing to do here, handled in xlogrecovery_redo() */
}
/* ================================================================
* 分支 11: XLOG_PARAMETER_CHANGE (0x60) — 参数变更
* ================================================================
* 触发条件:
* 主库修改以下参数之一并重启:
* max_connections, max_worker_processes, max_wal_senders,
* max_prepared_xacts, max_locks_per_xact, wal_level,
* wal_log_hints, track_commit_timestamp
*
* 重启时 postmaster 在 shutdown checkpoint 之后检测到参数变化,
* 写入 PARAMETER_CHANGE 记录
*
* payload: xl_parameter_change 结构体 (28 字节)
*
* 备库行为:
* 备库收到此记录后会调用 CheckRequiredParameterValues()
* 确保备库的这些参数值 >= 主库的值
* 如果不满足则报错并停止恢复
*/
else if (info == XLOG_PARAMETER_CHANGE)
{
xl_parameter_change xlrec;
/* 更新 pg_control 中的参数副本 */
memcpy(&xlrec, XLogRecGetData(record), sizeof(xl_parameter_change));
/* ---- 逻辑复制槽失效检查 ---- */
/*
* 如果备库在 Hot Standby 模式下运行,且主库的 wal_level 降低到了
* 低于 logical 的级别,而备库本身的 wal_level >= logical,
* 则需要失效过时的逻辑复制槽。
*
* 如果备库自身的 wal_level < logical,则不需要检查,因为:
* - 要么根本不允许创建逻辑槽
* - 要么已经失效了现有的逻辑槽
*/
if (InRecovery && InHotStandby &&
xlrec.wal_level < WAL_LEVEL_LOGICAL &&
wal_level >= WAL_LEVEL_LOGICAL)
InvalidateObsoleteReplicationSlots(RS_INVAL_WAL_LEVEL,
0, InvalidOid,
InvalidTransactionId);
/* ---- 更新 pg_control 中的参数值 ---- */
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
ControlFile->MaxConnections = xlrec.MaxConnections;
ControlFile->max_worker_processes = xlrec.max_worker_processes;
ControlFile->max_wal_senders = xlrec.max_wal_senders;
ControlFile->max_prepared_xacts = xlrec.max_prepared_xacts;
ControlFile->max_locks_per_xact = xlrec.max_locks_per_xact;
ControlFile->wal_level = xlrec.wal_level;
ControlFile->wal_log_hints = xlrec.wal_log_hints;
/* ---- 更新 minRecoveryPoint ---- */
/*
* 更新 minRecoveryPoint 以确保: 如果恢复被中断,
* 必须恢复到此点之后才能允许 Hot Standby。
*
* 这在 max_* 参数被降低时非常重要 —
* 确保不会对参数变更之前的 WAL 执行查询。
*
* 在崩溃恢复过程中不能更新本地副本(需要重放所有 WAL)。
*/
if (InArchiveRecovery)
{
LocalMinRecoveryPoint = ControlFile->minRecoveryPoint;
LocalMinRecoveryPointTLI = ControlFile->minRecoveryPointTLI;
}
if (LocalMinRecoveryPoint != InvalidXLogRecPtr &&
LocalMinRecoveryPoint < lsn)
{
TimeLineID replayTLI;
(void) GetCurrentReplayRecPtr(&replayTLI);
ControlFile->minRecoveryPoint = lsn;
ControlFile->minRecoveryPointTLI = replayTLI;
// 将 minRecoveryPoint 推进到当前 LSN
}
/* ---- 处理 commit timestamp 参数变更 ---- */
CommitTsParameterChange(xlrec.track_commit_timestamp,
ControlFile->track_commit_timestamp);
ControlFile->track_commit_timestamp = xlrec.track_commit_timestamp;
UpdateControlFile();
// 将更新后的 pg_control 写入磁盘
LWLockRelease(ControlFileLock);
/* ---- 检查备库参数是否满足要求 ---- */
CheckRequiredParameterValues();
// 验证备库的 max_connections 等参数 >= 主库的值
// 如果不满足则报 FATAL 错误并停止恢复
}
/* ================================================================
* 分支 12: XLOG_FPW_CHANGE (0x80) — full_page_writes 变更
* ================================================================
* 触发条件:
* 主库上执行:
* ALTER SYSTEM SET full_page_writes = off; (或 on)
* SELECT pg_reload_conf();
*
* 注意: full_page_writes 是可以在线修改的参数(不需要重启)
*
* payload: 仅 1 字节 (bool 值)
*/
else if (info == XLOG_FPW_CHANGE)
{
bool fpw;
memcpy(&fpw, XLogRecGetData(record), sizeof(bool));
// 读取 full_page_writes 的新值
/*
* 更新最后一次 FPW 被禁用的 LSN,
* 让 do_pg_backup_start() 和 do_pg_backup_stop() 能够检查
* 在线备份期间 full_page_writes 是否被关闭过。
*
* 如果备份期间 FPW 被关闭过,备份可能不一致。
*/
if (!fpw)
{
SpinLockAcquire(&XLogCtl->info_lck);
if (XLogCtl->lastFpwDisableRecPtr < record->ReadRecPtr)
XLogCtl->lastFpwDisableRecPtr = record->ReadRecPtr;
// 记录 FPW 被关闭的位置(仅记录最新的)
SpinLockRelease(&XLogCtl->info_lck);
}
/* 跟踪 full_page_writes 的当前状态 */
lastFullPageWrites = fpw;
// 更新模块级变量,后续 WAL 回放会参考此值
}
}
5 GDB调试记录
5.1 会话一: 流复制运行中触发各分支
Hit #1: CHECKPOINT_ONLINE (info=0x10)
触发操作: 在主库执行 CHECKPOINT;
--- WAL record header ---
xl_tot_len = 114
xl_xid = 0
xl_prev = 0x175FDC20
xl_info = 0x10
xl_rmid = 0
--- WAL position ---
ReadRecPtr = 0x175FDC58
EndRecPtr = 0x175FDCD0 (= lsn)
--- decoded data ---
main_data_len = 88
max_block_id = -1
--- CHECKPOINT_ONLINE payload (CheckPoint struct) ---
redo = 0x175FDC20
ThisTimeLineID = 1
PrevTimeLineID = 1
fullPageWrites = 1
nextOid = 82197
nextMulti = 6
nextMultiOffset = 11
oldestXid = 722
oldestXidDB = 1
oldestMulti = 1
oldestMultiDB = 1
oldestActiveXid = 1455
oldestCommitTsXid = 0
newestCommitTsXid = 0
--- backtrace ---
#0 xlog_redo (record=0x2ddd0f8) at xlog.c:7797
#1 ApplyWalRecord (xlogreader=0x2ddd0f8, record=..., replayTLI=...) at xlogrecovery.c:1941
#2 PerformWalRecovery () at xlogrecovery.c:1772
#3 StartupXLOG () at xlog.c:5452
#4 StartupProcessMain () at startup.c:282
分析要点:
xl_xid = 0: 检查点不属于任何事务main_data_len = 88: 正好是sizeof(CheckPoint)max_block_id = -1: 无块引用(检查点记录不包含数据页)oldestActiveXid = 1455: 在线检查点时有活跃事务(与 SHUTDOWN 检查点的0形成对比)fullPageWrites = 1: 全页写已开启redo = 0x175FDC20在ReadRecPtr = 0x175FDC58之前: 检查点的 REDO 点是检查点开始时的位置,而非检查点记录本身的位置
Hit #2: NEXTOID (info=0x30) — 第一次 OID 预取
触发操作: 在主库执行批量建表
DO $$ BEGIN
FOR i IN 1..5000 LOOP
EXECUTE format('CREATE TABLE IF NOT EXISTS oid_test_%s (id int)', i);
END LOOP;
END; $$;
每张表分配约 5 个 OID(表本身、行类型、数组类型、TOAST 表、TOAST 索引)。当 oidCount 减至 0 时,GetNewObjectId() 调用 XLogPutNextOid(nextOid + 8192)。
--- WAL record header ---
xl_tot_len = 30
xl_xid = 1460
xl_prev = 0x181D4780
xl_info = 0x30
xl_rmid = 0
--- WAL position ---
ReadRecPtr = 0x181D47B0
EndRecPtr = 0x181D47D0 (= lsn)
--- decoded data ---
main_data_len = 4
max_block_id = -1
--- NEXTOID payload ---
nextOid = 90389
ShmemVariableCache->nextOid = 82197 (before redo)
ShmemVariableCache->oidCount = 0 (before redo)
--- backtrace ---
#0 xlog_redo (record=0x2ddd0f8) at xlog.c:7797
#1 ApplyWalRecord (xlogreader=0x2ddd0f8, ...) at xlogrecovery.c:1941
#2 PerformWalRecovery () at xlogrecovery.c:1772
#3 StartupXLOG () at xlog.c:5452
#4 StartupProcessMain () at startup.c:282
分析要点:
main_data_len = 4: 仅一个 Oid 值(4 字节)nextOid = 90389 = 82197 + 8192: 正好是前一个值加上VAR_OID_PREFETCHxl_xid = 1460: 耗尽 OID 池的事务 IDShmemVariableCache->oidCount = 0: 确认 OID 池已耗尽
Hit #3: NEXTOID (info=0x30) — 第二次 OID 预取
触发操作: 同上,继续建表消耗 OID
--- NEXTOID payload ---
nextOid = 98581
ShmemVariableCache->nextOid = 90389 (before redo)
ShmemVariableCache->oidCount = 0 (before redo)
分析要点:
nextOid = 98581 = 90389 + 8192: 再次确认VAR_OID_PREFETCH = 8192机制- OID 预取是一种 “批量分配 + 惰性日志” 的设计:
- 不是每分配一个 OID 就写一条 WAL
- 而是每消耗完 8192 个 OID 才写一条 WAL
- 这大幅减少了 WAL 流量
Hit #5: FPI (info=0xB0) — 全页镜像
触发操作: 在主库执行 CHECKPOINT; 后对某张表执行 UPDATE(首次修改该页面产生 FPI)
--- WAL record header ---
xl_tot_len = 137
xl_xid = 1465
xl_prev = 0x191BA500
xl_info = 0xB0
xl_rmid = 0
--- WAL position ---
ReadRecPtr = 0x191BA5C0
EndRecPtr = 0x191BA650 (= lsn)
--- decoded data ---
main_data_len = 0
max_block_id = 0
--- FPI payload ---
max_block_id = 0
block[0]: relNumber=90849 spc=1663 db=5 blkno=0 fork=0 has_image=1
--- backtrace ---
#0 xlog_redo (record=0x2ddd0f8) at xlog.c:7797
#1 ApplyWalRecord (...) at xlogrecovery.c:1941
...
分析要点:
main_data_len = 0: FPI 记录没有 main data,只有块引用(block references)max_block_id = 0: 只包含一个块引用 (block_id = 0)block[0]详细信息:relNumber=90849: 关系文件编号(表的 OID)spc=1663: 表空间 OID(1663 = pg_default)db=5: 数据库 OIDblkno=0: 块编号(第 0 个 8KB 页面)fork=0: fork 编号(0 = main fork)has_image=1: 包含完整的 8KB 页面镜像
xl_tot_len = 137: 远小于 8192 (8KB),说明页面镜像经过了压缩
Hit #7: RESTORE_POINT (info=0x70) — 恢复点
触发操作: 在主库执行 SELECT pg_create_restore_point('test_restore_point_1');
--- WAL record header ---
xl_tot_len = 98
xl_xid = 0
xl_prev = 0x191BB100
xl_info = 0x70
xl_rmid = 0
--- WAL position ---
ReadRecPtr = 0x191BB138
EndRecPtr = 0x191BB1A0 (= lsn)
--- decoded data ---
main_data_len = 72
max_block_id = -1
--- RESTORE_POINT payload ---
name = (xl_restore_point struct, 72 bytes: 8-byte timestamp + 64-byte name)
--- backtrace ---
#0 xlog_redo (record=0x2ddd0f8) at xlog.c:7797
...
分析要点:
xl_xid = 0: 恢复点不属于任何事务(由 superuser 直接写入 WAL)main_data_len = 72:sizeof(xl_restore_point)= 8 (TimestampTz) + 64 (MAXFNAMELEN) = 72- PITR 使用方法: 在
postgresql.conf中设置recovery_target_name = 'test_restore_point_1',恢复时会在此 LSN 处停止 xlog_redo()中什么都不做 — 实际的恢复目标匹配逻辑在xlogrecovery.c中
Hit #8: SWITCH (info=0x40) — WAL 段切换
触发操作: 在主库执行 SELECT pg_switch_wal();
--- WAL record header ---
xl_tot_len = 24
xl_xid = 0
xl_prev = 0x191BB138
xl_info = 0x40
xl_rmid = 0
--- WAL position ---
ReadRecPtr = 0x191BB1A0
EndRecPtr = 0x1A000000 (= lsn)
--- decoded data ---
main_data_len = 0
max_block_id = -1
--- SWITCH payload ---
(no data -- xlog segment switch)
分析要点:
xl_tot_len = 24: 最小的 WAL 记录大小(仅 WAL 记录头,无 payload)main_data_len = 0: 无数据内容- EndRecPtr 的跳变:
ReadRecPtr = 0x191BB1A0— 记录位于段 0x19 中EndRecPtr = 0x1A000000— 跳到了段 0x1A 的起始位置- 这是 SWITCH 记录的特殊行为: 它在逻辑上"填满"当前段的剩余空间,使下一条记录写入新段
- 这种机制用于: WAL 归档场景中尽快将当前段归档到远程存储
Hit #9: FPW_CHANGE (info=0x80) — 关闭 full_page_writes
触发操作: 在主库执行
ALTER SYSTEM SET full_page_writes = off;
SELECT pg_reload_conf();
--- WAL record header ---
xl_tot_len = 27
xl_xid = 0
xl_prev = 0x1A000028
xl_info = 0x80
xl_rmid = 0
--- WAL position ---
ReadRecPtr = 0x1A000060
EndRecPtr = 0x1A000080 (= lsn)
--- decoded data ---
main_data_len = 1
max_block_id = -1
--- FPW_CHANGE payload ---
full_page_writes = 0
分析要点:
main_data_len = 1: 仅一个 bool 值 (1 字节)full_page_writes = 0(false): FPW 被关闭xlog_redo()会记录lastFpwDisableRecPtr,供在线备份检查使用- 生产环境中不建议关闭 full_page_writes(除非使用支持原子写的存储设备,如某些 SSD)
Hit #10: FPW_CHANGE (info=0x80) — 开启 full_page_writes
触发操作: 在主库执行
ALTER SYSTEM SET full_page_writes = on;
SELECT pg_reload_conf();
--- FPW_CHANGE payload ---
full_page_writes = 1
分析要点:
full_page_writes = 1(true): FPW 被重新开启- 开启 FPW 时不需要记录
lastFpwDisableRecPtr(仅关闭时记录)
5.2 会话二: 主库参数变更后重启
操作步骤:
- 在主库执行
ALTER SYSTEM SET max_connections = 2001; - 执行
pg_ctl -D pgdata9901 restart - 备库 startup 进程重新连接并回放 WAL
Hit #1: CHECKPOINT_SHUTDOWN (info=0x00)
触发操作: pg_ctl -D pgdata9901 restart(正常关闭写入 shutdown checkpoint)
--- WAL record header ---
xl_tot_len = 114
xl_xid = 0
xl_prev = 0x1A0000D8
xl_info = 0x00
xl_rmid = 0
--- WAL position ---
ReadRecPtr = 0x1A000110
EndRecPtr = 0x1A000188 (= lsn)
--- decoded data ---
main_data_len = 88
max_block_id = -1
--- CHECKPOINT_SHUTDOWN payload (CheckPoint struct) ---
redo = 0x1A000110
ThisTimeLineID = 1
PrevTimeLineID = 1
fullPageWrites = 1
nextOid = 90850
nextMulti = 6
nextMultiOffset = 11
oldestXid = 722
oldestXidDB = 1
oldestMulti = 1
oldestMultiDB = 1
oldestActiveXid = 0
oldestCommitTsXid = 0
newestCommitTsXid = 0
--- backtrace ---
#0 xlog_redo (record=0x2ddd0f8) at xlog.c:7797
#1 ApplyWalRecord (...) at xlogrecovery.c:1941
#2 PerformWalRecovery () at xlogrecovery.c:1772
#3 StartupXLOG () at xlog.c:5452
#4 StartupProcessMain () at startup.c:282
与 CHECKPOINT_ONLINE 的关键对比:
| 字段 | CHECKPOINT_SHUTDOWN | CHECKPOINT_ONLINE |
|---|---|---|
xl_info |
0x00 |
0x10 |
oldestActiveXid |
0 (InvalidTransactionId) | 1455 (有活跃事务) |
redo 与 ReadRecPtr 关系 |
相等 (0x1A000110) |
redo < ReadRecPtr |
| XID 计数器处理 | 直接信任(精确值) | 取最大值(最小值语义) |
| nextOid 处理 | 直接设置 | 完全忽略 |
分析要点:
oldestActiveXid = 0(InvalidTransactionId): 因为 shutdown 时已无活跃事务redo = 0x1A000110 = ReadRecPtr: shutdown 检查点的 REDO 点就是自身(因为关闭时所有脏页都已刷盘,不需要从更早的位置开始重做)- Hot Standby 逻辑: 会构造一个空的(或仅含 prepared transactions 的)RunningTransactions 快照,标记所有非 prepared 事务已完成
Hit #2: PARAMETER_CHANGE (info=0x60)
触发操作: 主库重启后检测到 max_connections 发生了变化
--- WAL record header ---
xl_tot_len = 54
xl_xid = 0
xl_prev = 0x1A000110
xl_info = 0x60
xl_rmid = 0
--- WAL position ---
ReadRecPtr = 0x1A000188
EndRecPtr = 0x1A0001C0 (= lsn)
--- decoded data ---
main_data_len = 28
max_block_id = -1
--- PARAMETER_CHANGE payload ---
MaxConnections = 2001
max_worker_processes = 8
max_wal_senders = 10
max_prepared_xacts = 2000
max_locks_per_xact = 64
wal_level = 2
wal_log_hints = 0
--- backtrace ---
#0 xlog_redo (record=0x2ddd0f8) at xlog.c:7797
#1 ApplyWalRecord (...) at xlogrecovery.c:1941
...
分析要点:
MaxConnections = 2001: 从原来的 2000 修改为 2001(触发了此记录)wal_level = 2: 对应WAL_LEVEL_REPLICA(注意: 0=minimal, 1=replica, 2=logical。这里wal_level = 2表示 replica 级别,因为枚举值定义中 WAL_LEVEL_REPLICA=1 对应内部值 2)main_data_len = 28: 正好是sizeof(xl_parameter_change)xl_prev = 0x1A000110: 紧跟在 CHECKPOINT_SHUTDOWN 之后。这是因为重启时 checkpointer 先写 shutdown checkpoint,然后 postmaster 检测参数变化后写 PARAMETER_CHANGExl_xid = 0: 参数变更记录不属于任何事务- 备库处理: 收到此记录后
CheckRequiredParameterValues()会验证备库的参数设置是否 >= 主库的值。如果备库的max_connections < 2001,则会报错并停止恢复
时序图:
主库重启时序:
pg_ctl restart
-> postmaster shutdown
-> checkpointer 写 CHECKPOINT_SHUTDOWN (0x00)
-> postmaster startup
-> 检测参数变化
-> 写 PARAMETER_CHANGE (0x60)
-> 正常运行
备库回放时序:
startup process
-> 回放 CHECKPOINT_SHUTDOWN
-> 恢复所有计数器
-> 构造空的 RunningTransactions
-> 回放 PARAMETER_CHANGE
-> 更新 pg_control 中的参数
-> CheckRequiredParameterValues()
7 触发方式
| info 值 | 类型 | 触发方法 | payload 大小 | redo 行为 |
|---|---|---|---|---|
0x00 |
CHECKPOINT_SHUTDOWN | pg_ctl stop (主库正常关闭) |
CheckPoint (88B) | 恢复所有计数器(直接信任);构造空事务快照;设置重启点 |
0x10 |
CHECKPOINT_ONLINE | CHECKPOINT; 或 checkpointer 自动 |
CheckPoint (88B) | XID 取最大值;忽略 nextOid;设置重启点 |
0x20 |
NOOP | 几乎不触发 | 无 | 什么都不做 |
0x30 |
NEXTOID | 大量 CREATE TABLE/INDEX 耗尽 OID prefetch pool | Oid (4B) | 直接设置 nextOid,oidCount 清零 |
0x40 |
SWITCH | SELECT pg_switch_wal(); |
无 | 什么都不做 |
0x50 |
BACKUP_END | SELECT pg_backup_stop(false); |
XLogRecPtr (8B) | 什么都不做 (由 xlogrecovery_redo 处理) |
0x60 |
PARAMETER_CHANGE | 修改 max_connections 等参数并重启主库 | xl_parameter_change (28B) | 更新 pg_control 参数;检查备库参数兼容性 |
0x70 |
RESTORE_POINT | SELECT pg_create_restore_point('name'); |
xl_restore_point (72B) | 什么都不做 (由 xlogrecovery.c 处理) |
0x80 |
FPW_CHANGE | ALTER SYSTEM SET full_page_writes = off/on; + SELECT pg_reload_conf(); |
bool (1B) | 记录 FPW 禁用 LSN;更新跟踪变量 |
0x90 |
END_OF_RECOVERY | SELECT pg_promote(); 或 pg_ctl promote |
xl_end_of_recovery | 验证时间线 ID |
0xA0 |
FPI_FOR_HINT | 需 checksums/wal_log_hints + hint bit 更新 | block refs + FPI | 恢复全页镜像(可能无 image 则跳过) |
0xB0 |
FPI | CHECKPOINT 后首次修改页面 | block refs + FPI | 恢复全页镜像 |
0xD0 |
OVERWRITE_CONTRECORD | 时间线切换覆盖跨页记录(极罕见) | xl_overwrite_contrecord | 什么都不做 (由 xlogrecovery_redo 处理) |
8 官方文档
8.1 WAL 编码约定
src/backend/access/transam/README 第 399 行起,描述了 WAL 编码的通用约定:
每个资源管理器定义自己的 WAL 记录类型。
xl_info的高 4 位由资源管理器自定义,低 4 位是通用标志位 (XLR_INFO_MASK)。
8.2 WAL 记录格式
src/include/access/xlogrecord.h 中详细注释了 WAL 记录的物理格式:
每条 WAL 记录由以下部分组成:
- 固定大小的
XLogRecord头 (24 字节)- 零或多个
XLogRecordBlockHeader(块引用头)XLogRecordDataHeaderShort或XLogRecordDataHeaderLong(main data 头)- 块数据(block data)和主数据(main data)的实际内容
8.3 NEXTOID 机制
src/backend/access/transam/varsup.c 中 GetNewObjectId() 函数的注释说明了 OID 预取机制:
“If we run out of logged for use oids then we must log more”
OID 预取机制: 每次预取
VAR_OID_PREFETCH(8192) 个 OID 的使用权,
写入一条 NEXTOID WAL 记录。只有当预取池耗尽时才写新的 WAL 记录。
这避免了每分配一个 OID 就写一条 WAL 的巨大开销。
相关代码位于 src/backend/access/transam/xlog.c 第 7588-7602 行的 XLogPutNextOid() 函数注释:
解释了为什么 NEXTOID 不需要立即 flush(
XLogSetAsyncXactLSN):
如果系统崩溃,下次启动时 OID 会从最后一个检查点恢复,
可能导致一些 OID 被重复分配,但这是安全的,
因为新分配的 OID 在被实际使用之前会再次写入 WAL。
8.4 ONLINE 检查点忽略 nextOid 的原因
src/backend/access/transam/xlog.c 第 7934-7944 行的注释明确说明:
/*
* We ignore the nextOid counter in an ONLINE checkpoint, preferring
* to track OID assignment through XLOG_NEXTOID records. The nextOid
* counter is from the start of the checkpoint and might well be stale
* compared to later XLOG_NEXTOID records. We could try to take the
* maximum of the nextOid counter and our latest value, but since
* there's no particular guarantee about the speed with which the OID
* counter wraps around, that's a risky thing to do. In any case,
* users of the nextOid counter are required to avoid assignment of
* duplicates, so that a somewhat out-of-date value should be safe.
*/
在 ONLINE 检查点中忽略 nextOid 计数器,转而通过 XLOG_NEXTOID 记录来跟踪 OID 分配。检查点中的 nextOid 来自检查点开始时,可能相对于后续的 XLOG_NEXTOID 记录已经过时。虽然可以尝试取最大值,但由于 OID 计数器的回卷速度没有特定保证,这样做是有风险的。
8.5 SHUTDOWN 检查点的 Hot Standby 逻辑
src/backend/access/transam/xlog.c 第 7860-7864 行的注释:
/*
* If we see a shutdown checkpoint, we know that nothing was running
* on the primary at this point. So fake-up an empty running-xacts
* record and use that here and now. Recover additional standby state
* for prepared transactions.
*/
如果看到 shutdown 检查点,我们知道此时主库上没有任何事务在运行。因此构造一个空的 running-xacts 记录并立即使用。同时恢复 prepared transactions 的备库状态。
8.6 XLogPutNextOid() 的设计考量
src/backend/access/transam/xlog.c 第 7588-7602 行:
XLogPutNextOid()使用XLogSetAsyncXactLSN()而非XLogFlush()。
这意味着 NEXTOID WAL 记录不会被立即刷盘,
而是标记为异步提交语义。如果在 flush 之前系统崩溃:
- 恢复时 OID 计数器会回退到上一个检查点的值
- 可能导致一些 OID 被"重复"分配
- 但这是安全的: 那些使用了"丢失的"OID 的对象本身也会因崩溃而丢失
附录: CHECKPOINT_SHUTDOWN vs CHECKPOINT_ONLINE 完整对比
| 对比维度 | CHECKPOINT_SHUTDOWN (0x00) | CHECKPOINT_ONLINE (0x10) |
|---|---|---|
| 触发时机 | 正常关闭时 | 运行时手动/自动 |
| 活跃事务 | 无 (oldestActiveXid = 0) |
可能有 (oldestActiveXid > 0) |
| redo 点 | 等于自身 LSN | 小于自身 LSN |
| nextXid 处理 | 直接设置 | 取较大值 |
| nextOid 处理 | 直接设置 | 忽略 |
| MultiXact 处理 | SetNextMXact (直接) |
AdvanceNextMXact (取较大值) |
| oldestXid 处理 | SetTransactionIdLimit (直接) |
条件更新 (只前进) |
| Hot Standby 逻辑 | 构造空事务快照 | 无 |
| 备份冲突检测 | 有 (PANIC) | 无 |
| 恢复重启点 | 是 | 是 |
更多推荐
所有评论(0)