新闻详情

新闻详情

首页 / 资讯中心 / 详情

Databasus 物理备份 WAL 保留策略解析:为什么边界 WAL 段绝不能被当作孤儿删除

发布时间:2026/9/25 11:51:40来源:尧图网络
Databasus 物理备份 WAL 保留策略解析:为什么边界 WAL 段绝不能被当作孤儿删除
数据库灾备【免费下载链接】databasusPostgreSQL backup tool with Point-In-Time-Recovery and restore verification项目地址https://gitcode.com/gh_mirrors/po/databasus点击查看免费下载本文基于 Databasus 仓库中已归档的变更规范physical-backups/wal-retention完整讲解 PostgreSQL 物理备份场景下哪些已归档 WAL 段必须保留、哪些可以删除的判定规则。读完后你会掌握边界 WAL 段误删 bug 的 LSN 对齐根因、以end_lsn为锚点的孤儿判定谓词、FULL 备份调度与删除路径之间的 advisory lock 串行化机制以及级联删除如何保护存活前驱备份所需的共享 WAL。一、背景一个让 PITR 静默失效的边界段删除 bug该规范出自一个真实的现场故障修复见归档变更 proposal.md。故障现象是FULL 物理备份刚报告成功PITR按时间点恢复就在几秒后不可用——因为 WAL 孤儿清扫orphan sweep删掉了恰好承载该 FULLstart_lsn和stop_lsn的那个 WAL 段。根因是两类 LSN 的对齐方式不同WAL 段的start_lsn/end_lsn由文件名推导而来见 wal_upload.go 中的segmentBounds因此永远是段大小默认 16 MB的整数倍例如0/AB000000FULL 的start_lsn是段内一个真实恢复位点位于长页头之后的第一条记录处例如0/AB000028偏移0x28。旧版孤儿谓词只保留满足f.start_lsn w.start_lsn某 COMPLETED FULL 的起始位点不超过段起始的段。对边界段而言0/AB000028 0/AB000000恒为假于是承载 FULL 起止位点的那一个段在每一次真实场景下都被判为孤儿。用户报告中连续 5 次复现start_lsn全部带0x28偏移0/8F000028、0/92000028等且清理日志紧跟在上传日志之后两秒出现。删除发生后的症状极具迷惑性ResolveRestoreSet无法越过 FULL 的stop_lsn报wal gap before target; latest restorable point is stop_lsn而界面仍显示 FULL 和 WAL 归档均成功——用户以为 PITR 可用恢复链其实已经断裂。更糟的是爆炸半径随时间扩大该谓词被任意更老的COMPLETED FULL 满足当保留策略剪掉最老的链后下一条链的边界段随即成为新的孤儿被删昨天还能恢复、今天不能了。二、规范核心以恢复链为中心的 WAL 保留规则spec.md 的总纲Purpose只有一句话但它是整个规范的灵魂一个 WAL 段只有在没有任何保留中的备份需要它时才是可弃的A segment is expendable only when no retained backup needs it。规范用四条 Requirement、十个 Scenario 把这条原则落成可验收的行为。2.1 要求一覆盖保留中备份恢复窗口的 WAL 段永不被当作未引用保留清扫只允许在同一条时间线上没有任何保留的、成功完成的物理 FULL 备份的起点早于该段结束位置时才把一个段判为未引用。规范特别解释了为什么恢复位点不会落在段边界上FULL 总是从所在段的内部一小段距离处开始因此承载它的段必然早于备份开始并且它还承载着备份结束之后的所有写入——这个段必须保留。三个验收场景场景条件预期行为FULL 的起止位点落在同一个段内该段的起止都在一个 WAL 段内部且段已归档保留清扫不动该段恢复到备份结束位点之后、被归档 WAL 覆盖的任意时间点都成功段完全早于所有保留中的备份某归档段的end_lsn不晚于其时间线上最老的保留 FULL 的起点该段作为未引用被删除锚定备份被剪除保留策略删掉最老的 FULL而更晚的 FULL 的起点落在被删备份原先锚定的段内该段仍被保留因为剩余备份的起点早于段结束从剩余备份出发的 PITR 依然可用注意第三个场景它要求锚定关系不绑定在某个具体 FULL 行上而是绑定在是否存在任何更早起点的 COMPLETED FULL这一集合性质上。2.2 要求二成功的 FULL 必须留下不间断的恢复链一旦物理 FULL 报告成功且 WAL 流保持健康系统必须从该备份的结束位点起保持归档 WAL 连续直到备份被保留策略移除任何后台维护都不允许在成功备份之后立即引入缺口。备份完成后数秒即运行保留归档 WAL 从备份结束位点到最新归档段必须无缺口报告的最新可恢复点必须晚于而非等于备份结束位点latest restorable point恰好等于 FULLstop_lsn正是现场故障的签名症状恢复到备份后几秒的时间点只要覆盖该时间点的 WAL 已归档恢复应执行而不是因 WAL 缺口被拒绝在更新的 FULL 正在运行时恢复PITR 应选用前一个已完成的 FULL 作为基础恢复集应包含从该基础出发的全部可用连续 WAL 连续段包括越过新 FULL 起止位点的部分。2.3 要求三FULL 备份执行窗口内归档的 WAL 受保护只要某数据库存在活跃 FULL 备份声明active FULL claim任何孤儿或级联删除都不允许移除它的 WAL。具体机制要求调度声明与所有删 WAL 的维护路径按数据库粒度串行化同一 advisory lock且每个删除事务在真正删段之前重新检查声明。所有备份都被删光、替换 FULL 正在跑孤儿清扫必须留下 WAL等 FULL 完成发布锚点后自然被锚定存在IN_PROGRESS行但没有活跃声明声明claim才是活跃工作的唯一事实来源孤立的IN_PROGRESS行不能保护 WAL孤儿清扫可以回收级联删除与替换 FULL 声明提交重叠级联删除必须等待声明创建完成观察到活跃声明后保留 WALFULL 整链移除还要保留 timeline history直到活跃 FULL 发布完成锚点该声明存活期间删除预览preview必须报告可删 WAL 与 timeline history 均为零。2.4 要求四删除 FULL 备份时保护存活前驱所需的 WAL删除一个已完成的 FULL 时若同一数据库同一时间线上还有更老的已完成 FULL则必须保留其 WAL——因为删除后那个更老的备份成为恢复基础必须仍能重放这段共享 WAL。且删除预览必须与破坏性操作使用同一判定报告相同的 WAL 数量与大小。规范同时划定了例外边界FULL_BACKUPS保留策略可以有意保留一个 FULL 作为独立恢复点同时剥掉它的增量备份和自有 WAL——前驱保护规则只在 FULL 行本身被删除时适用。删除较新的 FULL只要更老的 FULL 还在删除不带走任何 WAL被删 FULL 原区间内的 PITR 从更老的 FULL 出发成功删除时间线上最早的 FULL删除只移除完全包含在其所有权区间ownership span内的段跨越下一个 FULL 起点位点的那一个段必须留下。三、源码实现规范是如何落地的3.1 谓词修正与段尾比较而不是段首修正后的孤儿判定在 wal_segment_repository.go 的FindOrphans中用反连接实现SELECT * FROM physical_wal_segments w WHERE w.database_id ? AND NOT EXISTS ( SELECT 1 FROM physical_full_backups f WHERE f.database_id w.database_id AND f.timeline_id w.timeline_id AND f.start_lsn IS NOT NULL AND f.start_lsn w.end_lsn AND f.status ? -- COMPLETED ) ORDER BY w.start_lsn ASC读法很直白只要同时间线上存在任何 COMPLETED FULL其start_lsn早于该段的end_lsn该段就被引用。源码注释L81-L88把为什么锚定end_lsn写得很清楚FULL 的start_lsn是段内真实恢复位点而段自身边界来自文件名、是文件对齐的若与start_lsn比较就会把 FULL 自己起止所在的段判成孤儿。design.md 记录了三个被否决的替代方案值得一读把 FULL 的start_lsn向下取整到段边界再比较——算术上等价但会把 16 MB 段大小写进 SQL段大小是运行时可配的walmath.SetWalSize要么把值穿进查询要么硬编码保留旧谓词、在清理器里特判包含 FULLstart_lsn的段——两处编码同一不变量FindOrphans的下一个调用者会继承这个 bug无条件保留最新 N 个段——掩盖了错误谓词且保留集取决于到达顺序而非恢复需求。对比之下同一文件上方几行的FindByChainSpanL53-L79早就用重叠判定end_lsn startLSN AND start_lsn endLSN正确表达了链覆盖其注释同样点明FULL 起点通常在段中排除文件边界起点低于它的段会制造恢复重放窗口起点的假 WAL 缺口。修复的本质就是让FindOrphans与FindByChainSpan达成一致。另一个被否决的方案是把增量备份也加进反连接作为锚点增量的区间本就在根 FULL 锚定的链内不会多保留任何段却会把注释里已标记为顺序扫描的反连接成本翻倍。3.2 锁与重检FULL 调度、孤儿删除、级联删除三方串行化coordination.go 提供了三块积木AcquireBackupAndOrphanCleanupLockL14-L19以physical-backup: || databaseID哈希为键的事务级 advisory lockpg_advisory_xact_lock按数据库粒度把 FULL 声明创建与一切删 WAL 路径串行化HasInFlightFullBackupL21-L35查询physical_in_flight_backups中该库是否存在backup_type FULL的活跃声明HasCompletedFullBackupCoveringWalL37-L59以与FindOrphans相同的start_lsn end_lsn谓词复核是否存在 COMPLETED FULL 覆盖该 WAL 尾端。于是删除一个孤儿段的事务是三段式先拿锁再查活跃 FULL 声明有则中止删除最后复核完成锚点。调度器在插入 FULL 声明前先拿同一把锁级联删除在锁住目标 FULL 之前也拿这把锁——从源码结构看这封死了数据库从无锚定过渡到活跃 FULL的瞬间段被删掉的竞态窗口。这也解释了规范中预览与破坏性路径一致的要求预览GetDependentsSummary取同样的锁、看同样的声明声明存活时必然报告零可删 WAL 与 timeline history。3.3 恢复可见性与保留所有权解耦规范第二个要求成功备份后链不间断落在 restore_set.go 的ResolveRestoreSetL41-L93selectRestoreChainL221-L238按CompletedAt选基础链——PITR 取完成时间不晚于目标的最新链。这意味着较新的 FULL 还在跑时PITR 自动落到前一个已完成的 FULL正对应规范场景Restoring while a newer full backup is runningWAL 查询不再受该链的保留边界retention span截断而是从chain.Span.Start查到LSNMaxL61-L66拿到整段连续 WALcontiguousWalRunL275-L302从上一个备份的stop_lsn向前走收集到第一个 LSN 缺口为止的连续段并报告最远可重放位点。目标时间晚于该位点时抛出WalGapBeforeTargetError携带LatestRestorableLSN——界面上最新可恢复点就来自这里。设计文档解释了为什么所有权边界不改成successor.stop_lsn会让两条链的删除区间重叠也为什么不按第一个接收时间晚于目标的段截断恢复集接收时间不能证明该段包含目标时间戳之后的事务。保留侧的GetChainSpan仍维持互不重叠的所有权区间[full.start_lsn, successor.start_lsn)两条线各司其职。3.4 清扫器全貌与删除预算cleaner.go 的PhysicalBackupCleaner每个 tick 跑两遍L40-L70保留策略清理与孤儿清扫。孤儿清扫路径cleanOrphans→cleanOrphanWalForDatabaseL492-L553先用FindWalOrphansByDatabase找出候选再对每个段以单段区间[start_lsn, end_lsn)调DeleteOrphanWalSegmentsInSpan——由于段之间永不相交单段区间精确命中该孤儿不会误伤任何链覆盖的 WAL。每 tick 的 WAL 删除字节预算锚定在最新 COMPLETED FULL 的大小上walDeleteBudgetMBL203-L219避免一次删光。保留策略侧支持三种模式RetentionChains保留最新 N 条不可延展链、RetentionFullBackupsLAST_N 或 GFS 分层保留 FULL被保留的 FULL 走DeleteChainDependentsKeepFull剥掉增量与 WAL、自身留作独立恢复点、RetentionChainsAndFullBackups两者并集。这里正好对应规范第四要求划出的例外FULL_BACKUPS策略剥掉自有 WAL 是有意为之因为 FULL 行还在前驱保护不适用而整链删除走DeleteFull当更老的前驱存活时不带走共享 WAL无存活前驱时只删完全包含在所有权区间内的段tasks 记录中对应把deleteWalInSpanBudgeted的边界从start_lsn span.End收紧为end_lsn span.End见 tasks.md 2.4 项。四、回归测试为什么旧测试没抓住这个 bugdesign.md 里有一段非常精彩的失败分析既有测试chain_view/service_test.go:343和cleaner_test.go:249都把 FULL 的start_lsn种子成段对齐值如4*segmentBytes。段对齐恰好是旧谓词碰巧正确的唯一情形——真实世界里 FULL 永远落在段中。因此回归覆盖刻意新增段中start_lsn的用例让关键字偏移活在 fixture 里而不是被默认掉且测试名直接写明条件WhenFullStartsMidSegment防止后续重构悄悄把 fixture 拉回对齐值。端到端层面修复还补了一个备份→清扫→恢复完整时序场景取 FULL、用seedChainAndStreamPastTarget把 WAL 流过目标时间、从shared包内导出的单遍清理入口跑一轮清扫原先唯一导出入口Run是无限 ticker、二次调用会 panic、再向 API 申请该时间点的恢复令牌断言令牌被签发而非以 422 拒绝并接入 pg17 与 pg18 两个分发器版本。设计文档还否决了断言日志输出deleted orphan wal segment的方案——那测的是消息而不是结果任何静默删除的重构都会让它误通过。五、运维注意升级与既有损伤无 schema 变更、无数据迁移部署就是普通升级回滚即恢复旧谓词连带恢复旧的错误删除行为不需要任何数据步骤已经受损的库无法靠代码修复被删的 WAL 已从存储消失且复制槽推进后 PostgreSQL 也已从pg_wal回收。升级后必须重新做一次 FULL 备份才能恢复 PITR 能力——这是 tasks 中要求写入 PR 描述的升级说明5.3 项可观测的损伤签名latest restorable point恰好等于某个 FULL 的stop_lsn修复的代价是每条时间线最老的 COMPLETED FULL 附近多保留一个 16 MB 段——反连接按timeline_id匹配边界是每时间线而非每数据库。设计文档明确表态保留边界段是正确性而不是保险裕量且刻意不引入额外保留 N 段这类余量。六、延伸阅读规范正文spec.md动机与现场报告proposal.md设计决策与被否决方案design.md任务分解tasks.md孤儿谓词实现wal_segment_repository.go锁与声明重检coordination.go恢复集解析restore_set.go保留/孤儿清扫器cleaner.go同批归档的相关变更恢复位点授予与复制用户权限、故障后自动重新锚定2026-09-04-grant-wal-switch-to-replication-user、2026-09-04-automatic-physical-reanchor-after-failover赞分享数据库灾备【免费下载链接】databasusPostgreSQL backup tool with Point-In-Time-Recovery and restore verification项目地址https://gitcode.com/gh_mirrors/po/databasus点击查看免费下载相关推荐Databasus 物理备份 WAL 边界段修复为什么孤儿清扫会删掉 FULL 备份的起点段以及如何用一条 LSN 谓词修正它Databasus 物理备份 WAL 边界段修复为什么孤儿清扫会删掉 FULL 备份的起点段以及如何用一条 LSN 谓词修正它 本文基于 Databasus数据库灾备Databasus 物理备份 PITR 修复剖析边界 WAL 段为何会被孤儿清理误删以及 end_lsn 谓词如何救回恢复链Databasus 物理备份 PITR 修复剖析边界 WAL 段为何会被孤儿清理误删以及 end_lsn 谓词如何救回恢复链 Databasus 是支持 P数据库灾备Cataclysm-DDA末日生存首夜的 6 条避坑实战笔记Cataclysm DDA末日生存首夜的 6 条避坑实战笔记 你从一辆翻倒的皮卡里爬出来背包里半瓶水、一把生锈的螺丝刀三个街区外丧尸群正在慢慢转头。这就是游戏开发上一篇Mod Organizer 2终极指南3步掌握专业游戏模组管理技巧下一篇DeepL Chrome翻译插件3分钟快速安装与高效使用指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

