news 2026/9/13 4:39:23

Neon 拆分脑防护:基于 Generation Number 的 pageserver 远程存储安全机制解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Neon 拆分脑防护:基于 Generation Number 的 pageserver 远程存储安全机制解析

Neon 拆分脑防护:基于 Generation Number 的 pageserver 远程存储安全机制解析

【免费下载链接】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 仓库中的 RFC 文档 docs/rfcs/025-generation-numbers.md 为主体,系统讲解 Neon 如何在不引入 pageserver 内置共识算法、不依赖 EC2 实例节点隔离(fencing)的前提下,用一套由控制面(control plane)签发的"代数号(generation number)"机制,从根本上解决 pageserver 远程存储(S3)的拆分脑(split-brain)安全问题。读完本文,你将掌握:generation number 如何解决节点替换、租户迁移、自动故障切换(failover)场景下的并发写入冲突;对象键(S3 key)为何采用 generation 后缀而非前缀;删除操作与remote_consistent_lsn推进为何必须经过 generation 校验;以及该方案在当前仓库源码中的落地形态(如Generation类型、持久化删除队列、/re-attach/validate接口)。

背景:拆分脑为什么是 pageserver 远程存储的致命问题

在 Neon 的存算分离架构中,pageserver 负责把 Postgres 的 WAL 落盘为 immutable 的 layer 对象并上传到 S3。问题在于,S3 这类对象存储不具备原子操作与客户端隔离(fencing)能力——没有任何办法"物理上杀死"一个还在运行的旧节点。

RFC 在 Motivation 一节给出了拆分脑的精确定义:

从远程存储视角看,只要两个节点都认为同一个租户(tenant)挂在它们身上、并且都能向 S3 写入,拆分脑就发生了。这可能是网络分区、病态的长延迟(例如被挂起的 VM),或者软件 bug 造成的。

在 RFC 成稿时的部署模型中,控制面靠"一个租户同一时刻只 attach 到一个 pageserver"这一保证来排除双 attach 导致的拆分脑——但这意味着如果某个 pageserver 失联,系统就不敢把它上面的租户迁走(因为无法安全地 detach),从而阻碍了对故障的鲁棒响应。这一缺失进一步阻塞了两个把"偶发拆分脑"当作设计前提的重要能力:

  • 无缝租户迁移(seamless tenant migration);
  • 自动的 pageserver 实例故障处理(failover)。

generation number 方案正是为了让"在旧节点可能仍在运行的情况下,把租户 attach 到一个新节点"这件事变得安全。其前置研究包括 docs/rfcs/020-pageserver-s3-coordination.md、docs/rfcs/023-the-state-of-pageserver-tenant-relocation.md 与 docs/rfcs/026-pageserver-s3-mvcc.md。

需求与设计准则

RFC 明确了硬性需求:

  1. 兼容不具备原子/隔离能力的存储后端(即接受 S3 无原子操作、客户端无法被 fencing 的限制);
  2. 不依赖计算层的 STONITH 或节点隔离(不假设"可靠地杀掉一个 EC2 实例且它一定死透");
  3. 粒度按租户而非按pageserver划分——无缝迁移需要租户级粒度,failover 也更倾向于把故障节点的负载分散给多个 peer,而不是整体搬走。

设计准则(Design Tenets)则强调:不要再造一个共识系统(控制面已有一致性数据库可做原子操作,safekeeper 侧已有 Paxos 实现);严格保证数据正确性——偶尔的可用性损失可以容忍,偶发的数据丢失绝不允许。

非目标

本 RFC 刻意不覆盖:故障检测、failover 的编排、用于快速迁移的 standby 模式、租户的有意多写者操作(多写只被视为瞬态拆分脑)、分片(sharding)。这些特性与本 RFC 的交互见 Appendix B。

方案总览:generation number 核心机制

