十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Neon Page Server 架构深度解析:GetPage@LSN、WAL 摄取与分层存储的实现原理

Neon Page Server 架构深度解析:GetPage@LSN、WAL 摄取与分层存储的实现原理 Neon Page Server 架构深度解析GetPageLSN、WAL 摄取与分层存储的实现原理【免费下载链接】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 仓库 docs/pageserver.md 及其姊妹文档为主线深入剖析 Page Server页面服务器在 Neon 计算-存储分离架构中的核心职责与实现机制如何响应 Compute Node 的 GetPageLSN 请求、如何从 Safekeeper 摄取并重放 WAL、如何将数据组织成不可变的 L0/L1 层文件并上传至 S3以及分支Branching与 PITR 时间点恢复如何在存储层落地。读完本文你将掌握 Neon 存储引擎的完整数据流、层文件的生命周期与命名规则、垃圾回收GC的取舍逻辑以及 WAL redo 进程的多租户安全设计并可直接对照仓库源码进一步研究。Page Server 在 Neon 架构中的定位Neon 将传统 PostgreSQL 拆分为计算Compute与存储Storage两部分Compute Node 运行完整 PostgreSQL 实例而 Page Server 负责提供数据页。根据 docs/pageserver.mdPage Server 承担三项核心职责响应 Compute Node 的 GetPageLSN 请求按指定 LSNLog Sequence NumberWAL 中的字节位置提供页面内容从 WAL Safekeeper 接收并存储 WAL持续摄取 WAL 记录为重建任意历史页版本提供素材上传数据到 S3 以持久化并按需从 S3 下载把本地层文件归档到云端。一个关键的设计事实是Neon 没有 Page Server 副本S3以及其他兼容对象存储是所有数据的唯一容错主存储。出于降低延迟的考虑Neon 使用一套独立的容错 WAL 服务即 Safekeeper来临时保管尚未同步到 S3 的 WAL 记录——写入路径可以先把 WAL 交给 Safekeeper 完成持久化而无需等待 S3 上传完成这正是 Neon 能在不牺牲持久性的前提下降低写延迟的核心手段。从源码看这套职责落在 pageserver/src/lib.rs 组织的多个独立服务上页面请求、WAL 摄取、后台整理分别由不同线程/任务驱动下文逐一展开。服务构成多线程协作的共享仓库docs/pageserver-services.md 给出了 Page Server 的线程/服务全景图多个线程围绕一个页面版本共享仓库协作运行。整理后的服务划分如下Page Service页面服务监听来自 Compute Node 的 GetPageLSN 请求从仓库取出页面并返回WAL ReceiverWAL 接收器通过 PostgreSQL 物理流复制协议连接外部 WAL Safekeeper持续接收 WAL解码记录并写入仓库Backup service备份服务负责把 Page Server 的恢复数据外部化存储即上传远程对象存储Repository 后台任务包括 checkpointing将内存中累积的 WAL 落盘、compaction合并整理文件以降低读放大和 garbage collection回收过期文件HTTP 管理 API对外提供租户/时间线的管理接口WAL redo 进程用于把 WAL 重放为具体页面版本详见下文WAL redo 与多租户安全一节。对应实现可分别查看 pageserver/src/page_service.rs、pageserver/src/walingest.rs、pageserver/src/tenant/tasks.rs。Page Servicelibpq 协议上的页面服务docs/pageserver-page-service.md 说明Page Service 监听 GetPageLSN 请求并为每个进入的连接派生一个独立线程使用 libpq 协议与客户端Compute 的 PostgreSQL 实例通信每个请求最终调用仓库的页面获取函数。在源码中请求处理入口位于 pageserver/src/page_service.rs 的handle_get_page_at_lsn_request_batched约 L2454它批量处理页面请求并交由 Timeline 的读取路径完成页版本重构收到请求后Page Server 需要重建出该页在指定 LSN 时刻的内容——这正是存储层层文件设计要解决的核心问题。WAL Receiver物理流复制接入 WALWAL Receiver 以 PostgreSQL 物理流复制的方式连接 Safekeeper 并连续接收 WAL解码后将记录写入仓库。摄取逻辑在 pageserver/src/walingest.rs 中实现其ingest_record约 L234及一系列按记录类型分派的ingest_xlog_*/ingest_*函数如ingest_xact_record、ingest_xlog_smgr_create、ingest_xlog_smgr_truncate、ingest_relmap_update等负责把不同类型的 WAL 记录映射为对键值空间的写入。摄入过程中会维护disk_consistent_lsn水位线见 pageserver/src/tenant/timeline.rs 中的disk_consistent_lsn: AtomicLsn字段表示已完整落盘到本地并 fsync 的 WAL 位置是崩溃恢复与层文件状态判断的重要依据。Backup Service把数据托付给远程对象存储因为 Page Server 无副本、本地工作目录可能相当短命例如运行在 k8s 中且未挂载持久卷的 pod所以它必须与更可靠的外部存储交互以备份和恢复自身状态。Backup service 默认禁用启用后与单个远程存储交互。存储代码被设计为可扩展的只要实现相应的 Rust trait 即可接入任意存储后端。从当前仓库源码看RemoteStorageKind枚举定义于 libs/remote_storage/src/config.rs L80-L93已有四类实现后端说明LocalFs { local_path }本地文件系统主要用于测试AwsS3(S3Config)AWS S3用于生产AzureContainer(AzureConfig)Azure Blob 容器GCS(GCSConfig)Google Cloud Storage命令行启用示例沿用文档中的写法${PAGESERVER_BIN}为 pageserver 可执行文件# 本地文件系统 ${PAGESERVER_BIN} -c remote_storage{local_path/some/local/path/} # AWS S3凭据可通过环境变量注入 env AWS_ACCESS_KEY_IDSOMEKEYAAAAASADSAH*# \ AWS_SECRET_ACCESS_KEYSOMEsEcReTsd292v \ ${PAGESERVER_BIN} -c remote_storage{bucket_namesome-sample-bucket,bucket_regioneu-north-1,prefix_in_bucket/test_prefix/}使用 AWS 时如果本机 awscli 已配置过目标 bucket则密钥也可以放在~/.aws/credentials在 AWS 控制台用户设置页生成注意 bucket 名称中不带任何协议前缀。对于本地 S3 实现如 MinIO名称格式与凭据请参考其自身文档。与 pageserver 的其他设置一样也可以在 TOML 配置文件中配置远程存储所需字段如下# 本地文件系统 [remote_storage] local_path /Users/someonetoignore/Downloads/tmp_dir/# AWS S3 [remote_storage] bucket_name some-sample-bucket bucket_region eu-north-1 prefix_in_bucket /test_prefix/S3 凭据可通过AWS_SECRET_ACCESS_KEY与AWS_ACCESS_KEY_ID环境变量提供另外S3Config还支持endpoint用于非 AWS 的 S3 兼容存储例如http://127.0.0.1:5000和concurrency_limit等字段见 libs/remote_storage/src/config.rs L119-L144。# Azure Blob [remote_storage] container_name some-container-name storage_account somestorageaccnt container_region us-east prefix_in_container /test-prefix/Azure 凭据可通过AZURE_STORAGE_ACCESS_KEY环境变量指定。存储格式从 WAL 到不可变的层文件核心思路替代传统基备份与 WAL 归档docs/pageserver-storage.md 阐明了 Page Server 最主要的职责处理进来的 WAL并将其重排为一种能快速访问任意页版本的格式。Page Server 按关系页对进来的 WAL 进行切片再把切片后的 WAL 打包成大小合适的层文件layer files。层文件保存了数据库回溯到某个合理保留期retention之前的全部历史取代了传统 PostgreSQL 中的基础备份base backup与 WAL 归档WAL archive。层文件是不可变的创建后不再原地修改新 WAL 到来时创建新层文件不再需要的旧层文件则被删除。数据流动的整体画面如下原文档 ASCII 图Safekeeper 在右云端存储在左Cloud Storage Page Server Safekeeper L1 L0 Memory WAL ---- -------- |AAAA| |AAAA|AAAA| -------- | ---- -------- | | | |AA |BBBB| |BBBB|BBBB| |BB | AA | |BB -------- -------- |C | BB | |CC |CCCC|CCCC| ---- |CCCC|CCCC| --- |D | CC | --- |DDD ---- ADEBAABED -------- -------- | | DDD | |E |DDDD|DDDD| |DDDD|DDDD| |E | | | -------- -------- | | | |EEEE| |EEEE|EEEE| -------- ---- --------WAL 从右侧以流形式从 Safekeeper 到达立即被 Page Server 捕获并快速存进内存。内存可以理解为一个快速的重排缓冲区reorder buffer把进来的 WAL 暂存并重排使属于同一页、同一关系的 WAL 记录彼此靠近。当内存中累积的 WAL 足够多时被 flush 为一个新的L0 层文件内存随之释放当 L0 文件积累到一定数量后它们被合并并按 key 空间重新切片产生一组新的文件每个文件覆盖更窄的 key 区间、但更大的 LSN 区间即L1 层文件层文件再从本地盘复制到云存储做长期归档复制完成后理论上可以从本地删除但当前实现仍把一切保存在本地以换取快速访问若需要某层而本地没有则从云存储取回。L0 与 L1 文件都会被上传到云存储。这一先攒内存、落 L0、合并成 L1、再上云的流水线本质上是一个两级的 LSM 树变体详见 docs/pageserver-compaction.md。Layer map时间线内层文件的索引LayerMap跟踪一条时间线timeline中现存的所有层。其设计与实现位于 pageserver/src/tenant/layer_map.rs时间线首次被访问时服务端会列出timelines/timeline_id目录下的所有层文件为每个文件构建ImageLayer或DeltaLayer结构并填入映射表收到第一条新 WAL 时创建InMemoryLayer承接后续记录此后在checkpoint()过程中冻结内存层并拆分为新的 image/delta 层文件落盘。文档记载的早期实现是一个可伸缩数组Vec读取时线性扫描以定位包含目标页面的层而当前源码已演进为**基于持久化不可变二叉搜索树persistent BST**的结构按 LSN 起始序从旧到新逐层插入每次插入后保存该版本树的一个引用存入按 LSN 键控的 BTreeMap查询某个 (key, LSN) 时先在 BTreeMap 找到对应的树版本再以 key 在树中检索。LayeredTimeline的读取代码还感知祖先ancestor当前时间线找不到数据时会回到祖先时间线去取这正是分支的基础。层的不同状态docs/pageserver-storage.md 列出层的几种状态Open打开还可以追加新 WAL 记录Closed关闭只读不能再追加Historic 是它的同义词InMemory内存中需要在 pageserver 启动时从 WAL 重建为避免 OOM可把 InMemory 层溢出spill到磁盘上的临时文件ephemeral file见 pageserver/src/tenant/storage_layer/inmemory_layer.rs 中基于EphemeralFile的实现OnDisk磁盘上存储在磁盘若其 end-LSN 早于disk_consistent_lsn则确认已完整 flush 且 fsync 到本地盘Frozen冻结已关闭的 InMemory 层。OnDisk 层分两类ImageLayer镜像层某一特定 LSN 下、某 key 区间内所有键的快照凡不在镜像层中的键即可认定在该 LSN 处不存在DeltaLayer增量层某 key 区间在某个 LSN 区间内的 WAL 记录或页镜像集合。对应的 Rust 类型ImageLayer、DeltaLayer、InMemoryLayer、DeltaLayerName、ImageLayerName等统一从 pageserver/src/tenant/storage_layer.rs 导出。层生命周期冻结与落盘两步走LSN 区间的边界约定start_lsn包含end_lsn不包含。对于打开的内存层end_lsn为MAX_LSN对于冻结的内存层或 delta 层它是有效的上界镜像层表示单个 LSN 处的快照因此其end_lsn恒等于快照 LSN 1。每层都以打开的 InMemory 层开始Page Server 收到某时间线的第一条 WAL 记录时为它创建 InMemory 层并放入 layer map当该层写满后其内容落盘为 OnDisk 层。落盘是两步过程冻结freezing先把层标记为 closed使其不再接受新 WAL同时创建新的 InMemory 层承接此后的 WAL。此时该层处于 Closed InMemory 状态生成新层创建包含冻结层全部数据的 Delta 层写入并 flush 到磁盘后用新层替换 layer map 中的原冻结层并丢弃原冻结层以释放内存。层文件命名key 区间 × LSN 区间层文件存储在各时间线的子目录下.neon/tenants/tenant_id/timelines/timeline_id/。每个层文件覆盖一个 key 区间和一个 LSN 区间镜像层为单一 LSN可以想象成二维key-LSN 空间里的一个矩形。文件名由三部分组成起始 key、结束 key、以及 LSN 信息。镜像文件image file示例000000067F000032BE0000400000000070B6-000000067F000032BE0000400000000080B6__00000000346BC568 start key end key LSN增量文件delta file命名类似但覆盖一个 LSN 区间000000067F000032BE0000400000000020B6-000000067F000032BE0000400000000030B6__000000578C6B29-0000000057A50051 start key end key start LSN end LSNdelta 文件只包含在其 LSN 区间内被更新过的那些键值未被修改的键在其中不留任何痕迹。关于 key 空间如何划分relation、fork、segment 到 key 的映射可参见 pageserver/src/pgdatadir_mapping.rs 中的映射逻辑。L0 与 L1 的判别delta 层文件可以只覆盖部分 key 空间也可以覆盖整个 key 区间起始 key 全 0、结束 key 全 F000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__000000578C6B29-0000000057A50051覆盖整个 key 区间的文件称为L0 文件Level 0只覆盖部分 key 区间的称为L1 文件Level 1。级别并不显式存储在文件里只能通过观察 key 区间来区分读取路径无需对 L0/L1 做任何区别对待。文档补充说明早期层文件按单个关系切分如今已改为按 key 区间覆盖下文创建层文件一节沿用文档中简化的可读写法用表名代替 key 区间、省略 tenant/timeline id、缩写 LSN但描述的页版本查找、分支与 GC 原理仍然有效。例如完整路径.neon/tenants/941ddc8604413b88b3d208bddf90396c/timelines/4af489b06af8eed9e27a841775616962/rel_1663_13990_2609_0_10_000000000169C348_0000000001702000可简写为main/orders_100_200主分支上 orders 表、LSN 100-200 的 delta 文件base image 文件则写作main/orders_100。Checkpointing如何创建层文件一个逐步演进的例子假设系统里只有一条分支main两张表orders与customersWAL 尾部当前在 LSN 250。初始时磁盘上有main/orders_100 main/orders_100_200 main/orders_200 main/customers_100 main/customers_100_200 main/customers_200LSN 200 到 250 之间的最新变更仍保存在内存中——如果此时 Page Server 崩溃这段 200-250 的 WAL 需要重新从 Safekeeper 读取。每当内存中累积了足够多的 WALPage Server 就把内存中的变更写成新的层文件这个过程称为checkpointing注意与 PostgreSQL 的 checkpoint 概念不同只是名字相似。Page Server 只为自上次 checkpoint 以来被修改过的关系创建层文件。例如当前 WAL 尾部在 LSN 450、上次 checkpoint 在 LSN 400而customers表最近没有变化则磁盘上是main/orders_100 main/orders_100_200 main/orders_200 main/orders_200_300 main/orders_300 main/orders_300_400 main/orders_400 main/customers_100 main/customers_100_200 main/customers_200若稍后customers表被修改下一次 checkpoint 会为它创建新文件且新文件会覆盖自上一个层文件以来的空隙保证同一关系的 LSN 区间始终连续main/orders_100 main/orders_100_200 main/orders_200 main/orders_200_300 main/orders_300 main/orders_300_400 main/orders_400 main/customers_100 main/customers_100_200 main/customers_200 main/customers_200_500 main/customers_500读取页版本GetPageLSN 的处理每当 Compute Node 发来 GetPageLSN 请求Page Server 需要重构该页在请求 LSN 时刻的内容先查最近的内存层——若目标页版本在其中直接返回无需碰磁盘否则定位包含该页版本的层文件加载之从基镜像开始重放其中适用于该页的 WAL 记录直至目标 LSN。例如请求orders表在 LSN 250 的页Page Server 会加载main/orders_200_300文件由于层文件 起始 LSN 处的完整关系镜像 WAL重构过程就是从 LSN 200 的基镜像开始重放 200-250 之间作用于该页的 WAL 记录。文档中同时指出见 docs/pageserver-compaction.mdPostgres 以 8KB 页存储数据页内更新要么是整页镜像、要么是增量补丁每个写操作由其在 WAL 中的字节位置LSN标识因此页版本天然由pageLSN唯一标识——这正与层文件的 key-LSN 二维组织方式一一对应。checkpoint 相关配置源码默认值文档明确指出每条关系可以独立 checkpoint即各关系的 LSN 区间不必对齐同时同一关系也可以存在重叠的 LSN 区间。当前实现虽不会主动制造这两种情形代码总是把所有关系一起 checkpoint但读取代码需要能应对它们——例如在分支点附近做 GC、或设置显式恢复点时这类状态可能作为瞬态出现用于只保留仍被引用的那个精确点附近的层例如把main/orders_100_200替换成main/orders_150不过该 compaction 优化目前尚未实现。在代码中checkpoint 的触发与尺寸由租户级配置控制默认值定义在 libs/pageserver_api/src/config.rs配置项默认值作用checkpoint_distance256 MBDEFAULT_CHECKPOINT_DISTANCEInMemory 层持有的 WAL 若旧于该距离就 flush它实际决定了 L0 层文件的大小也约束了崩溃后需要重新消化的 WAL 量checkpoint_timeout10 分钟DEFAULT_CHECKPOINT_TIMEOUT即使活动停止内存层也会至少每隔该时间 flush 一次确保 WAL 最终上传多分支与 PITR祖先链上的线性历史假设在 LSN 250 处创建子分支250 ----main---------------------------- \ ---child--------------随后orders表在main与child上被不同地更新磁盘上出现main/orders_100 main/orders_100_200 main/orders_200 main/orders_200_300 main/orders_300 main/orders_300_400 main/orders_400 main/customers_100 main/customers_100_200 main/customers_200 child/orders_250_300 child/orders_300 child/orders_300_400 child/orders_400customers表在子分支上没被修改因此child目录下没有它的文件。若在child分支上请求customers的页面Page Server 在child目录里找不到对应层文件就会递归到父分支main去查找。从child分支的视角看每个关系的历史是线性的请求的 LSN 唯一地确定了需要查看哪个文件main分支视角下orders的历史main/orders_100、main/orders_100_200、main/orders_200、main/orders_200_300、main/orders_300、main/orders_300_400、main/orders_400child分支视角下则拼接为main/orders_100、main/orders_100_200、main/orders_200、main/orders_200_300、child/orders_250_300、child/orders_300、child/orders_300_400、child/orders_400。分支元数据记录了子分支的创建点 LSN 250。若请求 LSN 275 的页则从child/orders_250_300读取为了把 WAL 重放到 275可能还需要先重建 LSN 250 处的页版本即结合main/orders_200_300与main/orders_200而main/orders_200_300中 250-300 之间的页版本在子分支上会被忽略。一个值得强调的事实子分支在main尾部恰好是 LSN 250 时创建还是在main已前进之后于历史 LSN 250 处创建对存储逻辑没有任何区别——后者从历史 LSN 分支正是 Neon 支持PITRPoint-In-Time Recovery的方式。这与pitr_interval配置默认 7 天共同构成了 Neon 的按时间点恢复能力只要页版本在保留期内就可以从任意 LSN 分支出新的时间线。垃圾回收GC如何安全地删除旧层文件系统会持续产生新的层文件磁盘空间不是无限的因此需要 GC 删除不再需要的旧文件。目前 Page Server 支持从任何分支上、距离分支 tip足够近的任何 LSN 进行 PITR 和分支。足够近由一个 LSN 地平线horizon定义默认 64 MB即DEFAULT_GC_HORIZON 64 * 1024 * 1024见 libs/pageserver_api/src/config.rs L878。下面沿用文档例子假设 horizon 为 150 个 LSN 单位。单分支场景假设分支尾部在 LSN 525则 GC horizon 位于 525-150 375main/orders_100 main/orders_100_200 main/orders_200 main/orders_200_300 main/orders_300 main/orders_300_400 main/orders_400 main/orders_400_500 main/orders_500 main/customers_100 main/customers_100_200 main/customers_200end LSN 早于 GC horizon 375、且该表存在更新层文件的文件可以删除main/orders_100 DELETE main/orders_100_200 DELETE main/orders_200 DELETE main/orders_200_300 DELETE main/orders_300 STILL NEEDED BY orders_300_400 main/orders_300_400 KEEP, NEWER THAN GC HORIZON main/orders_400 .. main/orders_400_500 .. main/orders_500 .. main/customers_100 DELETE main/customers_100_200 DELETE main/customers_200 KEEP, NO NEWER VERSION注意main/customers_200虽然足够老但因为该表没有更新的层文件不能删除否则数据会丢失。多分支场景多分支时情况更复杂除近期文件外仍被子分支需要的旧快照文件也必须保留。例如子分支创建于 LSN 150且customers在子分支上被更新过main/orders_100 KEEP, NEEDED BY child BRANCH main/orders_100_200 KEEP, NEEDED BY child BRANCH main/orders_200 DELETE main/orders_200_300 DELETE main/orders_300 KEEP, NEWER THAN GC HORIZON main/orders_300_400 KEEP, NEWER THAN GC HORIZON main/orders_400 KEEP, NEWER THAN GC HORIZON main/orders_400_500 KEEP, NEWER THAN GC HORIZON main/orders_500 KEEP, NEWER THAN GC HORIZON main/customers_100 DELETE main/customers_100_200 DELETE main/customers_200 KEEP, NO NEWER VERSION child/customers_150_300 DELETE child/customers_300 KEEP, NO NEWER VERSIONmain/orders_100与main/orders_100_200虽然老于 GC horizon但子分支仍在引用不能删main/orders_200与main/orders_200_300则可以删除。如果之后orders在child分支上被修改会为它生成新的镜像与 delta 文件main/orders_100 main/orders_100_200 main/orders_300 main/orders_300_400 main/orders_400 main/orders_400_500 main/orders_500 main/customers_200 child/customers_300 child/orders_150_400 child/orders_400此后main/orders_100、main/orders_100_200理论上可以删除子分支已有更新的层文件但文档明确标注该优化尚未实现——只要子分支存在GC 算法目前仍会保留main分支上的这些文件。GC 的实现与配置源码佐证GC 后台任务的主循环在 pageserver/src/tenant/tasks.rs 的gc_loop约 L343周期性调用tenant.gc_iteration(...)其中gc_horizon来自租户配置get_gc_horizon()见 pageserver/src/tenant.rs L4162并同时传入pitr_interval——即 GC 的历史保留由WAL 字节数gc_horizon与时间pitr_interval双维度共同决定当gc_period Duration::ZERO或gc_horizon 0时自动 GC 被禁用源码注释与TaskKind::GarbageCollector上下文gc_horizon、gc_period均可通过租户配置/管理 API 覆盖配置结构体定义见 libs/pageserver_api/src/models.rsTenantConfig/patch 结构L697 附近序列化测试在 pageserver/src/tenant/config.rs L276-L285默认值一览libs/pageserver_api/src/config.rsgc_horizon 64 MB、gc_period 1 hr、pitr_interval 7 days、compaction_target_size 128 MB、compaction_period 20 s、compaction_threshold 10L0→L1 合并的最小层数、compaction_upper_limit 20单次合并的 L0 层数上界单位为checkpoint_distance的倍数、image_creation_threshold 3创建 L1 镜像层的 delta 层搅动阈值。WAL redo 与多租户安全要从某个页镜像 若干 WAL 记录重构出特定页版本Page Server 需要重放 WAL。这在 GetPageLSN 请求到来时按需发生也会作为后台整理任务的一部分发生用于重组数据以获得更快的访问。重放 WAL 有一个严峻的安全前提数据不能跨租户泄漏且某条时间线上的一条损坏 WAL 记录不能影响其他租户或其他时间线。传统 PostgreSQL 并不把这当安全问题——能访问 WAL 目录或拥有超级用户权限本就可完全控制系统但 Neon 的 pageserver 是多租户的必须在同一进程内执行属于不同租户的 WAL恶意 WAL 不能波及其他租户。为此详见 docs/pageserver-walredo.md每个租户独立进程为每个租户启动一个独立的 WAL redo 进程该进程用seccomp(2)系统调用把自身权限裁剪到重放 WAL 所需的最小集合——不能访问文件系统不能访问网络只能通过管道与父 pageserver 进程通信威胁模型即便攻击者把恶意 WAL 注入某时间线的 WAL 流并劫持了对应的 WAL redo 进程该进程也接触不到系统其他部分又因每租户一个进程被劫持者只能看到同租户的 WAL 与数据——而这些攻击者本来就能访问通信方式WAL redo 进程以 Neon 专属命令行参数启动postgres可执行文件进入 redo 模式pageserver 控制其生命周期租户被 detach 时杀掉对应进程。通信基于 stdin/stdout/stderr 的请求-响应式简单自定义协议协议说明位于 pageserver/src/tenant/walredo.rs重放某页的一组 WAL 记录时pageserver 先把该页的 before 镜像和 WAL 记录经 stdin 送入再发重放命令redo 进程返回 after 镜像部分记录不走 redo 进程一些 WAL 记录类型如 SLRU 相关的 commit 记录由 pageserver 内的专属 Rust 代码直接处理——SLRU 不使用 Postgres 标准缓冲管理器在 redo 模式里处理它们需要对 Postgres 大改另外带整页镜像如XLOG_FPI的记录在摄取阶段就被特殊处理直接存为页镜像而非 WAL 记录多页记录的复制某些 Postgres WAL 记录会修改多个页这类记录会被复制多份为每个受影响的页各存一份。虽然略有浪费但大多数记录只影响单页开销可接受且 WAL redo 永远针对单页进行若记录包含对其他页的修改则直接忽略。结语与延伸阅读总结下来Neon Page Server 用内存重排缓冲区 → L0 层文件 → L1 层文件 → 云存储归档的流水线把传统 PostgreSQL 的基备份 WAL 归档替换为不可变层文件构成的二维 key-LSN 空间以disk_consistent_lsn和gc_horizon/pitr_interval为锚点在无副本的前提下保证持久性、支撑按 LSN 的任意页版本读取、分支与 PITR并用每租户独立的 seccomp 受限 redo 进程守住多租户隔离底线。若希望深入本仓库继续研究推荐按以下路径展开docs/pageserver-storage.md层文件格式、生命周期与命名规范本文第 4-5 节的主体来源docs/pageserver-services.md服务线程模型与 Repository 抽象docs/pageserver-walredo.mdWAL redo 进程与多租户安全细节docs/pageserver-compaction.mdcompaction 的动机与两阶段算法L0→L1 合并、L1 镜像化源码入口pageserver/src/page_service.rs页面请求、pageserver/src/walingest.rsWAL 摄取、pageserver/src/tenant/timeline.rsdisk_consistent_lsn与 checkpoint/GC 调度、pageserver/src/tenant/layer_map.rs层索引、pageserver/src/tenant/storage_layer.rs层类型定义、libs/pageserver_api/src/config.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),仅供参考
返回列表