万物皆可KV(4)Neon PageServer 存储布局分析

发布时间:2026/7/29 18:54:49
万物皆可KV(4)Neon PageServer 存储布局分析 1. 结论Pageserver 的 Key 不是 SQL 主键它用tablespace/database/relation/fork/block等物理地址形成 18 字节 Key用 LSN 形成版本维度Value 保存完整页 Image 或可重放 WAL delta。表名、列、SQL 类型、约束和 PostgreSQL 索引结构都仍在 PostgreSQL 页内不形成独立 Pageserver KV。2. 总体 Key/Value 布局租户 / shard / timeline ← 位于目录、对象名和 layer summary不进入 18B Key └─ Pageserver 版本化 Key/Value ├─ Schema / 物理元数据 │ ├─ 00 DB directory / relmap / relation directory / relation size │ ├─ 01 SLRU directory / segment size │ ├─ 02 pg_twophase directory │ ├─ 03 control / checkpoint / 旧 AUX v1 │ └─ 60..7E 稀疏元数据 │ ├─ 62 AUX v2 hash Key - 路径/文件 Value │ ├─ 63 replication origin origin Key - LSN Value │ └─ 65 relation existence v2 relation Key - r/empty └─ Data ├─ 00 relation fork block Key - 8KiB Image 或 WAL delta ├─ 01 pg_xact / pg_multixact block Key - 8KiB Image 或专用 delta └─ 02 pg_twophase file Key - 原始文件 Image 写入 (18B Key, 8B LSN, Value) - Value::ser 追加到 ephemeral file - 内存 BTreeMapCompactKey, LSN - offset - freeze - delta layer: B-tree(18B Key | 8B LSN), BlobRef serialized Value blob 读取 get(Key, requested LSN) - 从新到旧遍历 delta/image layers - 收集 WAL delta直到 Image 或 will_init delta - 按 LSN 正序 redo - 返回该 Key 在 requested LSN 的完整 Bytes 压实 多个历史 Value - reconstruct - image layer image layer: B-tree18B Key, blob offsetLSN 位于 layer headerValue 是原始完整 Image2.1 公共逻辑 Key所有实际 layer 索引中的逻辑 Key 都是 18 字节Key field1:u8 | field2:u32 BE | field3:u32 BE | field4:u32 BE | field5:u8 | field6:u32 BEKey 规则源码事实总宽度固定 18B无分隔符、长度前缀或转义。排序字段顺序与 18B 大端字节序一致无符号数值按字节比较即可保持升序。版本逻辑 Key 本身不含版本。Delta layer 的物理索引 Key 追加LSN:u64 BE成为 26BImage layer 把单一 LSN 放在 layer header。metadata Key稀疏 metadata 编码器先构造 16B首字节 15B payload转为通用 Key 时field2高 16 位补零最终 layer 索引仍写 18B。CompactKey内存使用 128 位整数field2只保留低 16 位。因此正常生产 relation 依赖内置 tablespace OID 可表示用户自定义 tablespace 不受支持。空间边界0x00..0x03为 dense 对象0x60..0x7E被当作 sparse metadata可容忍 Key 缺失。NULL/collation/降序不适用。Key 字段都是物理标识SQL NULL、collation 和索引升降序留在 PostgreSQL 页内部。2.2 layer 中真实的 Key/Value 关系层B-tree KeyB-tree Valueblob ValueLSNDelta layerKey(18B) | LSN(BE u64)共 26B5B 大端BlobRef逻辑值为blob_offset 1 | will_init完整Value::ser1B/4B blob 长度头不压缩在 B-tree Key 中Image layerKey(18B)5B 大端 blob offset重建后的原始 Image Bytes不带Value::Image枚举标签1B/4B blob 长度头可 Zstd在 layer summary 中B-tree 使用 8KiB 页。节点头是child_count:u16 BE | level:u8 | prefix_len:u8 | suffix_len:u8后接公共 Key 前缀、定长 Key 后缀数组和每项 5B Value。写入必须严格递增页满时可提取页内公共前缀。blob 长度头payload_len 128: 0LLLLLLL payload_len 128: 1CCCllll llllllll llllllll llllllll CCC000未压缩 CCC001Zstd 最大 payload 长度0x0FFF_FFFF当前 layer blob 和 B-tree 节点没有独立 checksum 字段完整性主要由文件、summary 一致性检查及远端对象层承担。2.3 逻辑 Value envelopeDelta layer 使用大端、定宽整数的 bincode/Serde 编码Value::Image(bytes) variant:u32 BE 0 | len:u64 BE | bytes Value::WalRecord(record) variant:u32 BE 1 | NeonWalRecord variant:u32 BE | variant fieldsValue 没有独立格式版本字段。枚举顺序和字段序列属于永久格式兼容面layer 外层另有 storage format version。Image layer 不再保存上述 envelope只保存重建后的 Image payload。3. 真实 Key/Value 示例以下 5 组字节由临时 codec probe 直接调用当前生产 Key 构造器、AUX 编码器并直接编译当前Value/NeonWalRecord定义后生成统一选择LSN0/100即数值0x100。Delta index Key是真实持久 delta B-tree 的完整 26B KeyDelta Value是 blob 长度头之后的完整 Value payload。3.1 初始化空 DB directory逻辑输入新 timeline尚无数据库目录项LSN0/100。写入形式put(DBDIR_KEY, Value::Image(BeSer(DbDirectory{dbdirs:{}})))Key定位整个 PGDATA 的数据库目录可读分段00/00000000/00000000/00000000/00/00000000 逻辑 Key 转义/Display 000000000000000000000000000000000000 逻辑 Key Hex 000000000000000000000000000000000000 Delta index Key Hex逻辑 Key | LSN 0000000000000000000000000000000000000000000000000100Value保存空目录集合内层 Image 解码HashMap(spcnode, dbnode), relmap_exists元素数 0 内层 Image Hex0000000000000000 Delta Value 长度20B Delta Value Hex 0000000000000000000000080000000000000000 分段 00000000 - Value::Image tag 0000000000000008 - Image 长度 8 0000000000000000 - HashMap 元素数 0 delta blob 完整前缀14payload 长度 20| 上述 20B编码与使用Key 固定 DBDIR 地址 - 18B BE Key - 追加 LSN 得 26B delta Key Value空 HashMap - BE bincode 8B - Value::Image envelope 写入 commit - put_batch - ephemeral file - delta layer 读取 get(DBDIR_KEY, LSN) - Value::des - Image - DbDirectory::des 回表 不需要这是 Pageserver 自己的物理目录元数据3.2 relation 16384 main fork 的大小为 42 页逻辑输入默认 tablespace1663、database OID5、relnode16384、main fork0大小42页LSN0/100。写入形式put(rel_size_key, Value::Image(42u32.to_le_bytes()))Key定位 relation fork size可读分段00/0000067F/00000005/00004000/00/FFFFFFFF 逻辑 Key 转义/Display 000000067F000000050000400000FFFFFFFF 逻辑 Key Hex 000000067F000000050000400000FFFFFFFF Delta index Key Hex 000000067F000000050000400000FFFFFFFF0000000000000100Value保存页数内层 Image 解码42 内层 Image Hex2A000000 Delta Value 长度16B Delta Value Hex 0000000000000000000000042A000000 分段 00000000 - Value::Image tag 0000000000000004 - Image 长度 4 2A000000 - u32 little-endian 42 delta blob 完整前缀10payload 长度 16| 上述 16B编码与使用Key spc/db/rel/fork FFFFFFFF size sentinel - 18B BE - 追加 LSN Valuenblocks42 - u32 LE - Value::Image 写入 relation create/extend/truncate 与受影响页在同一 modification 中维护 读取 get(size Key, LSN) - Image - get_u32_le 回表 读取 relation page 前先查 size越过 EOF 返回零页3.3 relation block 7 的零页 Image逻辑输入同一 relation 的 block7写入 8192 个零字节LSN0/100。这是 relation 扩展产生 gap 时当前生产路径实际使用的 Image。写入形式put(rel_block_key, Value::Image(ZERO_PAGE))Key定位一个 PostgreSQL 物理页可读分段00/0000067F/00000005/00004000/00/00000007 逻辑 Key 转义/Display 000000067F00000005000040000000000007 逻辑 Key Hex 000000067F00000005000040000000000007 Delta index Key Hex 000000067F000000050000400000000000070000000000000100Value保存完整 8KiB 页内层 Image8192B全 00 Delta Value 总长度8204B Delta Value Hex真实首尾中间 8172B 00 省略 首 32B 0000000000000000000020000000000000000000000000000000000000000000 ... 尾 32B 0000000000000000000000000000000000000000000000000000000000000000 分段 00000000 - Value::Image tag 0000000000002000 - Image 长度 8192 00 × 8192 - PostgreSQL 页 delta blob 4B 头8000200Cpayload 长度 8204未压缩 物理 blob 总长度8208B编码与使用Key relation 物理地址 blkno 7 - 18B BE - 追加 LSN ValueZERO_PAGE 8192B - Value::Image envelope 写入 put_rel_page_image_zero - serialized batch - delta layer 读取 get(Key, requested LSN) 命中 Image 后直接返回无 WAL redo 回表 不需要它本身就是 PostgreSQL heap/index/catalog/TOAST 等 relation 的物理页3.4 visibility map 页的 ClearVisibilityMapFlags delta逻辑输入同一 relation 的 VM fork2、block3清除 heap block0x11对应位旧 heap block 为空flags0x03LSN0/100。写入形式put(vm_page_key, Value::WalRecord(ClearVisibilityMapFlags{...}))Key定位 visibility map 物理页可读分段00/0000067F/00000005/00004000/02/00000003 逻辑 Key 转义/Display 000000067F00000005000040000200000003 逻辑 Key Hex 000000067F00000005000040000200000003 Delta index Key Hex 000000067F000000050000400002000000030000000000000100Value保存可重放页增量可读解码 Value::WalRecord( ClearVisibilityMapFlags { new_heap_blkno: Some(17), old_heap_blkno: None, flags: 3 } ) Delta Value 长度15B Delta Value Hex 000000010000000101000000110003 分段 00000001 - Value::WalRecord tag 00000001 - ClearVisibilityMapFlags tag 01 00000011 - Some(u32 BE 17) 00 - None 03 - flags delta blob 完整前缀0Fpayload 长度 15| 上述 15B编码与使用Key relation VM fork 地址 - 18B BE - 追加 LSN Value专用 delta 字段 - BE bincode enum 写入 WAL ingest 解析记录 - put_rel_wal_record - delta layer 读取 从目标 LSN 向旧版本收集到 Image/will_init - 按 LSN 正序应用 delta 回表 不需要 SQL 回表redo 需要该 VM Key 的旧 Image且会按字段修改页3.5 AUX 文件pg_logical/mappings/test1逻辑输入路径pg_logical/mappings/test1文件内容abcLSN0/100。写入形式put(aux_hash_key, Value::Image(aux_v1_bucket))Key定位 AUX hash bucket16B metadata 输入 62 01 01 7F8B83D94F7081693471ABF91C 可读分段 62/00000101/7F8B83D9/4F708169/34/71ABF91C 逻辑 Key 转义/Display 62000001017F8B83D94F7081693471ABF91C 逻辑 Key Hex通用 18B 形式 62000001017F8B83D94F7081693471ABF91C Delta index Key Hex 62000001017F8B83D94F7081693471ABF91C0000000000000100 分段 62 - AUX v2 prefix 0000 - 16B metadata Key 展开到 18B 时的 field2 高位补零 01 01 - pg_logical / mappings 分类 7F8B...F91C - FNV-1a-128(test1) 的低 104 位Value保存完整路径以处理 hash bucket 与碰撞可读解码[(pg_logical/mappings/test1, babc)] 内层 AUX payload 长度37B Delta Value 长度49B Delta Value Hex 000000000000000000000025010000001970675F6C6F676963616C2F6D617070696E67732F746573743100000003616263 分段 00000000 - Value::Image tag 0000000000000025 - Image 长度 37 01 - AUX value format version 1 00000019 - path 长度 25 7067...7431 - UTF-8 path 00000003 - content 长度 3 616263 - abc delta blob 完整前缀31payload 长度 49| 上述 49B编码与使用Key 路径分类 文件名 hash - 16B metadata Key - 通用 18B Key - 追加 LSN Valueversion 完整 path/content - Value::Image 写入 先 get(bucket)按完整 path 更新/删除再整体重写该 bucket 读取 scan([62..63), LSN) - 每个 Value 解码 - 汇总完整路径 回表 不需要主定位依赖 Key碰撞消歧依赖 Value 中完整 path当前源码不存在独立 SQL 主键、唯一索引或非唯一二级索引 KV因此没有伪造相应示例。PostgreSQL 的 B-tree/GIN/GiST 等索引页与示例 3 使用完全相同的 relation block Key/Value 格式。4. Schema 如何保存4.1 SQL Schema 与 Pageserver 物理元数据的边界逻辑对象持久化状态KeyValue创建/修改/删除tenant / shard / timeline不进入用户数据 KV不适用位于目录、远端对象路径和 layer summary控制面 metadata/index JSON不是本报告的 Value由 Pageserver 控制面维护PostgreSQL database 目录独立 KV固定 DB directory Key整体 BE bincode map(spcnode,dbnode)-relmap_exists新 DB 或 relmap 首次出现重写整个 mapdrop 删除 map 项SQL schema/namespace嵌入其他 Valuepg_namespace 等 catalog relation 的 block Key原始 catalog 页 Image/WAL deltaDDL 作为 catalog 页 WAL 更新table / materialized view / sequence / TOAST relation物理 relation 独立成多组 KV定义嵌入 catalog 页spcnode/dbnode/relnode/fork/blockrelation page Image/WAL delta名字、owner 等定义在 catalog 页create/drop 改 relation directory、size 与 catalog pagescolumn、逻辑类型、nullable、default、generated嵌入 catalog/heap Value不形成字段级 Keypg_attribute、pg_type、pg_attrdef 等 relation 页字段变化只重写受影响页不形成 Pageserver schema Valueprimary key、unique/check/foreign key、index definition、predicate/include columns嵌入 catalog Value不形成定义级 Keypg_constraint、pg_index 等 catalog 页DDL 由 catalog 页 WAL 表示普通/唯一/表达式/部分/覆盖/全文/空间索引索引主体是普通 relation block KVKey 不含索引列或唯一性只含索引 relation 的 relnode/fork/blockPostgreSQL 索引页原始 bytesPostgreSQL 维护页Pageserver 不判重、不解析索引项relation directory v1独立 KV默认每个(spcnode,dbnode)一个 Key整体HashSet(relnode,forknum)create/drop 会重写整个集合relation existence v2可选独立稀疏 KV每个(spc,db,rel,fork)一个0x65Keyr表示存在空 Value 表示 removed可选迁移时逐 relation 建 Keydrop 写空 Imagerelation size独立版本化 KVrelation block 空间末尾blknumFFFFFFFF4B LEnblockscreate/extend/truncate 写新版本relmap (pg_filenode.map)独立 KV(spc,db,0,0,0)文件原始 bytesput 重写完整 Valuedrop 当前未实现sequence/identity 当前值嵌入 relation pagesequence relation block KeyPostgreSQL sequence 页与普通 relation 页相同权限、函数、视图定义、统计信息嵌入 catalog relation pages部分运行统计可在 AUX无对象级 Keycatalog 页或 AUX 文件 bytes由对应 WAL/AUX 更新object/OID allocator无 Pageserver allocator不适用PG control/catalog/SLRU 页中的状态由 PostgreSQL 产生并通过页/WAL 持久化partition、placement、TTL、DDL job没有 Pageserver 独立 schema KV不适用PostgreSQL 自己的 catalog 页Pageserver TTL/DDL job 不适用不适用4.2 目录 Value 的重写粒度源码事实DB directory、relation directory v1、SLRU segment directory、two-phase directory 都把整个 map/set 放在一个 Image Value。这些集合内一个元素变化会重新序列化并写入完整新版本。HashMap/HashSet 的非空元素迭代顺序不是 Key 排序协议读取不能依赖 Value 内条目顺序。relation directory v2 把一个 relation fork 拆成一个稀疏 KV避免 v1 单 Value 随 relation 数量增长但会增加 Key 数和 metadata 扫描项。机制推导v1 点查目录只需一个 Key但目录越大单项 DDL 的 Value 重写量越大。v2 的创建/删除只改一个短 Value代价是扫描、layer index 和版本项数量增加。5. Data 如何保存5.1 当前 Key space 全集对象Key 布局Value 布局访问DB directory0000relmap file00spcdbrelation directory v100spcdbrelation block00spcdbrelation size00spcdbSLRU directory01kind0SLRU block01kind1SLRU segment size01kind1two-phase directory0200two-phase file0200control file0300checkpoint0300AUX v1兼容保留0300relation existence v265spcdbAUX v2 bucket62class2Bhash104 的 16B 设计展开为 18Breplication origin63000x61relation-size metadata prefix 和0x64DB-directory metadata prefix 在当前版本只保留常量没有生产 Key 构造或写入。它们不能视为已实现格式。5.2 relation 主数据四种 fork 都显式进入 KeyforknumforkPageserver Value0main8KiB 页 Image 或 PostgreSQL WAL delta1FSM8KiB 页 Image 或 WAL delta2visibility map8KiB 页 Image、PG WAL delta或专用 VM delta3init fork8KiB 页 Image 或 WAL delta分片规则把它保留在 shard 0同一个 relation fork 的 Key 范围是[00|spc|db|rel|fork|00000000, 00|spc|db|rel|(fork1)|00000000)该范围包含所有 block并在末尾包含FFFFFFFFsize Key。普通点查先用 size Key 验证 EOF再取 block Key。Pageserver 不知道该页是 heap tuple、B-tree internal node、GIN posting list、catalog tuple 还是 TOAST chunk。5.3 所有 Value 类型与字段编码族类型/变体Key 中编码Delta Value 中编码NULL/边界/状态Value::Image(Bytes)无类型 tagu32 tag0u64 lenValue::WalRecord(Postgres)无类型 tagValue tag1record tag0ClearVisibilityMapFlags目标 VM 页在 Keyrecord tag1两个Optionu32u8 flagsOption 用 0/1 标记当前生产ClogSetCommitted目标 pg_xact 页在 Keyrecord tag2Vecu32i64 timestampVec 为u64 count BE 元素当前生产ClogSetAborted目标 pg_xact 页在 Keyrecord tag3Vecu32当前生产MultixactOffsetCreate目标 offsets 页在 Keyrecord tag4mid:u32moff:u32MultixactMembersCreate目标 members 页在 Keyrecord tag5moff:u32Vec(xid,status)AuxFile旧固定 AUX v1 Keyrecord tag6String path Option Bytes当前源码没有生产构造者兼容旧 ValueTruncateVisibilityMap目标 VM 页在 Keyrecord tag7两个usize当前生产Test任意测试 Keytesting feature 下的 record tag8非生产relation/SLRU sizesize sentinel 在 KeyImage 内层u32 LE最大u32页数无 NULLreplication origin LSNorigin ID 在 KeyImage 内层u64 BELSN0表示 dropAUX path/content分类/hash 在 KeyImage 内层版本 1、u32 BE长度和 bytes删除最后一项得到空内层 ValueSQL boolean/int/numeric/text/date/JSON/array/UUID/自定义类型等SQL 类型和值都不进入 Pageserver Key作为 PostgreSQL tuple/index page 的不透明 bytesSQL NULL bitmap、排序、collation、TOAST 由 PostgreSQL 页格式负责全部共享 relation-page 编码族这里没有独立 Pageserver SQL 类型注册表也没有“每列 Value codec”。因此不能把 PostgreSQL tuple 内的类型编码冒充为 Pageserver KV Value 格式。5.4 PostgreSQL 索引与 Pageserver 物理索引索引含义KeyValue主记录定位符/唯一性PostgreSQL primary/unique/non-unique B-tree索引 relation 的物理页 Key原始 B-tree 页 Image/WAL deltaTID、排序、NULL 与唯一性都在 PostgreSQL 索引页Pageserver 不解析查询仍由 PostgreSQL 回 heapPostgreSQL hash/GIN/GiST/SP-GiST/BRIN对应索引 relation 的物理页 Key对应 AM 的原始页与上相同PostgreSQL expression/partial/include index定义在 catalog 页主体仍是索引 relation 页原始页predicate/expression/include 不进入 Pageserver KeyPageserver delta layer B-tree18B Key8B LSN5BBlobRefPageserver image layer B-tree18B Key5B blob offset定位该 layer LSN 下的完整 ImageDelta B-tree 以(Key,LSN)字节序范围扫描。读取一个 Key 的目标版本时从Key|LSN下界开始遍历并过滤请求 LSN 区间遍历层次时从新到旧收集Value redo 时再反转为 LSN 正序。5.5 版本、删除、墓碑与分片版本LSN 是 Pageserver 的 MVCC 版本轴不是 SQL transaction ID 后缀。同一 Key/LSN 在进入内存 index 时保留最后一次写入早先追加的 raw bytes 可留在 ephemeral file但不再被索引。Image 或will_inittruedelta 截断对更老版本的依赖。Image layer 把一个 LSN 的完整状态物化降低后续必须重放的历史链长度这是机制性质不代表本文提供了性能基准。删除Dense relation/SLRU/database/two-phase range delete 最终调用的 in-memory tombstone 写入当前是 no-op。逻辑删除依赖 directory/size 新版本让旧 Key 不再可达源码明确承认旧物理存储暂时泄漏。relation directory v2 的Value空 bytes 是 sparse tombstoner是存在标记。replication origin drop 的Value是 8B 全零LSN::INVALID读取扫描跳过它。祖先 detach 路径还可能产生 sparse 空 Image。AUX 删除会重写 bucketbucket 无文件时内层Value为空。Key 仍可存在不等同 SQL NULL。没有用户数据 TTL。分片与 namespaceTenant/shard/timeline 不重复编码在 18B Key而是由本地路径、远端对象路径、layer 文件名及 summary 隔离。relation 普通页按hash(relnode, block/stripe_size)分布默认 stripe 是 2048 个 8KiB 页即 16MiB。init fork 和非 relation page 的特殊对象落在 shard 0relation size 等部分 metadata 可全 shard 保留。sharding 改变 Key 放在哪个 layer 集合不改变 18B Key 或 Value bytes。6. KV 数据库的启示可借鉴设计适用访问模式直接收益生效前提与代价固定宽度、BE 复合 Key物理对象点查、连续 block 扫描字节序即字段数值序范围上下界简单字段预算固定tablespace 压缩限制扩展性Key | LSNdelta 索引多版本 point lookup、PITR同一 Key 的版本相邻且按时间排序历史层多时读放大需要 compaction/imageImage redo delta 双 Value页级 WAL 系统可以选择写完整页或小 deltawill_init 明确历史截断点读取可能需要多条 delta 和 WAL redoLSN 移到 image layer header同一快照覆盖很多 Keyimage index Key 从 26B 降到 18B一个 layer 只能代表一个 materialization LSNrelation size 放在 block range 末尾extend/truncate 与尾页访问同一 fork 的 block 与 size Key 相邻、单范围覆盖FFFFFFFF被保留不能作为普通 block稀疏 metadata Key space可选对象、扫描目录missing/empty 可自然表示不存在empty Value 语义必须按对象区分扫描仅限 metadatahash Key 完整路径 Value路径较长、需要固定 KeyKey 固定 18BValue 可处理 hash collision更新 collision bucket 要读改写整个 Value不支持按任意路径字典序扫描immutable layer 内嵌 B-tree本地文件与对象存储共用格式layer 可原样上传不依赖外部 KV 引擎compaction、GC、下载和多 layer 选择由 Pageserver 自己承担物理页作为 Value托管完整 PostgreSQL 语义无需复制 SQL 类型/索引 codec能覆盖扩展类型无法用 Pageserver Key 做 SQL 行级/索引值级查询或局部列更新小目录整体 Value / 大目录拆 Key元数据规模分层可在点查次数与单项重写量之间选择需要迁移状态和双写/双读兼容逻辑