Windows 也能用 Claude Code?配合 Kimi K2 与 TaoToken,AI 编程体验直接起飞! 2026/9/26 2:33:36

Windows 也能用 Claude Code?配合 Kimi K2 与 TaoToken,AI 编程体验直接起飞!

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

阅读更多 →
Windows SmartScreen下载拦截全解析:从临时放行到组策略白名单 2026/9/26 2:33:36

Windows SmartScreen下载拦截全解析:从临时放行到组策略白名单

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

阅读更多 →
Jev模型深度拆解:推理提速20-200倍的轻量方案与实战指南 2026/9/26 2:33:29

Jev模型深度拆解:推理提速20-200倍的轻量方案与实战指南

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

阅读更多 →
Sa-Token JSON Body验签实战:从原始报文到国密SM2防重放 2026/9/26 2:33:29

Sa-Token JSON Body验签实战:从原始报文到国密SM2防重放

做服务端开发的人,应该都有过这种经历:对接第三方开放平台、支付回调或者公司内部服务联调的时候,对方丢过来一段 JSON 报文,要求“先验签,再处理”。我前阵子就遇到一个需求,系统权限体系用的是 Sa-Token&…

阅读更多 →
CTF Wiki 贡献文档要求解析:从内容格式、结构合理性到仓库存储的完整规范 2026/9/26 2:33:23

CTF Wiki 贡献文档要求解析:从内容格式、结构合理性到仓库存储的完整规范

文档网络安全教程 【免费下载链接】ctf-wiki Come and join us, we need you! 项目地址: https://gitcode.com/gh_mirrors/ct/ctf-wiki 点击查看 免费下载 CTF Wiki 是一个面向 CTF 学习者的开源安全知识库,其知识文档由社区贡献者共同维护。为了让每一…

阅读更多 →
弹幕网站的鼻祖NABC,谁排第一? 2026/9/26 2:33:16

弹幕网站的鼻祖NABC,谁排第一?

弹幕文化如今已是视频网站的标配,但追根溯源,这个“弹幕家族”的四位元老——NABC,各自的位置其实早已注定。N站(NICONICO):真正的开创者,没有争议的第一2006年12月12日,日本niconic…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