news 2026/9/13 4:11:27

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Neon Page Server 架构深度解析:GetPage@LSN、WAL 摄取与分层存储的实现原理

Neon Page Server 架构深度解析:GetPage@LSN、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 的 GetPage@LSN 请求、如何从 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.md,Page Server 承担三项核心职责:

  1. 响应 Compute Node 的 GetPage@LSN 请求:按指定 LSN(Log Sequence Number,WAL 中的字节位置)提供页面内容;
  2. 从 WAL Safekeeper 接收并存储 WAL:持续摄取 WAL 记录,为重建任意历史页版本提供素材;
  3. 上传数据到 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 的 GetPage@LSN 请求,从仓库取出页面并返回;
  • WAL Receiver(WAL 接收器):通过 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 Service:libpq 协议上的页面服务

docs/pageserver-page-service.md 说明:Page Service 监听 GetPage@LSN 请求,并为每个进入的连接派生一个独立线程,使用 libpq 协议与客户端(Compute 的 PostgreSQL 实例)通信,每个请求最终调用仓库的页面获取函数。

在源码中,请求处理入口位于 pageserver/src/page_service.rs 的handle_get_page_at_lsn_request_batched(约 L2454),它批量处理页面请求并交由 Timeline 的读取路径完成页版本重构;收到请求后,Page Server 需要"重建"出该页在指定 LSN 时刻的内容——这正是存储层层文件设计要解决的核心问题。

WAL Receiver:物理流复制接入 WAL

