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 = 0x175FDC20ReadRecPtr = 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_PREFETCH
  • xl_xid = 1460: 耗尽 OID 池的事务 ID
  • ShmemVariableCache->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: 数据库 OID
    • blkno=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 会话二: 主库参数变更后重启

操作步骤:

  1. 在主库执行 ALTER SYSTEM SET max_connections = 2001;
  2. 执行 pg_ctl -D pgdata9901 restart
  3. 备库 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 (有活跃事务)
redoReadRecPtr 关系 相等 (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_CHANGE
  • xl_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 记录由以下部分组成:

  1. 固定大小的 XLogRecord 头 (24 字节)
  2. 零或多个 XLogRecordBlockHeader(块引用头)
  3. XLogRecordDataHeaderShortXLogRecordDataHeaderLong(main data 头)
  4. 块数据(block data)和主数据(main data)的实际内容

8.3 NEXTOID 机制

src/backend/access/transam/varsup.cGetNewObjectId() 函数的注释说明了 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)
恢复重启点
Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