Neon AUX File v2:基于稀疏 Keyspace 的辅助文件存储架构(RFC 038 深度解读)
Neon AUX File v2基于稀疏 Keyspace 的辅助文件存储架构RFC 038 深度解读【免费下载链接】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/neonAUX 文件aux file是 Neon 中除 Postgres 关系页之外的零散小文件如pg_logical、pg_replslot、pg_stat等目录下的文件它们随 WAL 一起由计算节点compute node流式写入 pageserver。本文以仓库内 RFC 038AUX file v2 为主线结合 pageserver 与 neon 扩展源码完整剖析 v2 存储策略如何通过稀疏 keyspace 单文件单 Key的编码方式彻底解决 v1 策略的空间与写入放大问题读完你将掌握其 Key 编码规则、目录前缀分配、读写路径、压缩策略以及从 v1 到 v2 的迁移方案并能直接对照源码定位每一处实现。一、背景v1 AUX 文件存储策略的痛点在 v2 之前AUX 文件被存放在一个复合键AUX_FILES_KEY中。其工作方式是每次计算节点向 pageserver 流式发送neon-file记录时先更新内存中的 aux 文件哈希表再把整个序列化后的哈希表整体写回该键。这种全量覆盖写带来两个直接问题空间放大space bloat同一个复合键下反复保存完整快照历史版本堆积导致存储空间急剧膨胀写放大write amplification每新增或修改一个几 KB 的小文件都要重写包含所有 aux 文件的整个哈希表。随后 v1 曾做过一次改进——改为向 aux 文件键写入增量记录delta record即只更新哈希表中的某个 Key。这样 pageserver 在每个 LSN 上只保存增量缓解了空间放大。但正如 RFC 指出的改进后的 v1 仍然要求把全部 aux 文件缓存在内存中原因在于无法从复合的AUX_FILES_KEY中单独取出某一个 Key某一个文件。也就是说v1 始终逃不开所有文件捆在一起的建模方式。RFC 给出了先例思路对于大量小文件的存储可以使用 key-value store其中key 是文件名value 是文件内容——这正是 v2 的雏形即把每个 aux 文件映射为 keyspace 中的独立 Key。二、需求定义与设计原则RFC 为 v2 设定了两条硬性需求需求说明无空间膨胀空间放大固定且有界fixed space amplification无写入膨胀写入放大固定且有界fixed write amplification与 v1 的全量覆盖/增量修补思路不同v2 采用文件与存储键一一对应的架构每个 aux 文件只写一次、只占一份空间不再有重写整个哈希表的放大路径。受影响的组件仅限pageserver计算节点侧的neon-file协议与扩展接口见 pgxn/neon/README.md 对 file_cache 的说明无需结构性改动。三、核心概念Sparse Keyspace稀疏 Keyspacev2 落地的第一块基石是引入稀疏 keyspace这一新概念它推翻了 pageserver 此前的一条隐含假设。3.1 旧假设Keyspace 总是连续的传统上 pageserver 假定 keyspace 是连续的只要某个 key 范围0x0000-0xFFFF存在那么范围内的每一个键都真实存在于存储中。基于该假设很多代码会通过逐键迭代来遍历 keyspaceloop { // do something key key.next(); }如果 keyspace 非常大例如包含2^64个键这种循环将耗费无限时间。3.2 新语义只取 layer 文件做合并RFC 为稀疏 keyspace 定义了新的遍历规则并非范围内每个键都存在开发者不应逐个迭代 keyspace 中的键正确做法是先获取 key 范围内所有的 layer 文件再对它们做合并merge。在 aux file v2 中所有 aux 文件就存放在以AUX_KEY_PREFIX为前缀的稀疏 keyspace 内。该前缀定义于 libs/pageserver_api/src/key.rs/// The key prefix of AUX file keys. pub const AUX_KEY_PREFIX: u8 0x62; /// The key prefix of ReplOrigin keys. pub const REPL_ORIGIN_KEY_PREFIX: u8 0x63;AUX_KEY_PREFIX0x62与REPL_ORIGIN_KEY_PREFIX0x63相邻二者共同构成稀疏且非继承non-inherited的 keyspace 区域。同文件中的sparse_non_inherited_keyspace()用一个const_assert!(AUX_KEY_PREFIX 1 REPL_ORIGIN_KEY_PREFIX)保证这段区域在字节上是连续的。在 pageserver 内部partitioning会把 keyspace 拆分为 dense稠密与 sparse稀疏两部分参见 pageserver/src/pgdatadir_mapping.rs 中(dense_keyspace, sparse_keyspace)的返回值以及 pageserver/src/tenant/timeline/compaction.rs 中dense_partitioning/sparse_partitioning的分离处理。四、AUX v2 Keyspace 与 Key 映射4.1 编码规则前缀 目录号 FNV 哈希pageserver 使用定长 Key。为了把任意文件名的文件塞进 keyspacev2 采取预定前缀 文件名哈希的两段式编码前缀部分根据存放 aux 文件的目录分配预定前缀剩余位用文件名或路径剩余部分的 FNV 哈希填充。编码函数是 pageserver/src/aux_file.rs 中的encode_aux_file_key。RFC 给出的例子pg_logical/mappings/test1编码结果如下18B 全 Key 表示62 0000 01 01 7F8B83D94F7081693471ABF91C ^ aux prefix ^ assigned prefix of pg_logical/ ^ assigned prefix of mappings/ ^ 13B FNV hash of test1 ^ not used due to key representation对应到aux_hash_to_metadata_key的实现pageserver/src/aux_file.rs16B compact Key 的实际排布是key[0] AUX_KEY_PREFIX0x62key[1] dir_level1一级目录号如pg_logical→0x01key[2] dir_level2二级目录号如mappings→0x01key[3..16] fnv_hash(文件名).to_be_bytes()[3..16]FNV 哈希的低 13 字节。4.2 18B 全 Key 与 16B Compact Key 两种表示RFC 特别提醒pageserver 内部存在两种 Key 表示——18B 全 Key 表示与 16B compact紧凑表示。18B 表示的某些字段取值范围受限因此aux Key 只使用 16B compact 部分示例中的0000两个字节在 18B 表示下未使用正是这一表示差异的体现。4.3 目录前缀分配表新的 aux 文件类型加入存储时必须先在 pageserver/src/aux_file.rs 中为对应目录分配前缀。当前分配如下源码注释中的完整映射路径两字节前缀说明pg_logical/mappings/0x01 0x01逻辑复制映射文件pg_logical/snapshots/0x01 0x02逻辑复制快照文件pg_logical/replorigin_checkpoint0x01 0x03特殊文件哈希空串pg_logical/其他0x01 0xFF未支持的 pg_logical 文件pg_replslot/0x02 0x01复制槽文件pg_stat/pgstat.stat0x03 0x01统计文件其他所有未分配目录0xFF 0xFF兜底 keyspace需要注意两条硬性约束源码注释中明确强调新增文件类型必须从没写入过存储。否则它可能曾落在others0xFFFF前缀下新增前缀后会导致数据错位、损坏新增文件类型必须配套添加test_encoding_portable测试用例保证编码跨版本、跨平台稳定。在 debug 构建下遇到未支持的路径类型会打印告警提示其落入0x01FF或0xFFFF前缀并影响路径扫描。4.4 FNV 哈希的跨平台保证fnv_hash是 128 位 FNV-1a 的 const 实现pageserver/src/aux_file.rs初始状态与素数均为常量确保同一文件名在所有平台、所有 pageserver 版本上产生完全相同的哈希。test_hash_portable与test_encoding_portable两个测试用例pageserver/src/aux_file.rs直接断言了哈希值与编码结果的确定性例如assert_eq!( 62000001017F8B83D94F7081693471ABF91C, encode_aux_file_key(pg_logical/mappings/test1).to_string(), );其中7F8B83D94F7081693471ABF91C正是fnv_hash(test1)的低 13 字节。4.5 哈希碰撞的处理Key 的 value 是一个文件数组任意文件名哈希到 13 字节空间理论上存在碰撞可能两个不同文件映射到同一个 Key。RFC 的处理方式是每个 aux Key 的 value 不再是一个文件而是一个文件名 文件内容的数组。value 的序列化格式定义于encode_file_value/decode_file_valuepageserver/src/aux_file.rs首字节为版本号AUX_FILE_ENCODING_VERSION 0x01随后循环写入u32 文件名长度 文件名 bytes u32 内容长度 内容 bytes空 value零字节表示无文件。pub fn encode_file_value(files: [(str, [u8])]) - anyhow::ResultVecu8 { if files.is_empty() { return Ok(Vec::new()); // no files empty value } let mut encoded vec![]; encoded.put_u8(AUX_FILE_ENCODING_VERSION); for (path, content) in files { encoded.put_u32(path.len() as u32); encoded.put_slice(path.as_bytes()); encoded.put_u32(content.len() as u32); encoded.put_slice(content); } Ok(encoded) }4.6 用 Value::Image 承载 aux Key为了复用已有的页重建机制aux Key 使用Value::Image完整镜像存储。这样页重建page reconstruction的路径与普通数据完全一致——只需从存储中取出最新镜像即可无需为 aux 值编写额外的重建代码。五、入站逻辑复制的 Key 映射入站逻辑复制inbound logical replication场景下Postgres 需要replorigin_checkpoint文件保存复制原点replication origin检查点数据。RFC 明确说明该文件不直接以 aux v2 机制存入 pageserver而是在生成 basebackup 时通过扫描REPL_ORIGIN_KEY_PREFIXkeyspace 现场构造。对应实现中repl_origin_key(origin_id)按origin_id生成0x63前缀的 Keyrepl_origin_key_range()返回整个复制原点 keyspace 范围libs/pageserver_api/src/key.rs。这正是sparse_non_inherited_keyspace()把0x62..0x64整体划为稀疏区域的原因——aux 与 replorigin 两段 keyspace 都不适用逐键迭代的旧假设。六、稀疏 Keyspace 的读路径从 pageserver 读取 aux 文件有两个入口6.1 写路径读-追加-写回计算节点向 pageserver 添加一个 aux 文件时流程为先从存储get该 Key 的现有 value → 把新文件追加进该 Key 的文件数组 → 编码后写回。现有的getAPI 已天然支持这种读取 覆盖模式无需新增接口。6.2 basebackupvectored get 全量扫描生成 basebackup 时需要取回全部aux 文件使用的是vectored get API批量范围读取。由于要扫描的是稀疏 keyspace该路径做了两处针对性修改missing key 不再报错旧版 vectored API 会尝试获取请求范围内每一个 Key遇到缺失即报错。修改后落在NON_INHERITED_SPARSE_RANGE内的 Key 缺失不会触发 missing key 错误不再跟踪ummapped_keyspaceaux 文件读取通常需要该分支内与 key 范围相交的所有 layer 文件覆盖的 keyspace 很大跟踪未读到过的 keyspace开销高昂因此对稀疏 keyspace 明确不做此跟踪该行为由 pageserver 的 pull request #9631 引入。源码层面pageserver/src/tenant/storage_layer.rs 的update_key可以印证这一点当 key 处于稀疏 keyspacekey.is_sparse()且状态已 Complete 时直接返回OnDiskValueIo::Unnecessary并且不把该 key 记入keys_done——因为稀疏 keyspace 可能被多次访问且我们不跟踪未映射区域。在 pageserver/src/basebackup.rsbasebackup 生成时调用list_aux_files扫描全部 aux 文件随后逐一把(path, content)打进 tar 包并统计扫描耗时与内容总大小。七、压缩与 Image Layer 生成稀疏 keyspace 的引入同样要求压缩compaction代码适配范围内并非每个键都存在的事实RFC 给出两处修改7.1 L0 压缩的 hole 计算L0 compaction 的hole computation空洞计算被改造为能正确处理稀疏 keyspace。空洞hole本意是指被更高层覆盖、无需下探的区域稀疏 keyspace 中天然存在大量本来就没有数据的键空洞计算必须把它们与有数据但被覆盖的情况区分开。7.2 Image Layer 创建旧做法是对key.next()逐键调用 get/重建镜像这在稀疏 keyspace 下不可行。新做法使用vectored get API在给定 LSN 上扫描 keyspace 中所有存在的键只有当最新 LSN 与上次生成的 image layer 之间积累了过多 delta layer 时才为稀疏 keyspace 生成 image layer当前实现生成的 image layer总是覆盖完整的 aux key 范围全量覆盖RFC 注明该策略未来可以进一步优化如按需生成局部 image layer。这保证了稀疏 keyspace 的读取不会退化为逐键迭代同时把 image layer 的产生频率限制在 delta 堆积过多的场景兼顾读放大与写放大。八、迁移从 v1 到 v2RFC 明确决定v2 与 v1 互不兼容。曾考虑过新数据写 v2、旧数据留在 v1的无缝迁移方案但该方案会显著复杂化文件删除逻辑删除一个文件要同时检查两套存储。因此团队选择所有用户从存储中无 aux 文件的干净状态开始存量 v1 用户通过独立的aux_v2_migration迁移工具手动迁移。8.1 策略判定与存储迁移期间aux 文件策略aux policy记录在index_part.json中。当 tenant 被 attach 且未设置策略时pageserver 会扫描 aux 文件 keyspace 以识别当前实际使用的策略v1 或 v2。相关字段可见于 pageserver/src/tenant/remote_timeline_client/index.rs 的last_aux_file_policy其注释说明该字段如今已不再使用所有 tenant 均已过渡到新策略侧面印证了迁移的完成。8.2 策略归属规则存有v1 aux 文件的 timeline继续使用 v1 策略直到执行手动迁移新建 timeline默认使用v2策略在 v2 成为默认之前就已启用逻辑复制的用户使用 v1 策略曾尝试配置入站复制当时尚未支持的用户即使未真正加入逻辑复制测试计划也可能在 v1 存储中留下少量文件条目需要同样纳入迁移考量。8.3 迁移工具的执行流程aux_v2_migration工具包对所有启用了逻辑复制的项目执行如下流程扫描全部启用逻辑复制的项目将这些项目的计算节点置于维护模式maintenance mode——即挂起suspend全部 compute调用迁移 API 切换 pageserver 上的 aux 文件策略该操作会丢弃所有复制状态因此必须先挂起 compute重新启动所有计算节点。整个流程对用户的影响被压缩在compute 挂起 重启窗口内且由于逻辑复制状态会在切换时被重置用户需要知晓复制进度会回退到切换点。九、小结RFC 038 的关键技术要点维度v1v2RFC 038存储单元单一复合键AUX_FILES_KEY全量哈希表/增量哈希表每个文件独立 Key0x62前缀 目录前缀 13B FNV 哈希空间放大高历史快照堆积固定、有界写入放大高单文件写入触发整表重写/增量固定、有界内存占用需在内存缓存全部 aux 文件无需整体缓存可按 Key 单取Keyspace 模型假设连续引入稀疏 keyspace禁止逐键迭代读取方式读整个复合键写路径 get 追加basebackup 用 vectored get 扫描压缩逐键重建镜像vectored get 扫描 按 delta 堆积阈值生成全范围 image layer迁移—不兼容aux_v2_migration工具手动迁移策略记录于index_part.jsonRFC 038 以极简的文件名即 Key建模配合稀疏 keyspace 的新遍历语义让 aux 文件存储彻底摆脱了复合键带来的放大问题。读者若想深入验证本文所述细节可直接对照以下源码路径Key 前缀与稀疏区域定义libs/pageserver_api/src/key.rs编码、哈希与 value 序列化实现及测试pageserver/src/aux_file.rsbasebackup 中的 aux 扫描pageserver/src/basebackup.rs稀疏 keyspace 不跟踪 unmapped 区域的实现pageserver/src/tenant/storage_layer.rs稠密/稀疏 partitioning 与压缩处理pageserver/src/tenant/timeline/compaction.rs、pageserver/src/pgdatadir_mapping.rs迁移策略字段pageserver/src/tenant/remote_timeline_client/index.rs【免费下载链接】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),仅供参考