neon源码分析(4)slru页面redo与pg的区别

CLOG Redo: Postgres vs Neon

CLOG / pg_xact 的 redo(事务提交状态位图):

  • Postgres 原生实现:XLOG_XACT_* 记录会在 redo 时更新 pg_xact(CLOG),RM_CLOG_ID 记录负责 ZEROPAGE/TRUNCATE 等维护。
  • Neon 实现:pageserver ingest 阶段把 PG 的 xact/clog WAL "翻译"为少量 Neon 自定义记录(NeonWalRecord::*),然后在 Rust 侧按页回放,避免走 walredo postgres 进程。

1. Postgres(v17) 的 CLOG redo 过程

1.1 XACT rmgr: commit/abort 触发对 pg_xact 的更新

Postgres 在恢复/redo 时,解析 RM_XACT_ID 的 commit record,然后调用 TransactionIdCommitTree/AbortTree 去更新 pg_xact

来源:xact.c

c 复制代码
/*
 * xact_redo_commit(): redo 一条 XLOG_XACT_COMMIT 记录
 * 关键点:最终会把 xid + subxacts 标记到 pg_xact (CLOG) 里。
 */
static void
xact_redo_commit(xl_xact_parsed_commit *parsed,
                 TransactionId xid,
                 XLogRecPtr lsn,
                 RepOriginId origin_id)
{
    ...

    if (standbyState == STANDBY_DISABLED)
    {
        /*
         * 直接把 xid/subxids 标记为 committed(同步提交场景 lsn 可忽略)
         */
        TransactionIdCommitTree(xid, parsed->nsubxacts, parsed->subxacts);
    }
    else
    {
        /*
         * 恢复时走异步协议:需要记录 commit record LSN,
         * 以保证后续 hint bits/一致性相关规则不会越界。
         */
        TransactionIdAsyncCommitTree(xid, parsed->nsubxacts, parsed->subxacts, lsn);
        ...
    }
    ...
}

TransactionIdCommitTree/AsyncCommitTree 只是薄封装,本质都是 TransactionIdSetTreeStatus(...)

来源:transam.c

c 复制代码
/*
 * xid + 子事务树的状态写入 pg_xact。
 * 对 commit,最终状态是 TRANSACTION_STATUS_COMMITTED。
 */
void
TransactionIdCommitTree(TransactionId xid, int nxids, TransactionId *xids)
{
    TransactionIdSetTreeStatus(xid, nxids, xids,
                               TRANSACTION_STATUS_COMMITTED,
                               InvalidXLogRecPtr);
}

void
TransactionIdAsyncCommitTree(TransactionId xid, int nxids, TransactionId *xids,
                             XLogRecPtr lsn)
{
    TransactionIdSetTreeStatus(xid, nxids, xids,
                               TRANSACTION_STATUS_COMMITTED, lsn);
}

1.2 clog.c: TransactionIdSetTreeStatus 与"跨页原子性"的 subcommitted 设计

Postgres 的 CLOG(pg_xact)每个 xid 占 2bit,多个 xid 落在不同 CLOG page 时,需要用 SUB_COMMITTED 作为中间态来维持"对外观测的原子性"。

来源:clog.c

c 复制代码
/*
 * TransactionIdSetTreeStatus:
 * - 如果 xid 和全部 subxids 都落在同一页:一次加锁、一次写完。
 * - 如果跨页:
 *   1) 先把非首页上的 subxids 标记为 SUB_COMMITTED
 *   2) 再把首页(含 top xid)标 committed
 *   3) 最后把其余页上的 subxids 从 SUB_COMMITTED 升到 committed
 *
 * 目的:让并发检查者看到 top xid committed 时,子事务树状态"整体上"不违背语义。
 */
void
TransactionIdSetTreeStatus(TransactionId xid, int nsubxids,
                           TransactionId *subxids, XidStatus status, XLogRecPtr lsn)
{
    int64 pageno = TransactionIdToPage(xid);
    int i;

    for (i = 0; i < nsubxids; i++)
        if (TransactionIdToPage(subxids[i]) != pageno)
            break;

    if (i == nsubxids)
    {
        TransactionIdSetPageStatus(xid, nsubxids, subxids, status, lsn, pageno, true);
    }
    else
    {
        int nsubxids_on_first_page = i;
        if (status == TRANSACTION_STATUS_COMMITTED)
            set_status_by_pages(nsubxids - nsubxids_on_first_page,
                                subxids + nsubxids_on_first_page,
                                TRANSACTION_STATUS_SUB_COMMITTED, lsn);

        TransactionIdSetPageStatus(xid, nsubxids_on_first_page, subxids, status,
                                   lsn, pageno, false);

        set_status_by_pages(nsubxids - nsubxids_on_first_page,
                            subxids + nsubxids_on_first_page,
                            status, lsn);
    }
}

1.3 写入某个 xid 的 2bit:TransactionIdSetStatusBit

真正"改位图"的核心函数,直接在 SLRU 缓冲页内存上修改对应 byte/bit,然后标记 LSN 组信息等。

来源:clog.c