RFC 的 Implementation Part 1: Correctness 摘要 把整套机制浓缩为六条要点:

  • 引入每租户的 generation number,唯一标识"租户到 pageserver 进程"的一次 attachment;
  • 每当控制面修改某个Project所分配的 pageserver,或所分配的 pageserver 重启时,generation 递增;只有控制面有权递增
  • 对象键(object key)追加 generation 后缀——这是对抗拆分脑、支持安全多 attach 的主要机制;
  • 同 node ID 多进程拆分脑安全:pageserver 启动时调用控制面重新 attach(re-attach),从而递增所有挂载租户的 generation;
  • 删除安全:把 S3 上的 DELETE 推迟到"删除节点已向控制面验证:不存在更高 generation 的 attachment 还引用该 key"之后;
  • 用控制面签发 generation,避免在 pageserver 内内置共识系统(存储格式本身并不绑定这一选择,未来可替换)。

一个 u32 为何够用

generation 以 u32 存储,在 S3 键中表现为 8 字节十六进制字符串。RFC 的估算很有意思:即使按 5 年系统生命周期计算,u32 的范围也允许每秒 27 次重启——比现实情况高出若干个数量级。也就是说,"用尽" generation 几乎不可能成为运维问题。

源码中的 Generation 类型

这套设计在仓库中已经落地为真实的 Rust 类型,位于 libs/utils/src/generation.rs:

/// Tenant generations are used to provide split-brain safety and allow /// multiple pageservers to attach the same tenant concurrently. /// /// See docs/rfcs/025-generation-numbers.md for detail on how generation /// numbers are used. #[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Hash)] pub enum Generation { // The None Generation is used in the metadata of layers written before generations were // introduced. A running Tenant always has a valid generation, but the layer metadata may // include None generations. None, Valid(u32), }

关键设计点:

  • Generation::None专门用于向前兼容:generation 机制引入之前写入的 layer 元数据没有代数后缀,读取侧用None表示;
  • Generation::new(v)构造有效代数,get_suffix()返回-{g:08x}形式的 8 位十六进制后缀(GenerationFileSuffix的格式化实现见同文件 第 95-101 行);
  • next()None递增到Valid(1)previous()Valid(0)的"上一代"解释为None(对应"升级自无 generation 时代"的租户)——这一点直接服务于 RFC 中"加载 index 时假设旧 layer 无后缀"的向后兼容规则;
  • 特意不实现Display,只允许通过get_suffix()把代数写进文件名,避免在format!()中误用代数本身(见 第 131-133 行注释)。

layer 文件名解析也印证了这一格式:在 pageserver/src/tenant/storage_layer/layer_name.rs 中,layer 名形如<key start>-<key end>__<LSN start>-<LSN end>-<generation>(8 位十六进制),并且"省略 generation 后缀"被明确视为合法(对应老数据)。

对象键变更:为什么必须用 generation 后缀

后缀 vs 前缀:由对象列表演变而来

RFC 的 FAQ Why a generation suffix rather than prefix 给出了简洁答案:S3 只能按前缀(prefix)列出对象,不能按后缀。在"寻找远程 index"的流程中,pageserver 需要做<tenant>/<timeline>/index_part.json*这样的前缀列举来兜底;如果 generation 放在前缀位置,这类列举就无法工作。而"按 generation 列举以便对某一代做 scrub"的场景并不属于常规操作,因此不值得为此牺牲前缀列举能力。

键结构:层对象与 index 对象都带后缀

所有对象键(layer 对象与 index 对象)都以 attachment generation 作为后缀。两个 pageserver 即使因故障残留而持有相同 node ID,由于启动时 re-attach 会递增 generation,它们也不会写到相同对象;同一租户的多 attachment(如迁移期间)同理,每次 attachment 拥有独立代数。

实际对象形态在 pageserver/src/tenant/remote_timeline_client.rs 中有完整定义:index 对象的键通过generation.get_suffix()拼接(见该文件 2651-2760 行附近的IndexPart相关代码),测试辅助函数assert_remote_files也按{}{}+ suffix 的方式构造期望文件名。

Index 变更与可见性语义

由于键带了后缀,IndexPart也必须记录 generation:目前它存储键与 LSN 以重建键名,扩展为同时存储 generation。RFC 估算这会让 index 文件增大"每 layer 约 10 字节"(layer 已以字符串形式编码;若未来把 JSON index 迁移为二进制格式,开销会更小)。

