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

资讯详情

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

Neon Pageserver 快照优先存储与 PITR 设计演进:从 Snapshot-first RFC 到 Image/Delta Layer 落地

Neon Pageserver 快照优先存储与 PITR 设计演进:从 Snapshot-first RFC 到 Image/Delta Layer 落地 Neon Pageserver 快照优先存储与 PITR 设计演进从 Snapshot-first RFC 到 Image/Delta Layer 落地【免费下载链接】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导读本文基于 Neon 仓库中《Snapshot-first storage》RFCdocs/rfcs/009-snapshot-first-storage-pitr.md完整梳理了按 LSN 重建任意历史页版本这一核心问题的三种设计方案以快照文件 独立 WAL为主体的 Snapshot-first 方案、当前layered_repo分支采用的快照WAL 合并层实现、以及尚未实现的快照与按关系 WAL 分离的第三方案。读完本文你将理解 GetPageLSN 在只读副本、锚定副本与分支场景下的工作方式PITR时间点恢复的页版本重建原理以及该设计最终如何在今天的 Pageserver 中落地为 LSM 存储中的 Image Layer 与 Delta Layer参见 docs/pageserver-compaction.md、pageserver/src/tenant/storage_layer/image_layer.rs。一、问题背景GetPageLSN 与历史页版本重建Neon 将计算与存储分离计算节点PostgreSQL通过 GetPageLSN 协议向 Pageserver 请求指定 LSN 时刻的页面内容。RFC 开篇Preface明确指出一个基本事实GetPageLSN 可以用更旧的 LSN 来调用Pageserver 必须能够重建更早的页版本。这个能力在以下三类场景中必不可少滞后的只读副本lagging replicas只读副本落后于主库需要读取较旧 LSN 的页面锚定在旧 LSN 的副本anchored replicas副本被固定在某个历史 LSN 上持续对外提供一致读Pageserver 内部的时间点分支在更早的时间点创建分支时需要构造该时刻的数据库镜像。RFC 作者在该文档中刻意排除了**增量快照incremental snapshots**的考虑认为它不会改变问题的本质——即每个快照/快照文件都包含全部页面的完整镜像读取时无需再依赖更早的快照文件。另一个关键设计前提是按关系per-relation组织快照每个快照文件只包含一个关系的数据。这里的关系是一个模糊概念可以是 1 GB 的关系段relation segment可以包含关系的所有 fork如 main、fsm、vm 等也可以把每个 fork 当作独立存储对象待非关系对象non-relational工作完成后关系还可能指 PostgreSQL 数据目录中的其他版本化对象。这一每文件一个关系/键范围的粒度设计直接奠定了今天 Pageserver 中 Layer 文件按Key Range划分的基本形态。二、方案一Eric 的 Snapshot-first 存储RFC 主体2.1 快照与 WAL 的分工RFC 描述的快照机制如下每隔一段时间创建一个快照即为自上次快照以来被修改过的每个关系生成一个新的快照文件写入该关系在快照 LSN 时刻的完整内容。WAL 则由 WAL safekeeping 服务safekeeper以PostgreSQL 原始 WAL 文件格式单独存放在 S3 中。存储布局的抽象示意LSN 自下而上增长SNAPSHOT 100 WAL . | . | . | . | SNAPSHOT 200 | . | . | . | . | SNAPSHOT 300 | . | . V IN-MEMORY 400内存层IN-MEMORY保存 LSN 300 之后的最新修改磁盘上依次存在 LSN 100、200、300 的快照文件WAL 单独存储覆盖从 100 至今的完整区间。2.2 主库读路径内存层优先当主库发来 GetPageLSN 请求时若页面在内存层有踪迹直接返回最新版本若内存中完全没有该页面的记录说明该页面自最近一次快照上图中 LSN 300以来未被修改因此直接返回最近快照中的页面镜像即可无需扫描任何 WAL。这一无修改即免重放的判定是快照优先存储降低读延迟的关键。2.3 PITR 读路径快照 WAL 重放PITR 基于原始 WAL 文件实现。假设来自只读副本的请求为 LSN 250从 LSN 200 的快照文件读取该页面的镜像扫描 200 到 250 之间的 WAL把其中与该页面相关的 WAL 记录全部重放到镜像上得到 LSN 250 时刻的页版本。RFC 作者同时指出了朴素实现的性能瓶颈每次 GetPageLSN 都从头扫描一遍 WAL 区间代价高昂。实际工程中应对的做法是在服务端为 200~250 区间一次性构建一个内存数据结构例如按页面索引的 WAL 记录表之后对该区间内任意页面的请求都能快速定位所需记录。这一思想在后来的实现中演进为 Pageserver 的 Layer 内 B-tree 索引每个 Delta Layer 文件内部都带有一个从(Key, LSN)到数据偏移的 B-tree 索引详见下文第五部分。2.4 RFC 提出的问题与疑问RFC 在正文中坦诚列出了该方案尚待解决的疑问问题 1快照 LSN 列表存储在哪里每个时间线timeline上需要维护一份已发生快照的 LSN 列表否则无法判断某 LSN 处是否发生过快照。问题 2如何避免全区间 WAL 扫描假设某关系最近一次快照在 LSN 100现在请求 LSN 1000000 处的页面。如果不加优化必须扫描 100~1000000 的全部 WAL 才能确认其间是否有对该页面的修改——这显然不可接受。优化思路如果知道系统在 LSN 999900 处发生过一次快照哪怕不是针对这个关系那么该关系没有 999900 的快照文件本身就证明该关系在 100~999900 之间没有被修改过于是只需扫描 999900~1000000 的 WAL。问题 3快照事件的信息从哪里获得既然该关系的快照文件中没有 999900 的踪迹就必须从别处获取发生过快照的信息。RFC 给出了两个候选扫描所有关系对全部关系取快照 LSN 的并集得到全局快照 LSN 集合。若在 LSN 999900 看到任意关系的快照文件则可知若本关系有修改也必然会有更新的快照文件。缺点扫描昂贵至少应在首次计算后缓存在内存中且依赖所有文件按同一间隔快照这一隐含约束无法支持不同文件采用不同快照间隔限制较大显式元数据文件单独维护一个记录全部快照 LSN 的元数据文件。后一种思路在后来的实现中演变为 timeline 的metadata 文件参见 pageserver/src/tenant/metadata.rs 与 timeline 结构用于记录各层信息、GC 水位等时间线级元数据。三、方案二layered_repo分支的当前实现SNAPSHOTWAL 层RFC 的后半部分描述了layered_repo分支中已实现的变体快照与 WAL 合并在同一层文件中这样重建页版本时无需单独从 S3 拉取 WAL。SNAPSHOTWAL 100-200 | | | | SNAPSHOTWAL 200-300 | | | | IN-MEMORY 300-每个SNAPSHOTWAL文件包含两部分内容起始 LSN 处关系的完整页镜像snapshot从起始 LSN 到结束 LSN 之间、适用于该关系的全部 WAL 记录。由此该文件覆盖的 LSN 区间内任意页版本都能重建——这正是当前 Pageserver Delta Layer 的语义雏形。RFC 还透露了一个实现细节文件实际上以序列化的 BTreeMap存储页镜像与 WAL 记录作为条目放在同一个 B-tree 中。今天的 pageserver/src/tenant/disk_btree.rs 磁盘 B-tree 正是这一思路的延续。3.1 该实现的性能短板快照永远滞后一个周期RFC 以一个单一关系的例子剖析了该实现的问题从空关系开始收到 LSN 100~200 的 WAL一批 insert/update全部驻留内存决定物化到磁盘写入该关系在LSN 100的完整镜像 100~200 的全部 WAL。由于关系初始为空区间起始处的镜像也是空的继续接收 WAL 至 LSN 300再次物化得到两个文件SNAPSHOTWAL 100-200 SNAPSHOTWAL 200-300注意磁盘上存储的最新全量快照总是落后一个快照周期——第一个文件存的是 LSN 100 的镜像第二个存的是 LSN 200 的镜像当我们已经收到 LSN 300 的 WAL 时写下的却是 LSN 200 的镜像。RFC 明确指出这看起来有点傻按照方案一Eric 的 RFC的设计本应在 LSN 200 和 300 各写一次快照即新镜像正好赶上当前进度。这个落后一拍的问题正是引入第三方案、最终催生现代 compaction 中image layer 物化到最新 LSN机制的直接动因。四、方案三快照与按关系 WAL 分离未实现两全其美第三方案将快照文件与 WAL 文件分离存储但 WAL 按关系粒度组织成独立的 LSN 区间文件SNAPSHOT 100 WAL 100-200 . | . | . | . | SNAPSHOT 200 WAL 200-300 . | . | . | . | SNAPSHOT 300 . . IN-MEMORY 300-RFC 认为这可能是best of both worlds快照文件独立于 PostgreSQL WAL 格式快照的格式演进不受 WAL 格式约束不再落后一个快照周期写 LSN 300 的快照时直接写该关系在 LSN 300 的完整镜像并把 200~300 累积的 WAL 写入单独文件WAL 就近可得每个关系的 WAL 就放在其快照文件旁边重建时无需去 S3 单独拉取无需单独追踪快照 LSN快照的 LSN 信息由文件自身携带。RFC 还讨论了一个折中细节若想减少文件数量可以把 LSN 300 的快照与 200~300 的 WAL 放进同一个文件但作者倾向保持分离以便独立演进与 GC。五、进一步思考快照 LSN 与 WAL 范围不必对齐RFC 的Further thoughts部分指出快照文件的 LSN 与 WAL 文件的区间没有理由必须对齐例如SNAPSHOT 100 WAL 100-150 . | . | . WAL 150-250 . | SNAPSHOT 200 | . | . WAL 250-400 . | . | SNAPSHOT 300 | . | . | IN-MEMORY 300-这里的 WAL 区间100-150、150-250、250-400与快照 LSN100、200、300完全错位。作者坦诚不确定这样做的收益是什么但给出了一个可能的应用场景在某个 WAL 文件覆盖区间的中间额外物化一个快照文件——例如在一个 LSN 范围中间创建分支时或在预见到某个 LSN 将成为热点大量请求集中在该 LSN时额外生成快照可显著加速该点位的读取。这个按需在热点 LSN 物化镜像的想法在今天对应着 Pageserver compaction 中是否值得为某键范围创建新 image layer的启发式决策见 pageserver/src/tenant/timeline/compaction.rs 中关于 image layer 创建时机的逻辑。六、落地方向从 RFC 到现代 LSM 层存储RFC 文档写于存储架构早期其提出的快照/镜像 WAL/增量二元结构在后续设计中发展成了今天 Pageserver 的 LSM 层存储docs/rfcs/014-storage-lsm.md。RFC 009 中的三种方案与现行架构存在清晰的一一对应关系RFC 009 概念现代 Pageserver 对应物快照文件某 LSN 的完整页镜像Image Layer某键范围在单一 LSN 的完整镜像快照WAL 层文件 / 按关系 WAL 文件Delta Layer某键范围在某 LSN 区间内的增量WAL 记录/页镜像内存层 IN-MEMORYMemtable / Ephemeral Layer快照 LSN 元数据列表timeline metadata 文件6.1 Image LayerRFC快照的直接化身pageserver/src/tenant/storage_layer/image_layer.rs 的模块注释明确写道An ImageLayer represents an image or a snapshot of a key-range at one particular LSN. It contains an image of all key-value pairs in its key-range. Any key that falls into the image layers range but does not exist in the layer, does not exist.这恰是 RFC 中每个快照文件包含该关系在快照 LSN 时刻的完整内容文件之外即不存在语义的键空间化实现。Image layer 文件命名格式为key start-key end__LSN例如000000067F000032BE0000400000000070B6-000000067F000032BE0000400000000080B6__00000000346BC568键范围 单一快照 LSN。6.2 Delta LayerSNAPSHOTWAL 层文件的继承pageserver/src/tenant/storage_layer/delta_layer.rs 的注释同样与 RFC 一脉相承A DeltaLayer represents a collection of WAL records or page images in a range of LSNs, and in a range of Keys.并特别指出通常 Delta Layer 只包含相对于某个基准 LSN 的差异即 WAL 记录但如果关系扩展或新关系创建新页面没有旧版本可作基准就必须以完整页镜像或带will_init标志的 WAL 记录存储使其无需引用更旧页版本即可重放——这正是 RFC 中从空关系开始物化、镜像为空场景的工程化处理。Delta layer 文件命名格式为key start-key end__start LSN-end LSN对应 RFC 中SNAPSHOTWAL 100-200的 LSN 区间表示。两类文件都采用三段式结构summary固定大小文件头 values实际页镜像与 WAL 记录 index从 Key/LSN 到 values 偏移的 B-treeB-tree 索引正是 RFC 所设想为 WAL 区间构建内存索引以加速按页查找的持久化形态。6.3 层堆叠与重建RFC 读路径的现代实现现代读取路径的层级搜索遵循与 RFC 完全一致的逻辑从最新层向下搜索遇到 Image Layer 即可停止因为镜像层之下不可能再有该键的新版本pageserver/src/tenant/storage_layer.rs 中注释明确On hitting image layer, we can mark all keys in this range as done, because if the image layer does not contain a key, it is deleted/never added。主库读取读最新 LSN命中内存层/最新 delta无需任何重放PITR 读取读旧 LSN落到某 Image Layer 镜像再向上重放该 LSN 到请求 LSN 之间的 Delta Layer 记录——等价于 RFC 中快照200 重放 WAL 200~250的步骤。Pageserver 的 gRPC GetPage 入口在 pageserver/src/page_service.rs 的get_page中实现其中effective_request_lsn会结合 GC cutoff 等约束校准请求 LSN随后批量调用handle_get_page_at_lsn_request_batched完成逐页重建。6.4 Compaction镜像层是如何生成的RFC 第三方案提出的在 WAL 区间的中间额外物化快照、让镜像追上最新 LSN正是今天 compaction 的核心工作。docs/pageserver-compaction.md 给出了完整机制Pageserver 以每租户分片一个compaction_loop后台任务运行默认每compaction_period默认 20 秒唤醒检查新 WAL 先进入ephemeral layer临时层超过checkpoint_distance默认 256 MB见 docs/settings.md后排序刷出为 L0 层文件L0→L1 compaction取底部compaction_threshold默认 10到compaction_upper_limit默认 20个 L0 层归并排序写出大小为compaction_target_size默认 128 MB的 L1 delta 层L1 image compaction对 L1 键空间中与镜像层重叠的 delta 层达到image_creation_threshold默认 3的区段通过向量化读取vectored reads物化页镜像生成新的 Image Layer——这一步直接对应 RFC 中写快照 300 时写出该关系在 LSN 300 的完整镜像的设想。镜像层带来的收益与 RFC 的预期完全一致限制一次搜索需要检查的层数从而给读延迟设上限并允许对早于 GC 水位的层进行垃圾回收GC 相关的gc_horizon等参数同样见 docs/settings.md。层文件本身不可变、只增删不修改的特性也让其天然适配 S3 对象存储参见 docs/rfcs/014-storage-lsm.md 对 LSM 设计动机的阐述。七、小结一条清晰的架构演化脉络从这份 RFC 可以完整看到 Neon 存储引擎的一条核心设计脉络问题GetPageLSN 必须重建任意历史页版本以支撑只读副本、锚定副本与时间点分支方案一快照优先按关系存完整镜像快照WAL 独立存放PITR 快照镜像 区间 WAL 重放代价是需要额外追踪快照 LSN 列表方案二当前实现快照与 WAL 合并为层文件省去单独拉取 WAL但镜像总是落后一个周期方案三未实现快照与按关系 WAL 分离镜像追平进度、格式解耦被视为两全其美落地最终在 LSM 存储中以 Image Layer / Delta Layer 的形态实现compaction 机制负责在合适时机物化镜像、控制读放大并支撑 GC。对想要深入源码的读者建议按以下路径继续研读RFC 原文docs/rfcs/009-snapshot-first-storage-pitr.mdLSM 存储设计docs/rfcs/014-storage-lsm.mdCompaction 机制详解docs/pageserver-compaction.mdImage Layer 实现pageserver/src/tenant/storage_layer/image_layer.rsDelta Layer 实现pageserver/src/tenant/storage_layer/delta_layer.rs层管理与读取 pageserver/src/tenant/storage_layer.rs、pageserver/src/tenant/timeline.rsCompaction 与镜像层创建pageserver/src/tenant/timeline/compaction.rs配置项docs/settings.md【免费下载链接】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),仅供参考
返回列表