c 复制代码
/*
 * TransactionIdSetStatusBit:
 * - 要求调用方已持有 SLRU bank lock(保护共享缓冲池)
 * - 根据 xid 计算在 page 内的 byteno/bshift,然后写 2bit 状态
 * - 对 async commit,会更新 group_lsn[] 用于 WAL flush 规则
 */
static void
TransactionIdSetStatusBit(TransactionId xid, XidStatus status, XLogRecPtr lsn, int slotno)
{
    int byteno = TransactionIdToByte(xid);
    int bshift = TransactionIdToBIndex(xid) * CLOG_BITS_PER_XACT;
    char *byteptr;
    char byteval;
    char curval;

    byteptr = XactCtl->shared->page_buffer[slotno] + byteno;
    curval = (*byteptr >> bshift) & CLOG_XACT_BITMASK;

    /* 修改 2bit */
    byteval = *byteptr;
    byteval &= ~(((1 << CLOG_BITS_PER_XACT) - 1) << bshift);
    byteval |= (status << bshift);
    *byteptr = byteval;

    /* 异步提交下会更新 group_lsn[...],用于推断"flush 到哪个 LSN 才安全" */
    if (!XLogRecPtrIsInvalid(lsn))
        ...
}

1.4 RM_CLOG_ID: ZEROPAGE/TRUNCATE 的 redo

RM_CLOG_ID 只负责 CLOG "页面初始化为 0"以及"截断旧段"。redo 时通过 SimpleLruWritePage/SimpleLruTruncate 操作 CLOG 的 SLRU 文件。

来源:clog.c

c 复制代码
/*
 * clog_redo:
 * - CLOG_ZEROPAGE: 创建/清零新 page,并写入对应 SLRU 段文件
 * - CLOG_TRUNCATE: 推进 oldestXid 并删除过旧段文件
 */
void
clog_redo(XLogReaderState *record)
{
    uint8 info = XLogRecGetInfo(record) & ~XLR_INFO_MASK;

    if (info == CLOG_ZEROPAGE)
    {
        int64 pageno;
        int slotno;
        LWLock *lock;

        memcpy(&pageno, XLogRecGetData(record), sizeof(pageno));

        lock = SimpleLruGetBankLock(XactCtl, pageno);
        LWLockAcquire(lock, LW_EXCLUSIVE);

        slotno = ZeroCLOGPage(pageno, false);
        SimpleLruWritePage(XactCtl, slotno);

        LWLockRelease(lock);
    }
    else if (info == CLOG_TRUNCATE)
    {
        xl_clog_truncate xlrec;
        memcpy(&xlrec, XLogRecGetData(record), sizeof(xl_clog_truncate));

        AdvanceOldestClogXid(xlrec.oldestXact);
        SimpleLruTruncate(XactCtl, xlrec.pageno);
    }
    else
        elog(PANIC, "clog_redo: unknown op code %u", info);
}

2. Neon 的 CLOG redo 过程(pageserver)

Neon 的关键变化是:不直接把 PG 的 RM_XACT_ID/RM_CLOG_ID 原始 redo 交给 walredo postgres 进程,而是:

  1. ingest 阶段把"对 pg_xact 的影响"翻译成 Neon 自定义记录(NeonWalRecord::ClogSetCommitted/Aborted)并写入 pageserver 的 KV;
  2. 读页重建时用 Rust 代码按页回放这些记录("改位图")。

2.1 ingest:把 XLOG_XACT_COMMIT/ABORT 翻译成 ClogSetCommitted/Aborted

来源:walingest.rs

rust 复制代码
// ingest_xact_record():
// - 解析 commit/abort record 中的 xid + subxacts
// - 按"落在哪个 CLOG page"分组
// - 生成 NeonWalRecord::ClogSetCommitted / ClogSetAborted 写到对应 SLRU page key
//
// 注意:这里只生成 "最终态" committed/aborted,并没有生成 Postgres 那种跨页 subcommitted 中间态。
modification.put_slru_wal_record(
    SlruKind::Clog,
    segno,
    rpageno,
    if is_commit {
        NeonWalRecord::ClogSetCommitted { xids: page_xids, timestamp: parsed.xact_time }
    } else {
        NeonWalRecord::ClogSetAborted { xids: page_xids }
    },
)?;

2.2 ingest:CLOG_ZEROPAGE 直接写入 ZERO_PAGE(作为 base image)

来源:walingest.rs

rust 复制代码
// ingest_clog_zero_page():
// Postgres 会把新 CLOG page 清零并写到 pg_xact 段文件。
// Neon 在 pageserver 里则把这一页作为一张"page image"存进 KV(后续回放需要 base image)。
self.put_slru_page_image(
    modification,
    SlruKind::Clog,
    segno,
    rpageno,
    ZERO_PAGE.clone(),
    ctx,
).await?;

2.3 apply(redo):Rust 侧直接改位图,而不是走 SimpleLru

Neon 回放 ClogSetCommitted/Aborted 时,会验证 key 是 SlruKind::Clog,然后对每个 xid 在 page buffer 上写 2bit 状态。

来源:apply_neon.rs