RFC 的 Visibility 一节是理解整套语义的关键,它区分了两类可见性:

  • 对 pageserver 的对象可见性:pageserver 的实际可见集合由其 LayerMap 决定,而 LayerMap 从它加载的index_part.json-<gen>初始化。它从"最近的前一代"index 起步,这些对象在旧代被超越前一直保持可见(因为旧代避免删除)。
  • 对客户端的 LSN 可见性:由于 index 文件带 generation 后缀,"读到什么数据"取决于读者处于哪一代。两个同时 attach 同一租户的 pageserver 可能各自回放 WAL、维护独立的 LayerMap、写独立的 index 文件,因此可见数据不必已远程提交。一个新 attach 的 pageserver 的最高可见 LSN 甚至可能相对旧 attachment"倒退"——如果旧 attachment 在交接前没来得及把全部数据上传到 S3。

RFC 给出了一个关键示例序列,说明"最近的前一代"不等于"墙上时间最近":

0001 -> 0002 | -> 0003

PS1 在代 1 写入index_part.json-0001,PS2 在代 2 读取它,PS3 在代 3 读取它,随后 PS2 才完成工作写出index_part.json-0002,PS3 再写出index_part.json-0003。这不构成安全问题:若 0002 引用了 0001 中不存在的对象,0003 只是看不见它,必要时重做对应工作(如重新摄取 WAL 或压缩);仅被 0002 引用的对象不会被未来代读取,最终由 scrub 清理。

删除与 remote_consistent_lsn:必须经过 generation 校验

三步骤删除协议

写操作靠"写者总是把自家 generation 写进键"天然去冲突,但删除更棘手:如果 pageserver A 被隔离、真正活跃的是 pageserver B,那么 A连删除自己写过的对象都危险——因为 B 的元数据可能仍引用这些对象。

RFC 给出的解决方法是把"把对象从 index 解链"与"真正删除对象"之间插入一步generation 校验,删除严格遵循顺序:

  1. 写出index_part.json:保证后续任何元数据读者都不会再读取被解链的对象;
  2. 调用控制面,校验我们使用的 attachment generation 仍然是最新的;
  3. 校验通过才删除对象——因为结合可见性规则,任何更晚的 generation 都会使用第 1 步上传的index_part.json或其后继,而二者都不再引用该键,故该键对任何后续 attachment 都不可见。

注意第 2 步只确认"第 1 步写出的那个 index_part 不再引用的对象"可以安全删除;并发进行的其他删除需要各自的校验。若第 2 步失败,对象可能泄漏——这安全但有成本(见下文 scrub),在正常关机与干净迁移时可通过妥善 flush 删除来避免。为避免海量控制面请求,多个租户的校验合并为单次请求,删除先排队再统一校验(见 持久化删除队列)。

remote_consistent_lsn 的双值设计与推进顺序

对象删除之外,pageserver 还通过向 safekeeper 回传remote_consistent_lsn间接删除 WAL 数据(safekeeper 据此丢弃该 LSN 以下的数据)。它遵循同样约束:

  1. 把覆盖到 LSNL0的 index_part 上传到 S3;
  2. 调用控制面校验 generation 仍最新;
  3. 把对外通告的remote_consistent_lsn推进到L0

关键点是第 3 步通告的不是最新值,而是第 1 步上传的 index_part 中所包含的值,这提供了强顺序保证。为此每个 timeline 内部维护两个remote_consistent_lsn:一个反映最近一次写远程存储,一个反映最近一次通过 generation 校验;只有后者才能对外(对 safekeeper)通告。控制面完全不知道remote_consistent_lsn本身,它只负责校验 generation 新鲜度,从而授权 pageserver 把该信息分享给 safekeeper。

在源码中这一设计体现为 pageserver/src/tenant/timeline.rs 的get_remote_consistent_lsn_projected()(尚未校验、可能后退的值)与get_remote_consistent_lsn_visible()(已校验、保证不后退的值)两个接口。

pageserver 的 attach 与启动流程变更

Attachment:attach 请求携带 generation

/v1/tenant/{tenant_id}/attach请求体新增generation字段。pageserver不持久化它——一个 generation 只在进程存活期内有效,这正好呼应 RFC 开头"generation 的生命周期等于 pageserver 进程或租户 attachment 中较短者"的定位。

