Neon 时间线历史保留策略(Retention Policy)解析:PITR、快照与基于 LSN 的垃圾回收
【免费下载链接】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
本篇文章基于 docs/rfcs/011-retention-policy.md 展开,系统讲解 Neon 中"用户可见的时间线历史"是如何以保留策略(retention policy)的形式存在的:PITR 时间段、命名快照、自动快照与分支点如何在内部统一为 LSN 范围与 LSN 点,并由 base image + WAL slice 两套文件承载。结合 pageserver、safekeeper 与 pageserver_api 配置模型 的源码实现,你可以掌握 Neon 历史数据如何被保留、如何被回收,以及 GC(垃圾回收)阈值背后"空间阈值 + 时间阈值"双维度计算的真实机制,进而在部署与调优时正确设置gc_horizon、gc_period、pitr_interval等核心参数。
保留策略的用户视角:PITR 周期与快照
RFC 011 提出的核心模型是:用户为一条时间线(timeline)指定保留策略,该策略由两部分组成:
- PITR 周期(PITR period):需要保留的近期历史时长,以分钟、小时或天为单位。只要落在这个时间窗口内,用户就可以在任意时间点创建分支(branch)或快照(snapshot)、打开计算节点(compute node)并执行查询。也就是说,PITR 周期决定了"多老的历史仍然可以按时间点回放"。
- 快照(snapshot):一个命名的、确定的时间点。快照在内部由 LSN 表示,用户给快照起名后即可长期引用。系统还支持自动快照:用户可以指定一个时间间隔,让系统周期性地自动创建快照,例如"每天凌晨 2 点创建一个快照",并且可以设定一个保留时长,超过该时长的旧自动快照会被移除。
RFC 中的示意图直观地表达了这种"范围 + 点"的组合:
Snapshot Snapshot PITR "Monday" "Tuesday" PITR ----######----------+-------------+-------------######>左侧和右侧的######是 PITR 周期(连续的时间范围),中间的+是命名的快照点。值得注意的是,RFC 也特别提示:这个模型"可能过于灵活",现实产品希望保持用户界面简单,例如只允许在分支尖端(tip of a branch)设置 PITR 周期,但这对内部实现没有本质区别。
分支级策略
如果存在多条分支,可以针对不同分支指定不同的策略。这一点在 pageserver 的Timeline结构中有着直接体现:每条时间线独立持有自己的gc_info(GcInfo),其中记录了该时间线自己的保留相关的 LSN;而消费计量(consumption metrics)在统计 PITR 历史大小时,也是按 timeline 维度计算的(详见 consumption_metrics/metrics.rs 中的pitr_history_size_since_parent),因此每条分支的历史保留范围天然可以独立配置。
保留策略的内部表示:点与范围
RFC 强调,保留策略的底层构成是点(point,用于快照)和范围(range,用于 PITR 周期)。
系统必须能够重建保留策略覆盖范围内的任意页面(page)。在此范围之外的历史页面版本,则可以交给垃圾回收(GC)清理掉。对于"何时执行 GC、GC 多激进",系统拥有很大的自由度——这正是 pageserver 中 GC 相关调度逻辑 存在大量参数的原因。
在 pageserver 的实现中,这一思想被编码为GcCutoffs结构体:
space字段:根据gc_horizon(字节数阈值)计算出的 LSN,代表"为了保留指定字节数的 WAL 历史,必须保留多老的数据";time字段:根据pitr_interval计算出的 LSN,代表"为了支持至少一个 PITR 周期时长的历史读取,必须保留多老的数据";None表示尚未计算出 PITR cutoff;当pitr_interval为 0 时该值为Some(last_record_lsn)(即完全不保留历史)。
GcCutoffs::select_min()取两者中的最小值作为真正的 GC 边界:只有同时满足"空间阈值"与"时间阈值"才能执行 GC,任何一个方向都不允许把仍然需要的历史提前清理掉。这正是 RFC "保留策略 = 点 + 范围" 在工程上的落地。
底层存储:base image 与 WAL slice
页面版本以两类文件保存:
- base image:某个关系(relation)在特定 LSN 处全部页面的转储(dump)。
- WAL slice:一个 LSN 区间内(例如 100–200)的全部 WAL 记录。
RFC 用如下示意图说明数据布局:
| | | | --Base img @100 + | | | | WAL slice | | 100-200 | | | --Base img @200 + | | | | WAL slice | | 200-300 | | | + | V要恢复某个 LSN(例如 150)处的页面,就需要 LSN 100 处的 base image,以及覆盖 100–200 的 WAL slice。这一"base image + 在其上重放 WAL"的机制,在 pageserver 的 WAL redo 路径中有对应的实现(参见 pageserver/src/walredo.rs 与 pgxn/neon_walredo/walredoproc.c):读取 base image 后,将 WAL 记录逐条重放,直到目标 LSN。
按关系粒度组织与"热点感知"
这些工作以关系(relation)或关系段(relation segment)为粒度进行:
- 对更新非常频繁的关系,系统会更频繁地为其创建 base image 和 WAL slice;
- 对更新不频繁的关系,系统则更久地持有该关系较近期的 WAL,只有当需要释放原始 WAL 所占磁盘空间时,才把该关系的 WAL 落成 slice 文件。
RFC 明确指出这里需要一个"兜底机制"(backstop):在全部 WAL / base image 被持久复制到 S3 之前,原始 WAL 必须暂时存放在某处——要么在 WAL service(safekeeper),要么在 S3。也就是说,保留策略的容量承诺不能越过"尚未上传的 WAL"这条红线。在 safekeeper 侧,safekeeper/src/timeline.rs 中的WalResidenceData就是围绕"本地 WAL 保留/截断"这一约束工作的:它维护了 WAL 起始 LSN、截止 LSN 以及"已上传至 S3 的截止 LSN",从而让 safekeeper 可以在数据安全地进入 S3 之后才截断本地 WAL。
热数据与冷数据的处理
对频繁更新的关系,"按需生成 base image"的模式在 pageserver 中表现为image_creation_threshold(默认值为 3,见 libs/pageserver_api/src/config.rs 中的DEFAULT_IMAGE_CREATION_THRESHOLD):当某个键(key)在 L0/L1 层积累了足够多(超过阈值)的 WAL 记录时,压缩(compaction)过程就会强制生成一个 image 层,从而缩短每次页面重建时需要的 WAL 重放长度。这正是 RFC 所述"对更新频繁的关系更快地创建 base image"的实现机制。
同时,compaction.rs 中的compute_force_image_creation_lsn会基于 PITR cutoff 与pitr_interval计算出一个"强制创建 image 的 LSN"(image_layer_force_creation_period控制该周期):对于超过 PITR 周期、很快就要被 GC 回收的历史,系统会周期性地为仍需要保留的层强制生成 image,避免未来重建页面时需要跨越过长的时间区间重放 WAL。
分支点也是保留点
RFC 强调了一个容易忽略的事实:分支点(branch point)在内部也是"保留点"(retention point),与用户可见的快照地位相同。
如果一条分支从父分支的 LSN 100 处 fork 出来,那么父分支必须能够在 LSN 100 处重建任意页面,因为子分支还需要引用这些历史版本。反过来,如果某个页面在子分支上被修改,父分支就不再需要保留该页面的旧版本(除非它自身还有别的保留义务)。
在 pageserver 的实现中,这条规则体现为"retain LSN"机制:每条时间线的GcInfo会记录子分支及租约(lease)要求的 LSN(参见 pageserver/src/tenant/timeline.rs 中的retain_lsns列表与lsn_covered_by_lease)。GC 计算出的最终 cutoff 必须"不低于"这些 retain 点,否则子分支将无法读取其分支点处的数据。分支点、快照点和 PITR 窗口共同组成了完整的保留约束集合。
保留策略相关的配置参数与默认值
RFC 将保留策略拆解为"用户可见的 PITR 周期 + 快照 + 自动快照",而 pageserver 在运行时通过一组 Tenant 级配置实现它们。以下是 libs/pageserver_api/src/config.rs 中定义、并反映在 pageserver 的 OpenAPI 规格 与 HTTP 路由 中的核心参数:
| 参数 | 默认值 | 类型 | 作用 |
|---|---|---|---|
pitr_interval | "7 days" | Duration(时长字符串) | PITR 周期,即按时间维度必须保留的历史长度;为 0 表示禁用 PITR 历史 |
gc_horizon | 64 * 1024 * 1024(64 MiB) | u64(字节) | 按空间维度必须保留的 WAL 字节数,防止历史过短导致空间阈值先于时间阈值触发 |
gc_period | "1 hr" | Duration | GC 调度周期,即每隔多久执行一次 GC 计算与执行 |
image_creation_threshold | 3 | usize | 某键的 WAL 记录条数超过该阈值时强制创建 image 层 |
image_creation_preempt_threshold | 3 | usize | 预占式 image 创建阈值(提前量) |
这些配置既可以通过 pageserver 的配置文件设置全局默认值(default_tenant_conf),也可以通过 Tenant API 按租户/时间线覆盖(TenantConfigPatch支持对gc_horizon、gc_period、pitr_interval、image_creation_threshold等字段做增量修改,见 libs/pageserver_api/src/models.rs)。也就是说,RFC 设想的"不同分支可配置不同策略"在 API 层面由"全局默认 + 租户级/时间线级覆盖"的分层模型支持。
GC 执行与调度:从 cutoff 到回收
结合 pageserver/src/tenant/timeline/compaction.rs 可以看到 GC 的整体执行流程:
- 计算 cutoff:根据
gc_horizon(空间)与pitr_interval(时间)得到GcCutoffs,再与分支保留点、租约 LSN 以及上一次的 applied cutoff 取交集,得到本次 GC 的水位线(get_gc_compaction_watermark即返回min(space_cutoff, time_cutoff, latest_gc_cutoff, standby_horizon))。 - 调度:
gc_period决定 GC 任务的触发频率;若 PITR cutoff 尚未计算出(None),则select_min()不允许回收任何数据——这保证了系统在尚未建立正确的水位线之前绝不清除历史。 - 执行回收:在
gc_compaction流程中,低于 cutoff 的层会被识别、重写或丢弃;只有当"层范围完全位于 cutoff 之下"且没有 retain LSN 引用时,才真正删除数据。执行完成后会更新 applied cutoff,并通过 index-only 上传将latest_gc_cutoff持久化到index_part.json,从而保证重启后仍能基于正确的 cutoff 继续访问(见 compaction.rs 中关于index_part.json的注释)。
提示:源码注释特别强调——如果 GC 尚未计算 PITR cutoff,就什么都不能回收("if we haven't computed the PITR cutoff yet, we can't GC anything")。因此在实际部署中,
pitr_interval与gc_period的取值需要保证 GC 能及时运行并持续产生有效的 cutoff。
从 RFC 到实现的关键映射总结
| RFC 011 中的概念 | pageserver 中的实现 |
|---|---|
| PITR 周期(时间范围) | TenantConfig::pitr_interval,转换为GcCutoffs.time |
| 快照 / 分支点(LSN 点) | GcInfo中的 retain LSN 列表、子分支引用、租约 |
| base image | image 层(L1/L2 image 文件),由image_creation_threshold等参数驱动生成 |
| WAL slice | delta 层 / WAL 文件,覆盖一段 LSN 区间 |
| 兜底:上传 S3 前的原始 WAL 保留 | safekeeper 的WalResidenceData(WAL 起始 LSN 与已上传 LSN 的跟踪) |
| 垃圾回收 | GC 任务按gc_period调度,依据min(space, time)cutoff 执行gc_compaction |
总结
Neon 的保留策略在用户侧表现为"PITR 周期 + 命名快照 + 自动快照",在系统内部则统一收敛为"LSN 范围(PITR)+ LSN 点(快照、分支点)"的集合。RFC 011 提出的这套模型至今仍是 pageserver GC 机制的设计骨架:GcCutoffs同时以空间(gc_horizon)与时间(pitr_interval)两个维度界定保留边界,base image 与 WAL slice 的组合提供了任意 LSN 点的页面重建能力,而分支点作为隐式保留点保证了分支/快照读取的正确性。
理解这一模型的价值在于:当你需要调整 Neon 的历史保留能力(延长 PITR、增加自动快照、或为节省存储缩短历史)时,最终都会反映为对pitr_interval、gc_horizon、gc_period等参数的配置;而所有这些参数的正确边界,都受制于 RFC 中所强调的"必须能重建保留范围内任意页面"以及"原始 WAL 必须安全进入 S3 之后才能被截断"这两条底层约束。
【免费下载链接】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),仅供参考