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 为功能/查询优化做的扩展格式。
相关推荐
snhna9 小时前
向量数据库深度实战:从 HNSW 到 pgvector、Milvus 选型与 RAG 落地
数据库·人工智能·python·postgresql·milvus
周小董11 小时前
[1369]Windows上安装PostgreSQL
数据库·windows·postgresql
ss27320 小时前
03 -AI时代首先数据库-PostgreSQL发展历史:从伯克利到AI数据库
postgresql
东风微鸣1 天前
精简 Ubuntu Linux 固件包:每个 N100 节点省下约 450 MB
linux·ubuntu·postgresql
yumgpkpm1 天前
CDP 7.3.1(Cloudera Runtime 7.3.1)VS Acceldata ODP 3.3.6.4 核心引擎详细版本对比
大数据·hive·hadoop·postgresql·zookeeper·spark·hbase
明日清晨1 天前
mobaxterm root身份登录Ubuntu
linux·ubuntu·postgresql
有想法的py工程师2 天前
PostgreSQL【内核】 CBO 优化器的“早停下注”与数学模型失效分析
数据库·postgresql
有想法的py工程师2 天前
PostgreSQL 字符集双重转义击穿 磁盘 I/O
数据库·postgresql
l1t2 天前
DeepSeek总结的PostgreSQL因选择而宽松,因偶然而永久
数据库·postgresql
IvorySQL2 天前
PostgreSQL 日报| PostgreSQL JDBC 安全更新(10 月 10 日)
数据库·postgresql·区块链