寻找远程 index:GET 优先、ListObjects 兜底

因为 index 文件带 generation 后缀,pageserver 无法总是单次 GET 拿到最新 index(事先不知道最新是哪一代)。典型情况下,最近写过 index 的一代是"自己这一代减 1",但前一个节点可能拿到代数后还没来得及写 index 就崩溃了。因此启动一个 timeline 时:

  1. 先 GETindex_part.json-<my generation - 1>
  2. 若失败,发一次ListObjectsv2请求列举index_part.json*,按 generation 排序取最高、且<=自己当前代的那一个。

两条规则保证了"后一代写的对象永远不会对前一代可见":租户绝不能加载比自己 attachment 更新的 index。RFC 也提示了可选优化:让控制面记录"最近一次写 index 的是哪一代"以提高单次 GET 命中率,或让 pageserver 拿到新代数后主动先写一版 index。

启动时 re-attach:扫描磁盘 + 删除未授权内容

pageserver 启动时会调用新的控制面/re-attachAPI(见 Generation API),返回应挂载的租户列表及各自代数(控制面在返回前递增)。pageserver 仍会扫描本地磁盘,但对不在 re-attach 响应中的租户,直接删除本地内容——它们的缺席等价于一次隐式 detach。RFC 注明这一行为未来会随"secondary/standby location"概念而改变:拥有本地内容的节点届时可降级为该状态并保留部分数据。

这一流程在 pageserver/src/tenant/mgr.rs 中有对应实现:TenantStartupMode::from_reattach_tenant把 re-attach 响应转换为启动模式;响应中缺失的租户会被 detach 并删除本地文件(日志信息为"Detaching tenant, control plane omitted it in re-attach response");若控制面返回的代数低于本地记录("Control plane gave decreasing generation ..."),则该租户被降级为 secondary。

旧 index 的清理

删除旧 index 对正确性非必需,但若不清理,"ListObjects 兜底"会越来越贵。新 attachment 写出自己的 index_part 后,可以异步清理发现的旧index_part.json——既可以作为 pageserver 生命周期内首次写 index 后的显式步骤,也可以简单并入后台 scrub 周期执行。

控制面变更与 Generation API

存储与递增 generation

Project表新增 generation 字段。/v1/tenant/:tenant_id/attach所需代数由控制面在把租户分配到新服务器时递增提供:改变 assigned pageserver 的同一数据库事务里同时更新 generation,保证原子性。

在 storage controller 中对应实现位于 storage_controller/src/tenant_shard.rs(第 69-74 行注释:Latest generation number: next time we attach, increment this)。它还包含一条防御性断言:"Attempted to enter attached state without a generation"——试图在没有代数的情况下进入 attached 状态被视为严重 bug。

/re-attach/validate两个端点

RFC 指出该 API 可直接由控制面提供,也可做成独立微服务(实践中为做 E2E 测试确实需要写一个迷你版)。两个端点都很简单,只依赖持久化、可线性化的存储(如数据库):

/re-attach

  • 请求:{node_id: <u32>}
  • 响应:200 {tenants: [{id: <TenantId>, gen: <u32>}]}404表示未知 node_id;未来可用429表示检测到抖动(多个节点争夺同一 node_id 或进入重试循环);未知租户直接省略。
  • 服务端:查询数据库确定该 pageserver 应挂载的租户,对每个租户递增 attachment generation 并返回新值。
  • 客户端:响应中的租户以新代数激活;本地存在但响应中未引用的内容按 detach 处理(删除本地文件)。

RFC 还前瞻性地说明:未来若改用临时 node ID,此请求中的node_id会替换为某种关联 ID(如 EC 实例 ID 或首次使用时写入磁盘的 UUID),帮助控制面识别"是否同一个存储上的同一进程"。

/validate

  • 请求:{'tenants': [{tenant: <tenant id>, attach_gen: <gen>}, ...]}
  • 响应:200 {'tenants': [{tenant: <tenant id>, status: <bool>}...]}(未知租户省略)
  • 用途:让 pageserver 发现自己的 attachment 是否仍是最新。
  • 服务端:只读操作,比较请求中的代数与已知代数,相同则status: true
  • 客户端:未收到"当前代有效"的响应前,不得对该租户的远程数据执行任何删除

