
Neon Pageserver 的 Vectored Timeline Get批量读取重构协议解析与源码实现【免费下载链接】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/neonNeon 将计算与存储分离后Pageserver 的核心读路径是Timeline::get——按单个(Key, Lsn)定位并重建一个 PostgreSQL 页面。本文以 RFC 030Vectored Timeline Get为骨架梳理该特性的动机、API 设计、落地过程及其与 Layer Map、DiskBtree、VirtualFile、分片Sharding的关系并结合当前仓库源码验证其最终实现形态。读完你将掌握为什么相邻键的逐点查询低效、get_vectored如何用一次调用替代 N 次get、以及它在 basebackup / page_service / compaction 等场景下的真实用法。背景Timeline::get与 basebackup 的性能痛点在 Neon 的分层存储架构中一个时间线Timeline由一层层 L0/L1 delta 层与 image 层堆叠而成Timeline::get(key, lsn)是重建任一页面版本的入口。重建过程大致是从 Layer Map 自顶向下遍历各层收集重构数据reconstruct data对每个被访问的层向DiskBtree发起一次点查询point lookup命中则把结果装入 reconstruct data bag若已足以重建页面则进入 walredo 阶段否则继续向更底层遍历。RFC 030 指出basebackup 期间会对在键空间上彼此相邻的 SLRU 页面发起大量Timeline::get调用见 basebackup.rs 中 SLRU 段的读取逻辑。对 N 个相邻键做 N 次独立get会带来三类浪费Layer Map 遍历被重复执行即使所有数据都落在栈底同一张 image 层里每调用一次get仍要从头走一遍层遍历DiskBtree 内部页被反复访问跨全键空间的 L0 层必须被层遍历访问但很可能并不包含目标数据多个键的点查询还会重复读取同一批 Btree 内部页物理相邻的读被打散成 N 次 IO经验观测表明键空间相邻、同时写入的键在层文件中往往也物理相邻RFC 脚注引用了 2023-12 的 basebackup 缓慢问题调查。理想情况下为 N 个相邻键提供重构数据只需要一次大的文件系统读而文件系统若能连续存放层文件这次大读就会合并为对磁盘的一次 IOP。简而言之现有 API 是一页一查而 basebackup 的访问模式是一段一查两者失配。解决方案Timeline::get_vectored的 API 设计RFC 提出的核心是新增一个 vectored批量 / scatter-gather 式版本Timeline::get_vectored签名如下struct KeyVec { base: Key, count: usize, } async fn get_vectored(lsn: Lsn, src: [KeyVec]) - ResultVecBytes, ...;其中每个KeyVec { base, count }表示从base开始的count个连续键lsn统一应用于整批查询。RFC 给出了函数语义上等价的最小实现let mut keys_iter: impl IteratorItem Key src.map(|KeyVec { base, count }| (base..base count)).flatten(); let mut out Vec::new(); for key in keys_iter { let data Timeline::get(key, lsn)?; out.push(data); } out但这只是正确性基准。RFC 明确指出理想实现应满足三条硬约束每个struct Layer至多访问一次对每个被访问的层Layer::get_value_reconstruct_data至多调用一次即每张DiskBtree页至多读一次为合并发往 OS / NVMe 的读创造条件。在真实仓库中这个 API 经过演化后落地为更通用的形态get_vectored不再直接接收lsn [KeyVec]而是接收一个VersionedKeySpaceQuery键空间 LSN 的封装和一个IoConcurrency返回BTreeMapKey, ResultBytes, PageReconstructError见 timeline.rs。KeyVec的连续键段概念则由KeySpace/VersionedKeySpaceQuery承接。落地现状从 RFC 到仓库源码1.Timeline::get已是get_vectored的特例RFC 的 Rollout 章节提出本 epic 结束时Timeline::get将转发到Timeline::get_vectored。这一目标在仓库中已经实现现在的 Timeline::get 内部构造一个只含单键的KeySpace再调用get_vectored_impl最后从返回的BTreeMap中取出唯一键值——单点查询与批量查询共用同一条读路径这正是 RFC 要求的单页 vectored get 的基础性能应与原get一致。let mut reconstruct_state ValuesReconstructState::new(IoConcurrency::sequential()); let query VersionedKeySpaceQuery::uniform(KeySpace::single(key..key.next()), lsn); let vectored_res self.get_vectored_impl(query, mut reconstruct_state, ctx).await; let key_value vectored_res?.pop_first();2. 批量的 reconstruct data 状态ValuesReconstructStateRFC 提出要把get_reconstruct_data重构为持有并返回 N 个键的状态。仓库中对应实现是 storage_layer.rs 的ValuesReconstructStatekeys: HashMapKey, VectoredValueReconstructState——本批待检索的键及其重构进度keys_done: KeySpaceRandomAccum——已完成检索的键集合keys_with_image_coverage: OptionRangeKey——image 层覆盖的键范围layers_visited/delta_layers_visited——本次查询访问的层数与 delta 层数统计io_concurrency与num_active_ios——控制读 IO 的并发调度。在 get_vectored_impl 的循环中逐层检查keys把命中的键移入keys_done直到所有键都拿到足够数据或层栈耗尽。3. 层遍历的向量化与cont_lsn的处理RFC 特别点名了重构中最微妙的问题get_reconstruct_data在找到部分键的数据后需要记住cont_lsn继续遍历的起点 LSN批量版本必须按需保留各键自己的cont_lsn并在下一轮迭代时取仍缺数据的所有键的cont_lsn最大值继续。从当前代码结构看ValuesReconstructState以HashMapKey, VectoredValueReconstructState逐键追踪状态这一按键维护进度、整批推进层遍历的模型正是 RFC 设想的形态。4. DiskBtree 的向量化访问RFC 对DiskBtree::visit提出两个层次的目标最小方案让visit接收一个连续的键范围单个KeyVec在其范围内对所有值回调这对 basebackup 的连续键段场景已经够用理想方案接收[KeyVec]并排序Btree 遍历过程中窥视下一个键段以决定是下降还是回溯。当前 disk_btree.rs 的visit仍以单个search_key为起点、配合方向参数VisitDirection遍历底层通过OnDiskNode::binary_search定位首个匹配项后沿兄弟节点游走delta 层用它收集(offset, len)对以定位 delta 记录 blobimage 层则用 DiskBtree::get 直接取 image blob 偏移其内部同样是visit调用。RFC 所设想的多键段排序 窥视式下降/回溯是对这套代码的自然扩展。5. 读合并blob_io 与 VirtualFileRFC 指出DiskBtree::visit产出一批文件偏移随后要从VirtualFile中读取delta 层读路径依赖PageCache与VirtualFileimage 层读路径在 image_layer.rs 中。要真正把 N 次小读合并成一次大读需要向量化blob_io接口再向量化VirtualFileAPI。RFC 同时给出两条工程警示VirtualFile正处于 io_uring 改造的活跃期接口变动的耦合风险高缓冲区管理方面指导原则是不把PageCache视为缓冲管理的必要组成部分而应把它当作 IO 链路上可有可无的一跳避免此工作与PageCache的未来走向强耦合。从当前仓库看读路径的并发已由 IoConcurrency 抽象Sequential顺序 IO临时兜底方案或SidecarTask把 IO future 投递给一个独立的 sidecar 任务用FuturesUnordered并发执行。这正是以并发换取批量读延迟的落地体现。运维与配置与get_vectored相关的参数与监控该特性无需 feature flagRFC 明言但引入了两个影响读路径行为的配置项见 config.rs配置项默认类型作用max_get_vectored_keysMaxGetVectoredKeys单次get_vectored调用允许的最大键数超限返回GetVectoredError::Oversizedget_vectored_concurrent_ioGetVectoredConcurrentIo读路径并发模型Sequential或SidecarTask决定IoConcurrency的构造max_vectored_read_bytesMaxVectoredReadBytes批量读的字节上限如 basebackup 分段时按max_get_vectored_keys * BLCKSZ划分监控方面仓库在 metrics.rs 注册了pageserver_get_vectored_seconds直方图按task_kind如 basebackup、compaction、page_service分别统计get_vectored耗时便于定位各调用方的批量读延迟。主要调用方批量读在仓库中的真实使用RFC 提出的三大受益场景basebackup、compaction、page_service在仓库中均有对应实现basebackupSLRU 非惰性下载时先把整个 SLRU 键空间按max_get_vectored_keys * BLCKSZ字节分片再对每个分片调用timeline.get_vectored(query, io_concurrency, ctx)逐块喂给SlruSegmentsBuilder拼装段文件见 basebackup.rs。这正是 RFC 动机章节描述的为 SLRU 相邻页批量取数场景pgdatadir_mapping向 compute 提供页面时把请求的键范围构造为VersionedKeySpaceQuery后批量取页pgdatadir_mapping.rs并在批内逐键处理错误其中还保留了把get_vectored的错误改为按键粒度返回的 TODOpage_serviceget_vectored目前强制每批上限 32 个键page_service.rs 附近注释为将来向 compute 暴露 vectoredget_page_at_lsn利于 seqscan / prefetch铺路并复用同一套IoConcurrency并发模型compaction / 元数据扫描scan接口基于get_vectored_impl扫描整个元数据键空间语义接近 RocksDB 的 scan iterator存在即返回、缺失不报错见 timeline.rsdetach_ancestor在剥离祖先时间线时直接调用get_vectored_impl绕过max_get_vectored_keys上限检查detach_ancestor.rs。这些调用方共同验证了 RFC 的判断批量读的价值不仅限于 basebackup而是贯穿 Pageserver 读路径的通用能力。与分片Sharding的交互RFC 专门讨论了与 sharding 的边界分片把键空间按key_to_shard_number切分与Timeline::get一样get_vectored的调用方必须只查询属于当前Timeline分片的键。仓库实现中这一约束以断言/校验落实get_vectored会对查询键空间内的每个键执行assert!(!self.shard_identity.is_key_disposable(key))timeline.rsTimeline::get也保留了debug_assert!形式的双重校验。RFC 同时给出一个前瞻性警告上层代码对键空间 分片的约束感知很弱例如KeySpace并未按分片条带拆分——若有人把 compaction 代码朴素地改成对整段键空间发起 vectored get就会违反分片约束。因此这类断言在未来很长一段时间内都值得保留。演进路径与工程节奏RFC 给出的落地顺序是先做前三项、再回头看先明确 API返回类型VecBytes与impl Stream之间的取舍、与 peers 迭代再做向量的 Layer Map 遍历与get_reconstruct_data状态重构接着向量化Layer::get_value_reconstruct_data/DiskBtree最后才触碰 IO 合并这一最难的部分因为受 io_uring 与PageCache变局影响。工程节奏上RFC 明确要求以多个小 PR 分周交付、不做 feature flag、一次性切换以便在每周发布中隔离性能回归。这一演进路径在仓库中清晰可见核心的get_vectored_impl、ValuesReconstructState、IoConcurrency已就位而DiskBtree::visit的多键段化、blob_io/VirtualFile的向量化仍留有演进空间page_service中向 compute 暴露批量取页的 TODO 也仍然开放——这正是 RFC 作为长期 epic 的中间态。小结RFC 030 用一份紧凑的协议为 Neon Pageserver 定义了批量读路径的蓝图以get_vectored统一单点与批量查询把逐页遍历层栈 逐点查 Btree N 次文件读压缩为一次层遍历 每层至多一次 Btree 访问 尽量合并的底层读。当前仓库中Timeline::get已退化为get_vectored的单键特例basebackup、元数据扫描、剥离祖先时间线等路径均已切换到批量读且全程不依赖 feature flag——它以配置项max_get_vectored_keys、get_vectored_concurrent_io和直方图指标pageserver_get_vectored_seconds融入生产运维体系。若要深入源码建议从 timeline.rs 的get/get_vectored/scan三连读起再对照 storage_layer.rs 的ValuesReconstructState与IoConcurrency即可还原 RFC 设想的完整读路径。【免费下载链接】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),仅供参考