Neon 中的 postgres_ffi:用 Rust 精确驾驭 PostgreSQL 磁盘格式与 WAL 的 FFI 封装库
【免费下载链接】neonNeon: Serverless Postgres. We separated storage and compute to offer autoscaling, code-like database branching, and scale to zero.项目地址: https://gitcode.com/GitHub_Trending/ne/neon
本文以 libs/postgres_ffi/README.md 为骨架,结合 libs/postgres_ffi 下的源码、测试与基准,系统讲解 Neon 中这一关键模块:它如何借助 bindgen 从 PostgreSQL 头文件自动生成 Rust 结构体,如何按 PostgreSQL 大版本组织绑定,以及如何用 Rust 读写 pg_control、解析关系文件命名、解码与生成 WAL 记录。读完本文,你将理解 Neon 这类"存算分离"数据库在纯 Rust 代码中处理 PostgreSQL 专有格式的完整工程思路,并掌握在 Neon 仓库中定位、使用与扩展 postgres_ffi 的方法。
一、模块定位:封装一切"PostgreSQL 专有格式知识"
Neon 将 PostgreSQL 的存储与计算分离:计算节点(compute node)仍运行完整的 PostgreSQL,而 pageserver 以纯 Rust 实现持久化存储层,safekeeper 负责 WAL 的接收与复制。这意味着 Rust 侧代码必须能够读懂 PostgreSQL 的数据文件、控制文件与 WAL 记录,这正是postgres_fficrate 的职责。
按 libs/postgres_ffi/README.md 的说明,该模块是"一组用于处理 PostgreSQL 文件格式的工具":它包含一批通过 bindgen 从 PostgreSQL 头文件自动生成的结构体,以及用 Rust 读写、操作这些结构的函数。README 还提出了一个重要的架构原则——WAL 布局和 PostgreSQL 文件格式的"内部知识"应当全部封装在本模块内,代码库其他部分不应深入了解这些格式细节:
"The rest of the codebase should not have intimate knowledge of PostgreSQL file formats or WAL layout, that knowledge should be encapsulated in this module."
从 Cargo.toml 可以看到它的依赖画像:bytes(字节缓冲)、crc32c(CRC32-C 校验,用于 pg_control 与 WAL 记录)、regex(文件名解析)、serde(结构体的序列化/反序列化)、postgres_ffi_types(版本无关的共享类型)以及postgres_versioninfo(大版本枚举PgMajorVersion),构建期依赖bindgen。
1.1 非跨平台、随版本变化的磁盘格式
README 强调了一个关键约束:PostgreSQL 的磁盘格式既不能跨 CPU 架构与操作系统移植,也会在每个大版本中变化。因此:
- 结构体布局必须严格对应某个特定版本的 PostgreSQL C 结构;
- 绑定与依赖它们的代码是版本相关的,不能混用;
- 模块当前按
postgres_ffi::v14、postgres_ffi::v15、postgres_ffi::v16组织(README 撰写时),而当前仓库代码已进一步扩展出v17子模块(见下文第四节)。
二、bindgen:从 PostgreSQL 头文件自动生成 Rust 结构体
自动生成的入口是 bindgen_deps.h,它作为 bindgen 的输入,集中 include 了所需的 PostgreSQL 头文件:
#include "c.h" #include "catalog/pg_control.h" #include "access/xlog_internal.h" #include "storage/block.h" #include "storage/bufpage.h" #include "storage/off.h" #include "access/multixact.h"该文件头注释明确说明了工作流:"如果需要向 Rust 代码暴露新的结构体,就在这里添加头文件,并在 build.rs 中白名单该结构体"。这意味着新增绑定是声明式的:改头文件、改白名单、重新构建即可,不需要手写任何 FFI 胶水。
生成的绑定通过 lib.rs 中的postgres_ffi!宏装配到各版本子模块中:
pub mod bindings { include!(concat!(env!("OUT_DIR"), "/bindings_", stringify!($version), ".rs")); include!(concat!("pg_constants_", stringify!($version), ".rs")); }即构建产物目录OUT_DIR下会为每个版本生成bindings_v14.rs、bindings_v15.rs等文件,再与手工维护的版本相关常量文件pg_constants_v14.rs等合并,共同构成该版本的bindings模块。每个版本子模块还从绑定中重新导出最常用的符号,例如CheckPoint、ControlFileData、DBState_DB_SHUTDOWNED、XLogRecord(见 lib.rs)。
2.1 构建产物的典型内容
从 lib.rs 末尾的重新导出可以看出,v14 的绑定中包含了大量"不太可能跨版本变化"的基础类型,被提升到 crate 顶层供全局使用:
| 类型 | 说明 |
|---|---|
BlockNumber | 块号(u32) |
CheckPoint/ControlFileData | 检查点与控制文件结构 |
MultiXactId/MultiXactOffset | MultiXact 事务 ID 与偏移 |
OffsetNumber | 页内元组偏移 |
Oid/TimeLineID/TransactionId | 对象 ID、时间线 ID、事务 ID |
PageHeaderData | 页头结构 |
RepOriginId | 复制源 ID |
XLogRecord/XLogRecPtr/XLogSegNo | WAL 记录与指针 |
uint32/uint64 | PG 风格的无符号类型别名 |
同时导出了若干版本无关的常量(lib.rs),其值与 PostgreSQL 默认编译参数对应:
pub const BLCKSZ: u16 = 8192; // 数据块大小 pub const RELSEG_SIZE: u32 = 1024 * 1024 * 1024 / BLCKSZ; // 每个段文件 1GB pub const XLOG_BLCKSZ: usize = 8192; // WAL 页大小 pub const WAL_SEGMENT_SIZE: usize = 16 * 1024 * 1024; // WAL 段大小 16MB注释指出这些值对应pg_config.h,可用--with-blocksize、--with-segsize修改,但 Neon 假定使用默认值。也就是说,绑定与常量都以"标准 PostgreSQL 编译配置"为前提。
三、按版本组织的模块结构与运行时版本分派
lib.rs用宏一次性为所有支持的版本生成子模块,并提供一个"枚举所有版本"的宏:
#[macro_export] macro_rules! for_all_postgres_versions { ($macro:tt) => { $macro!(v14); $macro!(v15); $macro!(v16); $macro!(v17); }; } for_all_postgres_versions! { postgres_ffi }每个postgres_ffi::vN子模块包含:
bindings:bindgen 生成的绑定 + 版本相关常量;controlfile_utils:pg_control 读写;nonrelfile_utils:CLOG、MultiXact 等非关系文件的工具;wal_craft_test_export/wal_generator:测试用 WAL 生成;waldecoder_handler:该版本的流式 WAL 解码器实现;xlog_utils:WAL 文件与 LSN 工具。
3.1 运行时按版本分发:dispatch_pgversion!
由于一条 WAL 流或一份 basebackup 可能来自 v14~v17 中的任意一个版本,postgres_ffi 提供了编译期分发宏 dispatch_pgversion!,把"某个PgMajorVersion"映射到"对应的pgv命名空间":
dispatch_pgversion!(my_pgversion, { pgv::constants::XLOG_DBASE_CREATE })其语义是:若my_pgversion是支持的版本,则在作用域内以pgv别名use对应版本的绑定后执行代码块;若不支持则panic!(也可传入第三个参数提供默认处理,例如返回错误而不是 panic)。同类宏还有 enum_pgversion!,用于生成"跨版本枚举"类型——把各版本的同名结构(如CheckPoint)统一包装成一个枚举,并提供pg_version()方法和从具体版本类型到枚举的Into转换。
这两个宏是 postgres_ffi 处理"多版本共存"的基石:pageserver、safekeeper 在运行时面对的 PostgreSQL 版本不确定,而 Rust 的类型必须在编译期确定,分发宏把"运行时版本"转化为"编译期分支"。
四、pg_constants:手工维护的版本无关常量
README 专门提到 pg_constants.rs 中有一批常量是"从 PostgreSQL 头文件手工复制,而非自动生成",并指出它们"大多也应该自动生成,但这是 TODO"。文件注释补充了保留手工方式的理由:"集中在一处、便于加注释"。
这些常量覆盖了 Neon 需要直接理解 WAL 语义的方方面面,大致可分几类:
- WAL 记录类型(rmgr 操作码):
XLOG_HEAP_INSERT/DELETE/UPDATE/HOT_UPDATE/LOCK、XLOG_HEAP2_VISIBLE/MULTI_INSERT、XLOG_CHECKPOINT_SHUTDOWN/ONLINE、XLOG_PARAMETER_CHANGE、XLOG_FPI、XLOG_SWITCH等; - 资源管理器 ID(rmgrlist):
RM_XLOG_ID=0、RM_XACT_ID=1、RM_SMGR_ID=2、RM_CLOG_ID=3……RM_LOGICALMSG_ID=21,以及 Neon 自定义的RM_NEON_ID=134和一批XLOG_NEON_*操作码(对应 pgxn/neon/neon_rmgr 中的 Neon WAL 扩展); - XLogRecord 头布局:
XLR_BLOCK_ID_DATA_SHORT=255、XLR_BLOCK_ID_DATA_LONG=254、XLR_BLOCK_ID_ORIGIN=253、XLR_BLOCK_ID_TOPLEVEL_XID=252、BKPBLOCK_*标志位、BKPIMAGE_HAS_HOLE等; - 事务与 CLOG:
FIRST_NORMAL_TRANSACTION_ID=3、CLOG_XACTS_PER_BYTE=4、TRANSACTION_STATUS_*等; - MultiXact、可见性映射(visibilitymap)、SLRU 布局等。
此外还有两个对 Neon 运维很实用的导出:
pub const PGDATA_SPECIAL_FILES: [&str; 3] = ["pg_hba.conf", "pg_ident.conf", "postgresql.auto.conf"]; pub static PG_HBA: &str = include_str!("../samples/pg_hba.conf");注释解释了原因:basebackup 恢复时不能覆盖postgresql.conf(因为 safekeeper 同步需要先有配置),而这三个文件可以安全地在备份恢复后修改;samples/pg_hba.conf则是随 crate 附带的默认认证配置样例。
4.1 版本相关常量文件
与pg_constants.rs相对,每个版本还有自己的pg_constants_v14.rs~pg_constants_v17.rs,存放随版本变化的常量(例如各版本不同的 WAL 记录布局参数),它们与 bindgen 输出一起被include!进对应版本的bindings模块。
五、文件系统层工具:关系文件与非关系文件
5.1 关系文件命名解析 relfile_utils.rs
PostgreSQL 数据目录中关系文件的命名遵循relpath()与_mdfd_segpath()的规则。postgres_ffi 用正则实现了解析函数parse_relfilename,把文件名解析为(relfilenode, forknum, segno)三元组:
<oid> → (oid, 0, 0) <oid>_<fork name> → (oid, forknum, 0) <oid>.<segment number> → (oid, 0, segno) <oid>_<fork name>.<segment number>正则:^(?P<relnode>\d+)(_(?P<forkname>[a-z]+))?(\.(?P<segno>\d+))?$。forkname通过forkname_to_number(来自postgres_ffi_types::forknum)映射为 fork 编号,如main→0、fsm→1、vm→2、init→3。文件中还带有一套完整的单元测试(relfile_utils.rs),验证合法输入(含3147483648这样超出 i32 的 relfilenode)、非法输入(foo、1.2.3、1234_invalid、负数、超长数字)以及边界情况(0被接受,超大的段号也被接受),可作为理解解析规则的第一手资料。
5.2 CLOG 与 MultiXact 工具 nonrelfile_utils.rs
非关系文件方面,该文件实现了与 PostgreSQL C 宏/函数等价的 Rust 版本:
transaction_id_set_status/transaction_id_get_status:在 CLOG 页中读写某个 XID 的事务状态(提交/中止/子提交),按CLOG_XACTS_PER_PAGE、CLOG_XACTS_PER_BYTE、CLOG_BITS_PER_XACT计算字节偏移与位偏移;clogpage_precedes:判断 CLOG 页序(等价于 clog.c 中的CLOGPagePrecedes,处理 XID 回绕);slru_may_delete_clogsegment:判断某个 SLRU 段是否可删除(对应 slru.c 的SlruMayDeleteSegment());mx_offset_to_*系列:把 MultiXact 成员偏移换算为 flags/member 在页内与段内的位置。
测试 test_multixid_calc 特意声明:这些测试值由一个小 C 程序调用 PostgreSQL 宏生成,用来证明 Rust 实现与 PostgreSQL 的MXOffsetTo*宏逐位一致——这是"磁盘格式兼容"最有力的验证方式。
六、pg_control 控制文件的读写 controlfile_utils.rs
global/pg_control是 PostgreSQL 启动时最先读取的文件之一:它记录上次是否干净关闭、最新检查点的位置与副本,并包含版本号、块大小、对齐与字节序等"数据目录与二进制是否兼容"的信息。由于磁盘格式不跨平台,这些字段必须被严格解析。文件注释还透露了一个布局细节:有效数据被设计为小于 512 字节以支持原子更新,实际文件为 8192 字节,其余部分填充零。
ControlFileData提供了两个方法:
decode(buf):先校验长度不小于结构体大小,再用crc32c对crc字段之前的内容计算校验和并与文件中的 CRC 比对,最后用utils::bin_ser::LeSer反序列化;encode():序列化后重新计算 CRC 写回,并填充到完整的PG_CONTROL_FILE_SIZE(8192 字节)。
CRC 位置通过std::mem::offset_of!(ControlFileData, crc)计算,等价于 C 的offsetof,保证无论 bindgen 生成何种布局都能正确定位。可用 PostgreSQL 自带的pg_controldata工具对照查看内容。
6.1 为"按 LSN 启动"生成 pg_control xlog_utils.rs
控制文件工具在 Neon 中最重要的应用场景是 generate_pg_control:pageserver 持久化了 pg_control 与 checkpoint 记录,当 compute 节点需要从某个 LSN 启动 basebackup 时,该函数据此合成一份全新的 pg_control。其关键语义包括:
- Neon 内部 checkpoint 结构中的
redo字段约定与 PostgreSQL 不同(关闭检查点指向 WAL 记录末尾而非开头,在线检查点则置 0); was_shutdown = Lsn(checkpoint.redo) == lsn用于判断"该 LSN 处是否存在关闭检查点",即是否从干净关闭状态启动;- 输出结构中
redo恒被设为启动 LSN,以提示无需 WAL 重放(还需要 PostgreSQL 侧 Neon 定制代码配合); - 即使并非干净关闭,
state也写为DBState_DB_SHUTDOWNED,因为 Neon 启动流程本就忽略控制文件中的状态(类似独立 PostgreSQL 的归档恢复),checkPoint指针置 0。
返回值是(pg_control 内容, system_identifier, was_shutdown)三元组。这正是"存算分离"下 basebackup 无需重放 WAL 的关键一环,相关持久化细节见 pageserver 的 walingest.rs。
七、WAL 处理:从文件命名到流式解码
7.1 WAL 段文件工具
xlog_utils.rs 实现了与 PostgreSQL 同名函数对应的 Rust 版本(刻意保留 C 风格命名):
XLogFileName(tli, logSegNo, wal_seg_size):把时间线 ID 与段号格式化为000000010000000000000001形式的 24 位十六进制文件名(时间线 8 位 + log 8 位 + seg 8 位);XLogFromFileName/IsXLogFileName/IsPartialXLogFileName:反向解析文件名、判断是否为合法 WAL 段名或.partial段名;XLogSegmentsPerXLogId/XLogSegNoOffsetToRecPtr:段号与 LSN 的换算;normalize_lsn:若 LSN 落在页首则后移一个页头(段首是XLOG_SIZE_OF_XLOG_LONG_PHD,其余是XLOG_SIZE_OF_XLOG_SHORT_PHD),否则 8 字节对齐——这是 WAL 记录起始位置的硬性要求。
Neon 的 PostgreSQL 时间线(PG timeline)固定为 1,与 Neon 自身的 timeline 概念无关(见 lib.rs):
pub const PG_TLI: u32 = 1;7.2 事务 ID 与页操作
lib.rs 还导出几个纯 Rust 移植的小工具:
transaction_id_is_normal/transaction_id_precedes:对应 transam.h/transam.c,处理 32 位 XID 回绕的模 2^32 比较;page_is_new/page_get_lsn/page_set_lsn:通过检查pg_upper==0判断页未初始化(对应 PostgreSQL 的PageIsInit()宏),以及读写页头前 8 字节的 LSN;fsm_logical_to_physical:对应 freespace.c,把 FSM 逻辑地址转为物理块号。
7.3 流式 WAL 解码器 WalStreamDecoder
lib.rs 定义了WalStreamDecoder,一个面向"从 safekeeper 拉取/接收 WAL 字节流"场景的有状态解码器:
State枚举表示三态:WaitingForRecord(等待下一条记录)、ReassemblingRecord(一条记录跨多个 chunk,正在拼接)、SkippingEverything(跳过直到某个 LSN);feed_bytes(buf)持续喂入字节,poll_decode()返回Result<Option<(Lsn, Bytes)>>——每次产出"记录结束 LSN + 记录原始字节";- 底层通过
dispatch_pgversion!分派到对应版本的 waldecoder_handler.rs 实现; WalDecodeError携带出错位置 LSN,便于日志与断点续传。
解码器在仓库中的实际用法可参考 find_end_of_wal:从某个记录边界 LSN 开始,按段遍历(优先尝试.partial,再尝试完整段),逐段喂给解码器并推进"已知最大完整记录末尾"的 LSN,直到段缺失或 EOF——这是判断一个数据目录中 WAL 写到哪里的通用工具。
7.4 生成空的 WAL 段 generate_wal_segment
计算节点启动需要一个从给定 LSN 开始、头两页带正确页头的空 WAL 段(pg_waldump等工具才能识别)。该函数:
- 若 LSN 在段首页:写长页头(
XLogLongPageHeaderData,含 system id、段大小、块大小、magic),并在需要时伪造一条XLP_FIRST_IS_CONTRECORD的"假记录"使xlp_rem_len指向第一个真实记录起始处; - 若 LSN 在段中某页:在对应页偏移处写短页头,同样处理 contrecord 语义;
- 其余部分全部补零,最后返回一个完整 16MB 的段。
7.5 测试与基准用 WAL 生成器 wal_generator.rs
与解码相对,模块还提供生成WAL 的能力,供测试与基准使用:
Record:一条记录的载荷(rmid、info、data),encode(prev_lsn)负责加 XLogRecord 头、按数据长度选择XLR_BLOCK_ID_DATA_SHORT/LONG头、并用crc32c_append计算 CRC;RecordGeneratortrait 与WalGenerator:迭代器式地把记录序列写成"页头 + 记录 + 8 字节对齐填充"的完整、良构 WAL,可跨页跨段;- 文件注释给出了直观的布局示意(Segment/Page/Record 层级),并提醒 WAL 格式与版本相关,需按目标版本导入(如
postgres_ffi::v17::wal_generator::WalGenerator)。
配套的 wal_craft crate 承接更复杂的"手工构造 WAL 写入测试"需求(README 注释里也提到"如果要构造 WAL 并为本模块写测试,请放到 wal_craft crate"),并提供了共享的测试导出模块wal_craft_test_export。
八、WAL 记录的解码与人类可读描述 walrecord.rs
walrecord.rs承载了 WAL 记录层的核心解码逻辑,其入口 decode_wal_record 严格遵循 xlogrecord.h 描述的记录布局逐段解析:
XLogRecord 固定头 XLogRecordBlockHeader(可多个) ├─ 若 BKPBLOCK_HAS_IMAGE:XLogRecordBlockImageHeader │ └─ 若 HAS_HOLE 且压缩:XLogRecordBlockCompressHeader ├─ 若未设 BKPBLOCK_SAME_REL:RelFileNode └─ BlockNumber XLogRecordDataHeader[Short|Long] 各 block 数据区 主数据区(main data)该函数把结果填入调用者复用的DecodedWALRecord结构(注释明确说明这是 WAL 消化热路径,复用结构体以避免分配),产出:记录的xl_xid、xl_info、xl_rmid、原始字节、涉及的块列表DecodedBkpBlock(含 relfilenode 三元组、fork、blkno、全页镜像的 hole 偏移/长度/压缩标志等)以及main_data_offset。
8.1 各版本 rmgr 数据解码
同文件按版本实现了若干 rmgr 记录的载荷解码,例如:
v14::XlHeapInsert/XlHeapDelete/XlHeapUpdate/XlHeapLock/XlHeapMultiInsert/XlParameterChange;v15复用 v14 的定义,v16重新定义了字段有变化的XlHeapDelete/XlHeapUpdate/XlHeapLock,并新增rm_neon子模块(PG16 起引入 Neon 自定义 RMGRRM_NEON_ID=134来承载 Neon 风格 WAL,见 pg_constants.rs);v17新增XlEndOfRecovery解码,其余复用 v14/v16。
版本差异是真实的:例如XlClogTruncate的pageno在 PG17 之前是 u32,PG17 起是 u64(walrecord.rs);DecodedWALRecord::is_dbase_create_copy也按版本区分 PG14 的XLOG_DBASE_CREATE与 PG15+ 的XLOG_DBASE_CREATE_FILE_COPY。
8.2 事务记录解析 XlXactParsedRecord
XlXactParsedRecord::decode对应 PostgreSQL xactdesc.c 中的ParseCommitRecord/ParseAbortRecord,解析 commit/abort 记录中的时间戳、xinfo标志位(XACT_XINFO_HAS_*系列)、db/ts 信息、子事务列表、涉及的 relfilenode 列表等——Neon 需要据此知道提交影响了哪些关系文件。XlRunningXacts则解析XLOG_RUNNING_XACTS记录中的活动事务快照。
8.3 描述函数 describe_postgres_wal_record
describe_postgres_wal_record 把常见 WAL 记录翻译成人类可读的字符串(如"HEAP INSERT"、"HEAP2 MULTI_INSERT"、"XLOG FPI"),用于dump_layer_file之类的调试工具。注释坦承这是手写的近似实现,理想方案是复用 PostgreSQL 的 rmgrdesc 基础设施。
九、基准测试与验证手段
benches/README.md 给出了围绕 WAL 解码器的性能测试方式(criterion + pprof):
# 全部基准 cargo bench --package postgres_ffi # 指定基准文件 cargo bench --package postgres_ffi --bench waldecoder # 指定用例,例如 1024 字节完整记录 cargo bench --package postgres_ffi --bench waldecoder complete_record/size=1024 # 列出可用用例 cargo bench --package postgres_ffi --benches -- --list # 生成火焰图(profiling 10 秒,输出 target/criterion/*/profile/flamegraph.svg) cargo bench --package postgres_ffi --bench waldecoder complete_record/size=1024 -- --profile-time 10图表与统计见target/criterion/report/index.html,基准会自动与上一次运行对比,也支持--baseline/--save-baseline。由于decode_wal_record位于 WAL 消化热路径,这类基准对 pageserver 的写入延迟意义重大。解码器基准实现位于 benches/waldecoder.rs,依赖 wal_craft 生成测试 WAL 数据。
十、在 Neon 仓库中的实际应用
postgres_ffi 是 Neon 中"纯 Rust 读写 PostgreSQL 格式"的唯一知识源,其主要消费方包括:
- pageserver:消化 WAL(
decode_wal_record、WalStreamDecoder)、维护 pg_control 与 checkpoint(generate_pg_control)、解析关系文件(parse_relfilename)、识别 Neon 自定义 RMGR 记录(RM_NEON_ID); - safekeeper:接收、存储与转发 WAL 段(
XLogFileName、IsPartialXLogFileName、WalStreamDecoder),并生成空 WAL 段供 compute 启动(generate_wal_segment); - compute 节点 basebackup:
find_end_of_wal定位 WAL 末尾、generate_wal_segment制造可识别的起始段; - 测试与基准:
wal_craft、wal_generator、WalStreamDecoder基准。
总结
postgres_ffi用清晰的工程分层解决了"存算分离数据库在 Rust 中兼容 PostgreSQL 磁盘格式"的难题:bindgen 自动生成结构体保证与 C 布局一致,按大版本组织子模块并用dispatch_pgversion!宏做运行时分发来容纳版本差异,pg_constants集中维护语义常量,controlfile_utils/relfile_utils/nonrelfile_utils/xlog_utils/walrecord各司其职地覆盖控制文件、关系文件、CLOG/MultiXact、WAL 段与 WAL 记录的全链路读写。README 中提出的"将格式知识封装在本模块"的原则,至今仍是 Neon 代码库中处理 PostgreSQL 专有格式的一致约定。
【免费下载链接】neonNeon: Serverless Postgres. We separated storage and compute to offer autoscaling, code-like database branching, and scale to zero.项目地址: https://gitcode.com/GitHub_Trending/ne/neon
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考