/load/ignore的归宿

由于 pageserver 改为只在启动时依据/re-attach响应挂载租户,/load/ignore原语义不再成立:/load功能上等同于 attach,将被移除(凡用/load之处改为 attach);/ignore等价于"不删本地文件的 detach"。

Timeline 创建与删除的特殊处理

timeline 的创建/销毁与普通读写不同:写入方是控制面而非 safekeeper/endpoint,因此需要防范两类场景:

  • 租户已 attach 到 pageserver B,而 pageserver A 正在处理控制面发来的 create/delete RPC;
  • pageserver A 收到创建请求后失联,租户被改挂到 B,创建请求重发给 B。

创建:靠 generation 后缀天然防错

由于 timeline 远程路径内所有对象都带 generation 后缀,一个慢节点在更新的代已经建好 timeline 并写入数据之后才尝试创建,只会写出一个带旧代数后缀的index_part.json,不会造成破坏。Timeline ID 永不重用,因此无需担心 create/delete/create 循环;若未来灾难恢复场景要"反删除"重用 ID,则需向所有 pageserver 确认对该 timeline 返回 404,并 flush 各自的删除队列。

控制面可以安全地在创建进行中改换租户归属(新 attachment 用新代数),但必须检查"timeline 创建成功"响应对应的 pageserver generation 是否仍最新:若已过期,该响应不构成成功,应丢弃(scrubber 会清理 S3 状态),或更优——向旧 attachment 发一个 timeline 删除请求。

删除:控制面持久化意图并无限重试

租户/timeline 删除豁免generation 校验,也不必走 GC/压缩 layer 删除那条队列:一旦控制面发出删除命令,就承诺持续重试直到完成,因此允许过期 pageserver 继续删除。控制面的相应义务是:删除期间必须等待真正完成(状态 404);pageserver 不可用时,要么等待同 node_id 的替代者,要么把租户 re-attach 到别处;删除意图必须在发出任何 RPC 前持久化(这正是仓库中持久化Operation记录、无限重试的既有机制)。

RFC 给出的两个示例:

  • 删除中途节点重启:只要返回过 202,远程已写入删除标记;后续任何同 node_id 的化身都会看到标记并继续删除;若原节点永久丢失且无同 node_id 替代,控制面必须把租户 re-attach 到别的节点。
  • 创建中途节点失联:控制面看到 HTTP 超时后,向租户最新 attachment 点持续重发请求直到成功;过期节点若仍在执行创建,写出的旧代数 index 会由清理旧 index 的同一机制最终清除。

一种特殊泄漏

最坏情况下,最新代已完成删除(含清空 timeline 路径),但某个慢速/被分区的节点仍以旧代数向该路径写入——这不会被 per-timeline scrub 捕获(timeline 已删除,无处 attach)。该场景很罕见,控制面可通过"迁移期间等多 attach 状态结束再处理删除"进一步压缩其概率;未来也可用外部工具做顶层 scrub,识别控制面数据库中不存在的 tenant/timeline 路径。

不安全场景:坏行为基础设施下的边界

RFC 明确划出安全边界:上述保证只适用于 EC2 + 临时磁盘这类基础设施。若基础设施可能透明地重启 pageserver 而旧进程仍存活(如 VM 被重新调度而未隔离旧实例),存在风险:控制面向节点 A 发/attach,A 死亡并被替换,控制面在不递增代数的情况下重试同一请求,就可能出现两个物理节点用同一代数。EC2 临时存储下只要控制面不复用 node_id 就不会发生,但换基础设施必须重新审视。若要彻底防护,可引入"node generation"区分持有同一 node_id 的不同进程,或干脆放弃静态 node_id、给每个 pageserver 进程签发临时 ID。

持久化删除队列与 scrub:优化的两翼

持久化删除队列(Persistent Deletion Queue)

