尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Neon 中的 postgres_ffi:用 Rust 精确驾驭 PostgreSQL 磁盘格式与 WAL 的 FFI 封装库

Neon 中的 postgres_ffi:用 Rust 精确驾驭 PostgreSQL 磁盘格式与 WAL 的 FFI 封装库 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字节缓冲、crc32cCRC32-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块号u32CheckPoint/ControlFileData检查点与控制文件结构MultiXactId/MultiXactOffsetMultiXact 事务 ID 与偏移OffsetNumber页内元组偏移Oid/TimeLineID/TransactionId对象 ID、时间线 ID、事务 IDPageHeaderData页头结构RepOriginId复制源 IDXLogRecord/XLogRecPtr/XLogSegNoWAL 记录与指针uint32/uint64PG 风格的无符号类型别名同时导出了若干版本无关的常量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子模块包含bindingsbindgen 生成的绑定 版本相关常量controlfile_utilspg_control 读写nonrelfile_utilsCLOG、MultiXact 等非关系文件的工具wal_craft_test_export/wal_generator测试用 WAL 生成waldecoder_handler该版本的流式 WAL 解码器实现xlog_utilsWAL 文件与 LSN 工具。3.1 运行时按版本分发dispatch_pgversion!由于一条 WAL 流或一份 basebackup 可能来自 v14v17 中的任意一个版本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等资源管理器 IDrmgrlistRM_XLOG_ID0、RM_XACT_ID1、RM_SMGR_ID2、RM_CLOG_ID3……RM_LOGICALMSG_ID21以及 Neon 自定义的RM_NEON_ID134和一批XLOG_NEON_*操作码对应 pgxn/neon/neon_rmgr 中的 Neon WAL 扩展XLogRecord 头布局XLR_BLOCK_ID_DATA_SHORT255、XLR_BLOCK_ID_DATA_LONG254、XLR_BLOCK_ID_ORIGIN253、XLR_BLOCK_ID_TOPLEVEL_XID252、BKPBLOCK_*标志位、BKPIMAGE_HAS_HOLE等事务与 CLOGFIRST_NORMAL_TRANSACTION_ID3、CLOG_XACTS_PER_BYTE4、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.rspg_constants_v17.rs存放随版本变化的常量例如各版本不同的 WAL 记录布局参数它们与 bindgen 输出一起被include!进对应版本的bindings模块。五、文件系统层工具关系文件与非关系文件5.1 关系文件命名解析 relfile_utils.rsPostgreSQL 数据目录中关系文件的命名遵循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正则^(?Prelnode\d)(_(?Pforkname[a-z]))?(\.(?Psegno\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.rsglobal/pg_control是 PostgreSQL 启动时最先读取的文件之一它记录上次是否干净关闭、最新检查点的位置与副本并包含版本号、块大小、对齐与字节序等数据目录与二进制是否兼容的信息。由于磁盘格式不跨平台这些字段必须被严格解析。文件注释还透露了一个布局细节有效数据被设计为小于 512 字节以支持原子更新实际文件为 8192 字节其余部分填充零。ControlFileData提供了两个方法decode(buf)先校验长度不小于结构体大小再用crc32c对crc字段之前的内容计算校验和并与文件中的 CRC 比对最后用utils::bin_ser::LeSer反序列化encode()序列化后重新计算 CRC 写回并填充到完整的PG_CONTROL_FILE_SIZE8192 字节。CRC 位置通过std::mem::offset_of!(ControlFileData, crc)计算等价于 C 的offsetof保证无论 bindgen 生成何种布局都能正确定位。可用 PostgreSQL 自带的pg_controldata工具对照查看内容。6.1 为按 LSN 启动生成 pg_control xlog_utils.rs控制文件工具在 Neon 中最重要的应用场景是 generate_pg_controlpageserver 持久化了 pg_control 与 checkpoint 记录当 compute 节点需要从某个 LSN 启动 basebackup 时该函数据此合成一份全新的 pg_control。其关键语义包括Neon 内部 checkpoint 结构中的redo字段约定与 PostgreSQL 不同关闭检查点指向 WAL 记录末尾而非开头在线检查点则置 0was_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.rspub 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_upper0判断页未初始化对应 PostgreSQL 的PageIsInit()宏以及读写页头前 8 字节的 LSNfsm_logical_to_physical对应 freespace.c把 FSM 逻辑地址转为物理块号。7.3 流式 WAL 解码器 WalStreamDecoderlib.rs 定义了WalStreamDecoder一个面向从 safekeeper 拉取/接收 WAL 字节流场景的有状态解码器State枚举表示三态WaitingForRecord等待下一条记录、ReassemblingRecord一条记录跨多个 chunk正在拼接、SkippingEverything跳过直到某个 LSNfeed_bytes(buf)持续喂入字节poll_decode()返回ResultOption(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、dataencode(prev_lsn)负责加 XLogRecord 头、按数据长度选择XLR_BLOCK_ID_DATA_SHORT/LONG头、并用crc32c_append计算 CRCRecordGeneratortrait 与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.rswalrecord.rs承载了 WAL 记录层的核心解码逻辑其入口 decode_wal_record 严格遵循 xlogrecord.h 描述的记录布局逐段解析XLogRecord 固定头 XLogRecordBlockHeader可多个 ├─ 若 BKPBLOCK_HAS_IMAGEXLogRecordBlockImageHeader │ └─ 若 HAS_HOLE 且压缩XLogRecordBlockCompressHeader ├─ 若未设 BKPBLOCK_SAME_RELRelFileNode └─ 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/XlParameterChangev15复用 v14 的定义v16重新定义了字段有变化的XlHeapDelete/XlHeapUpdate/XlHeapLock并新增rm_neon子模块PG16 起引入 Neon 自定义 RMGRRM_NEON_ID134来承载 Neon 风格 WAL见 pg_constants.rsv17新增XlEndOfRecovery解码其余复用 v14/v16。版本差异是真实的例如XlClogTruncate的pageno在 PG17 之前是 u32PG17 起是 u64walrecord.rsDecodedWALRecord::is_dbase_create_copy也按版本区分 PG14 的XLOG_DBASE_CREATE与 PG15 的XLOG_DBASE_CREATE_FILE_COPY。8.2 事务记录解析 XlXactParsedRecordXlXactParsedRecord::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_recorddescribe_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/size1024 # 列出可用用例 cargo bench --package postgres_ffi --benches -- --list # 生成火焰图profiling 10 秒输出 target/criterion/*/profile/flamegraph.svg cargo bench --package postgres_ffi --bench waldecoder complete_record/size1024 -- --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消化 WALdecode_wal_record、WalStreamDecoder、维护 pg_control 与 checkpointgenerate_pg_control、解析关系文件parse_relfilename、识别 Neon 自定义 RMGR 记录RM_NEON_IDsafekeeper接收、存储与转发 WAL 段XLogFileName、IsPartialXLogFileName、WalStreamDecoder并生成空 WAL 段供 compute 启动generate_wal_segmentcompute 节点 basebackupfind_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),仅供参考
返回列表