WAL Receiver 以 PostgreSQL 物理流复制的方式连接 Safekeeper 并连续接收 WAL,解码后将记录写入仓库。摄取逻辑在 pageserver/src/walingest.rs 中实现,其ingest_record(约 L234)及一系列按记录类型分派的ingest_xlog_*/ingest_*函数(如ingest_xact_recordingest_xlog_smgr_createingest_xlog_smgr_truncateingest_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_ID='SOMEKEYAAAAASADSAH*#' \ AWS_SECRET_ACCESS_KEY='SOMEsEcReTsd292v' \ ${PAGESERVER_BIN} -c "remote_storage={bucket_name='some-sample-bucket',bucket_region='eu-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_KEYAWS_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>目录下的所有层文件,为每个文件构建ImageLayerDeltaLayer结构并填入映射表;
  • 收到第一条新 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 类型(ImageLayerDeltaLayerInMemoryLayerDeltaLayerNameImageLayerName等)统一从 pageserver/src/tenant/storage_layer.rs 导出。

层生命周期:冻结与落盘两步走

LSN 区间的边界约定:

  • start_lsn包含
  • end_lsn不包含

对于打开的内存层,end_lsnMAX_LSN;对于冻结的内存层或 delta 层,它是有效的上界;镜像层表示单个 LSN 处的快照,因此其end_lsn恒等于快照 LSN + 1。

每层都以"打开的 InMemory 层"开始:Page Server 收到某时间线的第一条 WAL 记录时,为它创建 InMemory 层并放入 layer map;当该层写满后,其内容落盘为 OnDisk 层。落盘是两步过程:

  1. 冻结(freezing):先把层标记为 closed,使其不再接受新 WAL,同时创建新的 InMemory 层承接此后的 WAL。此时该层处于 Closed InMemory 状态;
  2. 生成新层:创建包含冻结层全部数据的 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 LSN

delta 文件只包含在其 LSN 区间内被更新过的那些键值;未被修改的键在其中不留任何痕迹。关于 key 空间如何划分(relation、fork、segment 到 key 的映射),可参见 pageserver/src/pgdatadir_mapping.rs 中的映射逻辑。

L0 与 L1 的判别:delta 层文件可以只覆盖部分 key 空间,也可以覆盖整个 key 区间(起始 key 全 0、结束 key 全 F):

000000000000000000000000000000000000-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,两张表orderscustomers,WAL 尾部当前在 LSN 250。初始时磁盘上有:

main/orders_100 main/orders_100_200 main/orders_200 main/customers_100 main/customers_100_200 main/customers_200

LSN 200 到 250 之间的最新变更仍保存在内存中——如果此时 Page Server 崩溃,这段 200-250 的 WAL 需要重新从 Safekeeper 读取。

每当内存中累积了足够多的 WAL,Page 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

读取页版本(GetPage@LSN 的处理)

每当 Compute Node 发来 GetPage@LSN 请求,Page Server 需要重构该页在请求 LSN 时刻的内容:

  1. 先查最近的内存层——若目标页版本在其中,直接返回,无需碰磁盘;
  2. 否则定位包含该页版本的层文件:加载之,从基镜像开始重放其中适用于该页的 WAL 记录,直至目标 LSN。

例如请求orders表在 LSN 250 的页,Page Server 会加载main/orders_200_300文件,由于层文件 = 起始 LSN 处的完整关系镜像 + WAL,重构过程就是从 LSN 200 的基镜像开始,重放 200-250 之间作用于该页的 WAL 记录。文档中同时指出(见 docs/pageserver-compaction.md):Postgres 以 8KB 页存储数据,页内更新要么是整页镜像、要么是增量补丁,每个写操作由其在 WAL 中的字节位置(LSN)标识,因此"页版本"天然由page@LSN唯一标识——这正与层文件的 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 MB(DEFAULT_CHECKPOINT_DISTANCEInMemory 层持有的 WAL 若旧于该距离就 flush;它实际决定了 L0 层文件的大小,也约束了崩溃后需要重新消化的 WAL 量
checkpoint_timeout10 分钟(DEFAULT_CHECKPOINT_TIMEOUT即使活动停止,内存层也会至少每隔该时间 flush 一次,确保 WAL 最终上传

多分支与 PITR:祖先链上的线性历史

假设在 LSN 250 处创建子分支:

@250 ----main--+--------------------------> \ +---child-------------->

随后orders表在mainchild上被不同地更新,磁盘上出现:

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_400

customers表在子分支上没被修改,因此child目录下没有它的文件。若在child分支上请求customers的页面,Page Server 在child目录里找不到对应层文件,就会递归到父分支main去查找

child分支的视角看,每个关系的历史是线性的,请求的 LSN 唯一地确定了需要查看哪个文件:

  • main分支视角下orders的历史:main/orders_100main/orders_100_200main/orders_200main/orders_200_300main/orders_300main/orders_300_400main/orders_400
  • child分支视角下则拼接为:main/orders_100main/orders_100_200main/orders_200main/orders_200_300child/orders_250_300child/orders_300child/orders_300_400child/orders_400

分支元数据记录了子分支的创建点 LSN 250。若请求 LSN 275 的页,则从child/orders_250_300读取;为了把 WAL 重放到 275,可能还需要先重建 LSN 250 处的页版本,即结合main/orders_200_300main/orders_200;而main/orders_200_300中 250-300 之间的页版本在子分支上会被忽略。

一个值得强调的事实:子分支在main尾部恰好是 LSN 250 时创建,还是在main已前进之后于历史 LSN 250 处创建,对存储逻辑没有任何区别——后者(从历史 LSN 分支)正是 Neon 支持PITR(Point-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 = 375:

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/orders_400_500 main/orders_500 main/customers_100 main/customers_100_200 main/customers_200

end 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 VERSION

main/orders_100main/orders_100_200虽然老于 GC horizon,但子分支仍在引用,不能删;main/orders_200main/orders_200_300则可以删除。如果之后orderschild分支上被修改,会为它生成新的镜像与 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_100main/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::ZEROgc_horizon == 0时自动 GC 被禁用(源码注释与TaskKind::GarbageCollector上下文);
  • gc_horizongc_period均可通过租户配置/管理 API 覆盖,配置结构体定义见 libs/pageserver_api/src/models.rs(TenantConfig/patch 结构,L697 附近),序列化测试在 pageserver/src/tenant/config.rs L276-L285;
  • 默认值一览(libs/pageserver_api/src/config.rs):gc_horizon = 64 MBgc_period = 1 hrpitr_interval = 7 dayscompaction_target_size = 128 MBcompaction_period = 20 scompaction_threshold = 10(L0→L1 合并的最小层数)、compaction_upper_limit = 20(单次合并的 L0 层数上界,单位为checkpoint_distance的倍数)、image_creation_threshold = 3(创建 L1 镜像层的 delta 层搅动阈值)。

WAL redo 与多租户安全

要"从某个页镜像 + 若干 WAL 记录重构出特定页版本",Page Server 需要重放 WAL。这在 GetPage@LSN 请求到来时按需发生,也会作为后台整理任务的一部分发生(用于重组数据以获得更快的访问)。

重放 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_lsngc_horizon/pitr_interval为锚点,在无副本的前提下保证持久性、支撑按 LSN 的任意页版本读取、分支与 PITR,并用每租户独立的 seccomp 受限 redo 进程守住多租户隔离底线。

若希望深入本仓库继续研究,推荐按以下路径展开:

  • docs/pageserver-storage.md:层文件格式、生命周期与命名规范(本文第 4-5 节的主体来源);
  • docs/pageserver-services.md:服务线程模型与 Repository 抽象;
  • docs/pageserver-walredo.md:WAL redo 进程与多租户安全细节;
  • docs/pageserver-compaction.md:compaction 的动机与两阶段算法(L0→L1 合并、L1 镜像化);
  • 源码入口:pageserver/src/page_service.rs(页面请求)、pageserver/src/walingest.rs(WAL 摄取)、pageserver/src/tenant/timeline.rs(disk_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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 4:10:20

时间复杂度与渐进分析:大O、大Ω、大Θ从入门到实战判断

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 4:09:21

.NET日志框架核心原理与实现实战

1. .NET日志框架核心原理剖析日志系统是现代应用程序不可或缺的组成部分&#xff0c;它如同飞机的黑匣子&#xff0c;记录着程序运行时的关键信息。在.NET生态中&#xff0c;日志框架的设计哲学主要体现在以下几个核心维度&#xff1a;1.1 日志分级机制.NET日志系统采用分级设计…

作者头像 李华
网站建设 2026/9/13 4:09:15

Linux rlogin命令详解:远程登录工具的基本用法与安全实践

1. rlogin命令概述与基本用法rlogin&#xff08;Remote Login&#xff09;是Linux系统中用于远程登录的传统工具&#xff0c;它允许用户通过网络连接到另一台Unix/Linux主机并启动交互式会话。这个命令诞生于早期的BSD Unix系统&#xff0c;至今仍在许多场景下发挥作用。1.1 命…

作者头像 李华
网站建设 2026/9/13 4:07:37

APF人工势场法路径规划原理与MATLAB实现

1. APF人工势场法路径规划的核心原理人工势场法(Artificial Potential Field, APF)是机器人路径规划中经典的局部避障算法。它的核心思想是将目标点视为引力源&#xff0c;障碍物视为斥力源&#xff0c;通过计算合力来引导机器人运动。这种方法最早由Khatib在1986年提出&#x…

作者头像 李华
网站建设 2026/9/13 4:07:06

LunaTranslator 视觉小说实时翻译新手指南:10 分钟跑通第一句中文

LunaTranslator 视觉小说实时翻译新手指南&#xff1a;10 分钟跑通第一句中文 【免费下载链接】LunaTranslator 视觉小说翻译器 / Visual Novel Translator 项目地址: https://gitcode.com/GitHub_Trending/lu/LunaTranslator LunaTranslator 是一款完全开源免费的视觉小…

作者头像 李华
网站建设 2026/9/13 4:06:49

AI Agent用户记忆系统设计:跨会话持久化与安全架构

1. 项目概述&#xff1a;为什么“让 Agent 记住你”不是功能&#xff0c;而是分水岭“走进AI Agent第三篇&#xff1a;让 Agent 记住你”——这个标题乍看像一篇技术教程的普通章节&#xff0c;但如果你在真实业务中搭过三个以上生产级Agent系统&#xff0c;就会立刻意识到&…

作者头像 李华