写出新 index_part 到真正执行删除之间存在一个"对象只在内存中被引用"的窗口:pageserver 非正常停止会导致对象泄漏。这带来一对矛盾——延迟/批量删除可以摊薄控制面校验与 DeleteObjects 请求的成本,而立即删除可以最小化泄漏。持久化删除队列正是两全方案。

RFC 强调该队列的存在理由是优化而非正确性,只要遵守"执行删除前必须校验 generation"这一规则,细节有大量灵活度。其要点:

  • 作用域按 pageserver 全局而非按租户,原因有四:作为合并向控制面校验请求的中央点(避免每个Timeline对象直接触碰控制面 API、也不必了解校验规则);把队列与租户 attachment 生命周期解耦("休眠"不活跃租户时无需等待删除完成);摊薄持久化 I/O 成本;把删除合并成更少的、更大的 DeleteObjects 调用。附带好处是停用租户时无需排空其队列。
  • 删除流程:识别删除需求 → 从所有引用处解链(写index_part.json)→ 入队(每条记录tenant_id, attachment_generation, S3 key)→ 批量校验并执行(对一批条目调用控制面;通过校验的子集执行DeleteObjects)。
  • 可接受的泄漏:在第 2、3 步之间崩溃会丢失对无引用对象的追踪;入队条目也可能因摊薄 flush 成本而不立即持久化。这些都可接受,由 scrubber 兜底。未来改进手段包括:为入队条目设置持久化时限、优雅停机时主动 flush、租户 detach 时主动 flush、为每条目记录是否已过解链阶段(intent/commit 双条目)。
  • 可绕过队列的操作:整个 timeline 的删除豁免 generation 校验,任何时刻都可删该路径下所有对象;但由于小 timeline 的对象数凑不满一次 DeleteObjects,仍建议走删除管线最后一段与其他删除合并,因此队列应暴露两个输入通道:generation 感知的常规删除,与可跳过校验和持久化队列的 timeline 删除快速通道。

上述设计在 pageserver/src/deletion_queue.rs 及其子模块 validator.rs、list_writer.rs 中完整落地:删除被先写入持久化DeletionListvalidator定期把待删条目按租户聚合成批量/validate请求,只有"当前代或紧邻上一代"的列表才被接受执行(上一代被允许是因为本地磁盘保存的删除列表能证明该盘连续服务于两代、中间没有其他节点介入——见 validator.rs 第 196-219 行注释);校验失败则丢弃并记日志"Dropping stale deletions for tenant ... objects may be leaked"。对remote_consistent_lsn的更新同样进入该管线接受校验,过期代的更新会被丢弃("Dropped remote consistent LSN updates for tenant ... in stale generation")。

清理孤儿对象(scrubbing)

孤儿对象 = 不再被运行节点或元数据引用的对象,产生途径包括:节点 PUT 完 layer 后、写引用它的 index_part 前崩溃;过期节点相信自己仍是合法写者而持续写出一批对象;pageserver 在"解链"与"入队"之间崩溃。

孤儿对象功能上无害,只是占用 S3 容量。清理方式:做一次ListObjectsv2并与最新元数据交叉比对,找出未引用对象。scrub 只由 attached pageserver 执行(而非第三方进程),其删除请求与普通删除一样必须通过 generation 校验——这防止过期 pageserver 误把新一代写入的对象当作陈旧对象删掉。外部 scrubber 理论可行,但同样必须经过与删除队列一致的校验流程。

运维影响

可用性依赖

引入控制面协调后,三类操作产生新的依赖:

  1. 启动新 pageserver(或重启后激活):原先"即使控制面不可用也能就地重启恢复服务"的场景将不再成立,直到控制面可用。RFC 讨论过一种折中——与控制面通信超时后以旧代数恢复服务(若已持久化到磁盘),但这样做会破坏"代数唯一标识租户到 pageserver进程的 attachment"这一不变量(退化为标识"机器"或"磁盘状态"),且控制面本就是必要且高可用的组件,因此大概率不需要。
  2. 执行已入队的删除:运维上无碍——延迟删除无害,唯一代价是 S3 容量。
  3. 推进remote_consistent_lsn以触发 WAL 裁剪:若 safekeeper 磁盘紧张且控制面长期不可用,可能成为问题;届时可改为让 safekeeper 一上传到 S3 就尽早删除本地段,而不是等remote_consistent_lsn推进。