rust 复制代码
// apply_in_neon():
// - 输入:某一页的 base image(通常来自 ZERO_PAGE 或历史 image) + 该页的 WAL deltas
// - 输出:更新后的页字节
NeonWalRecord::ClogSetCommitted { xids, timestamp } => {
    let (slru_kind, segno, blknum) = key.to_slru_block().context("invalid record")?;
    assert_eq!(slru_kind, SlruKind::Clog);

    for &xid in xids {
        // 校验该 xid 确实落在这个 (segno, blknum) 上
        ...
        transaction_id_set_status(
            xid,
            pg_constants::TRANSACTION_STATUS_COMMITTED,
            page,
        );
    }

    // Neon 额外把 commit timestamp 追加到页尾(不是 Postgres 原生 pg_xact 格式)
    ...
}

NeonWalRecord::ClogSetAborted { xids } => {
    ...
    transaction_id_set_status(xid, pg_constants::TRANSACTION_STATUS_ABORTED, page);
}

这里的 transaction_id_set_status() 就是对 CLOG 页的 byte/bit 做更新,逻辑与 Postgres 的 TransactionIdSetStatusBit 对应,但不涉及共享缓存/锁/刷盘。

来源:nonrelfile_utils.rs

rust 复制代码
// transaction_id_set_status():
// 计算 byteno/bshift,并写 2bit 状态到页内。
pub fn transaction_id_set_status(xid: u32, status: u8, page: &mut BytesMut) {
    let byteno: usize =
        ((xid % pg_constants::CLOG_XACTS_PER_PAGE) / pg_constants::CLOG_XACTS_PER_BYTE) as usize;
    let bshift: u8 =
        ((xid % pg_constants::CLOG_XACTS_PER_BYTE) * pg_constants::CLOG_BITS_PER_XACT as u32) as u8;
    page[byteno] =
        (page[byteno] & !(pg_constants::CLOG_XACT_BITMASK << bshift)) | (status << bshift);
}

3. 核心差异对比(结论)

3.1 "是否维护 SLRU 缓冲池/刷盘"不同

  • Postgres:
    • redo 目标是本地 PGDATA/pg_xact 段文件
    • 通过 slru.c 的共享缓冲池 + 锁(LWLock)管理并发,必要时 SimpleLruWritePage 刷盘
    • 还维护 group_lsn[] 来满足 WAL rule(异步提交时尤其关键)
  • Neon:
    • redo 目标是 pageserver 的 版本化 KV 中的 "SLRU page key"
    • 不需要实现 SimpleLru 的共享缓存/LRU/刷盘;按需读取 base image 并回放得到目标页
    • slru_* keyspace 只是一种"命名空间",底层仍是 timeline 的 layer 存储

3.2 "跨页 subcommitted 中间态"处理不同

  • Postgres:
    • TransactionIdSetTreeStatus 在 commit 且跨页时会先写 SUB_COMMITTED 再最终 committed
    • 目的:并发观察者在读取 CLOG 时不会看到语义上不一致的中间状态
  • Neon:
    • ingest 时按页把 xid/subxids 分组,直接写 committed/aborted(不写 SUB_COMMITTED)
    • 原因:Neon 的可见性是 按 LSN 版本化 的;commit 对应的 ClogSetCommitted 记录只在 commit 的 LSN 之后可见,
      从时间维度上避免了 Postgres "同一时刻跨页更新导致的中间态观测"问题(读某个 LSN 的视图是稳定的)。

3.3 timestamp 存储方式不同(Neon 的扩展)

  • Postgres:
    • pg_xact 只存 2bit 状态;commit timestamp 由 commit_ts SLRU(可选)维护
  • Neon:
    • ClogSetCommitted apply 时把 timestamp 附加到页尾(见 apply_neon.rs),属于 Neon 为功能/查询优化做的扩展格式。
相关推荐
Navicat中国5 小时前
Navicat Premium 17 在设计表结构时,未显示上移 / 下移功能按钮,什么原因?
postgresql·navicat·表设计
l1t9 小时前
DeepSeek总结的Postgres 扩展天花板:当一个实例试图包揽一切时
数据库·postgresql
YMatrix 官方技术社区12 小时前
全栈向量化 + 库内流计算:YMatrix 亮相 Postgres Conference 2026,双引擎重塑 AGI 时代 PostgreSQL 性能底座
大数据·postgresql·agi·ymatrix·超融合数据库
hudson202214 小时前
轻松审计数据库变更
postgresql
瀚高PG实验室14 小时前
PostgreSQL 的 CREATE STATISTICS 未检查 schema 的 CREATE 权限 HGVE-2025-E010
数据库·postgresql·瀚高数据库
ZPC82102 天前
Ubuntu 实时性优化(专属定制版,适配 fast_shm 通信)
linux·数据库·postgresql
有想法的py工程师2 天前
PostgreSQL 在AWS的 T 系列实例上的性能陷阱
数据库·postgresql·aws
l1t2 天前
DeepSeek总结的PostgreSQL 19查询提示功能
数据库·postgresql
dishugj3 天前
psql-客户端工具日常使用命令整理
数据库·postgresql