此外,控制面目前单区域运行:本 RFC 新增的请求要么低频、要么不在数据路径上,跨区域延迟不是大问题,但确实失去了这些操作的区域隔离;"console 与控制面拆分"的工作完成后,每个区域将拥有自己的控制面,本 RFC 的所有操作都可交给区域级控制面处理。RFC 还提出一个"逃生舱口":灾难场景下允许人工以手工选定的 generation 启动 pageserver,使其独立于控制面恢复上线。

分阶段滚动(Rollout)

组件间虽有耦合,但数据面大部分组件可独立于控制面先行部署(初始使用静态 generation):

  • Phase 1:pageserver 带特殊配置上线——一切按 generation 1 处理、attach 时不等待控制面代数、跳过删除与 remote_consistent_lsn 更新中的控制面调用点。
  • Phase 2:部署控制面变更——跟踪并递增 generation。
  • Phase 3:启用 pageserver 中依赖控制面的逻辑——启动时依赖/re-attach、处理 generation 校验请求。

向后/向前兼容

向后兼容很直接:读 index 时,元数据不含 generation 的 layer 视为无后缀路径;定位 index 文件时用"列举兜底"路径,若只有一个无后缀 index 就加载它。无需重写既有 layer——新 index 格式本就能表示无代数 layer。

向前兼容需要两阶段(很可能跨多个 release,因为读侧代码通常会先就绪):第一步部署"能读懂新 index 格式与 generation 后缀、但写键时不带 generation"的 pageserver;第二步才部署"写键带 generation"的版本。因为旧 pageserver 对 generation 一无所知、读不了带代数的对象名,所以必须先有读的能力、再开始写。

高可用/failover 场景示例(Appendix A)

generation 机制对多种 failover 模型均适用:

就地重启 pageserver:重启后向控制面 re-attach,拿到所有挂载租户的新代数并按之激活;若某 attachment 其实已过期(离线期间被改派他人),re-attach 响应会通过"不为该 attachment 递增代数"来告知——这会隐式阻断该租户的删除,pageserver 还应主动停止 S3 上传;控制面最终会 detach 该租户。若控制面干脆未在响应中包含某租户而本地仍有其数据,pageserver 删除本地数据且不加载激活。控制面可借此机制清理"停机过久、租户全被迁走"的节点。

节点故障(Case A:租户改挂其他节点):节点 0 失联后,外部机制把其租户按分发规则迁往节点 1、2,每个租户因 assigned pageserver 变化而递增代数。节点 0 仍以旧代数写 S3:index_part.json-00000001index_part.json-00000002并存,旧后缀下新写的对象对系统其余部分无关紧要(endpoint 已从新位置读取),只是可随时清理的垃圾;节点 0 无法删除任何东西——它要么连不上控制面,要么删除队列处理时校验请求报错。

节点故障(Case B:同 node_id + 网络盘直接替换):这是动态 VM/容器环境(以网络块设备提供存储、失联即自动替换 node_id)的典型场景。节点 0a 失败后生成物理上独立的"新节点 0"(0b),而 0a 可能仍在运行(环境不保证隔离)。0b 启动时 re-attach 拿到所有租户的更高代数;0a、0b 继续并行写 S3 但互不冲突(后缀不同),且 0a 的写入对系统不可见(endpoint 只从 0b 读)。

Appendix B:与其他特性的互通性

  • 分片键空间(Sharded Keyspace):本设计与分片天然契合——attachment 的工作单元从 Tenant 变为 TenantShard,TenantShard 与 Tenant 一样拥有 generation;租户的写入负载(摄取、压缩)经分片分散到多个 pageserver,但每个分片仍保证"同一时刻恰好一个合法写者"。
  • 读副本(Read Replicas):此处指 S3 pageserver 状态的被动读者(非 Postgres 读副本)。对低于 remote persistent LSN 的历史读,任何节点随时可读:远程数据逻辑上不可变,本 RFC 的延迟删除缓解了"物理上不可变"(压缩导致页面实际位置移动)的问题。读副本需感知代数以读取最新元数据(找到后缀最大的 index_part.json),可通过控制面查询或 ListObjectsv2 发现。
  • 无缝迁移(Seamless Migration):为做到完全无缝,会短暂地有意双 attach——旧节点继续服务读,等待新节点就绪。本 RFC 使双 attach 成为可能:两节点可同时 attach,迁移目标端代数更高;旧节点能摄取与服务读,但不能删除;新节点的 attachment 也必须避免删除旧节点仍在使用的 layer。为此控制面的 attachment 定义需要新增状态。
  • 热备位置(Warm Secondary Locations):为加速节点丢失后的租户移动,可投入磁盘容量让 standby 位置保留本地数据。与本 RFC 无冲突:standby 不写 S3,无需代数;租户 re-attach 到 standby 时照常递增 attachment generation,只是多了一个热缓存。
  • 临时节点 ID(Ephemeral Node IDs):本 RFC 刻意不改变 pageserver 的标识/注册方式,避免拆分脑防护与更根本的管理变更耦合。改用临时 ID 可带来额外韧性——即便出现两个同 node_id 的物理节点,控制面也不可能误以同一代 attach 到两者(当前依赖 EC2 保证消除该场景)。由于node_id在本设计中几乎不被使用,pageserver 侧几乎无需改动;/re-attachAPI 届时扩展为同时下发临时 ID,并接受关联标识符(如 EC 实例 ID)以便控制面把租户重新挂回原物理服务器。

总结

generation number 方案用"控制面签发、随 attachment 生命周期递增、写入 S3 键后缀、删除与 LSN 推进前必须校验"四个环节,把拆分脑防护从"依赖控制面单 attach 保证 + 节点隔离"升级为"存储格式层自洽"的安全模型。它不引入新共识、不依赖节点 fencing、按租户粒度工作,并且以Generation类型(libs/utils/src/generation.rs)、持久化删除队列(pageserver/src/deletion_queue.rs)与/re-attach/validate接口(storage_controller/src/http.rs 中注册于/upcall/v1/re-attach/upcall/v1/validate)等形式在仓库中完整落地。对读者而言,理解这套机制的关键在于记住两条主线:写入靠键后缀天然隔离,删除靠控制面校验确保安全——前者解决并发写冲突,后者保证"旧节点永远删不掉新节点还在引用的数据"。

【免费下载链接】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:38:02

TiDB 如何用保存点(SAVEPOINT)回滚事务中的部分修改

TiDB 如何用保存点&#xff08;SAVEPOINT&#xff09;回滚事务中的部分修改 【免费下载链接】tidb TiDB is built for agentic workloads that grow unpredictably, with ACID guarantees and native support for transactions, analytics, and vector search. No data silos. …

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

机器学习股价预测源码解析:特征工程、模型选型与回测防过拟合

简介&#xff1a;一套基于机器学习算法的股票价格分析与预测源码包&#xff0c;面向计算机、人工智能、大数据、数学、电子信息等专业正在进行课程设计、期末大作业或毕业设计的学生&#xff0c;也适合对金融量化方向感兴趣、具备一定Python基础的学习者对照调试与二次开发。压…

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

电子洁净库房温湿度均一性监控与WiFi网格化布点实战

/* 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:34:24

自建多模型对话后端:统一API、流式响应与适配器实战

简介&#xff1a;面向有一定Java与全栈基础的开发者&#xff0c;基于AI大模型API的自建后端对话服务项目ChatMASTER&#xff0c;可解决多模型统一接入、同步/流式响应&#xff08;含打字机式输出效果&#xff09;与私有化部署等问题。项目支持DeepSeek、月之暗面&#xff08;Ki…

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

用 gs-quant 算出因子还能撑多久:IC 半衰期实战指南

用 gs-quant 算出因子还能撑多久&#xff1a;IC 半衰期实战指南 【免费下载链接】gs-quant Python toolkit for quantitative finance 项目地址: https://gitcode.com/GitHub_Trending/gs/gs-quant 上周组里刚评完一批候选因子&#xff0c;紧接着就有人追问&#xff1a;…

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

AI辅助专业工作:技术边界与真实价值解析

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

作者头像 李华