“多消费者日志保留前沿把这些要求合成可执行合同:各自登记最早需要位置,先持久保存可恢复进度再推进pin,仅删除结束位置不超过最小pin的完整段。前沿90只允许段长40的日志回收到80;新消费者…”
一条日志已经被数据库读过,未必可以删除:脏页恢复、长事务回滚、正在复制的备份和落后的副本可能仍要它。保留前沿把这些要求放到同一张表里。真正关键的不是求一次最小值,而是谁有资格推进这个最小值,以及推进后崩溃会从哪里重新开始。
形式陈述
一个连续日志域
沿用WAL的单一数据库身份、历史代际和严格递增位置。设已稳定日志的半开区间为[H,F):F是下一个位置,H是最早仍可读取的位置。原始记录可以变长,回收以完整物理段为单位;本页数字例每段40个位置,不把段边界误当事务边界。
每个有效消费者登记一个pin r,表示它从其持久恢复状态重新开始后,可能需要的日志全部位于[r,F)及未来追加部分,不会再请求r之前的内容。这是消费者必须证明的恢复合同,不由管理器看到“已读到100”自动推出。登记满足H≤r≤F,未登记的消费者不享有保留承诺。
可回收的逻辑前沿为
只有结束位置不大于R的完整段可以删除。若R位于某段中间,该段整体保留;实际H推进到已删除前缀的末端,通常H≤R。全部消费者为空时R=F,也仍只能回收完整稳定段。
持久注册表的原子操作
管理器提供可恢复、串行化的元数据操作,返回令牌(消费者名,代际)。这是一项明确的底层原子持久接口;附件用状态副本模拟它,不把普通Python字典写入叫作掉电持久化。
- 登记:在同一原子边界内检查r≥H、分配新代际并持久记录pin,然后才返回令牌。读取旧H后延迟登记,必须按登记当时的H重检
- 推进:消费者先使足以从r′继续的状态可靠落盘,保证以后不会回退到旧恢复点;管理器验证当前令牌和该完成凭证后,持久把r推进到r′≥r。仅收到、解析、在内存应用或发送确认,都不能替代所需持久状态
- 取消:持久撤销当前令牌的保留承诺;同名重建得到更大代际。取消之后,旧令牌的读、推进和完成发布全部拒绝。暂停或暂时不活跃本身不取消pin
- 回收:与以上操作串行化,读当前全部有效pin,选择完整段前缀并持久发布新的H,之后才回收其物理内容。若物理删除前崩溃,多保留一些废段安全;反过来先删字节后记H,会让旧元数据仍许诺已不存在的范围
消费者持久状态先前移、pin暂时未动,只会多保留。pin已经前移而持久状态仍旧,则可能少保留;这两个崩溃窗口不能对称处理。若取消pin是为节省空间,那个消费者以后恢复服务需新基线,不能拿原令牌悄悄从新H开始跳过历史。
必须登记哪些需要
数据库恢复者登记脏页重做与事务撤销各自的保守下界;正在进行的备份登记它的q;复制消费者登记从其持久状态重启所需的位置。多个用途可以合并成一个更小pin,也可分开,取min的安全性相同。长事务的撤销pin不能只因页面已刷净而删除;备份的源pin不能只因“已复制一部分页”便释放。
某个下界何时允许变大,由相应协议决定。比如日志已复制到独立可靠归档,消费者可改从归档读取,但必须在释放源pin前兑现可读性合同。注册表只保护本日志域;如果同时删除唯一归档,就需要把归档自身的消费者需要另行核算。
直觉
每个pin像一张还没交回的旧账页借条。最靠前的借条决定最老的哪一段仍不能扔。把“翻到下一页”写在易失便签上没有用:机器重启时若仍只能从旧页读起,就必须继续保留旧页。
代际解决同名重用问题。名叫backup的旧任务停顿后,新任务可以也叫backup;但旧消息不能替新任务推进进度或宣布完成。名字定位业务,代际定位本次承诺。
例子与边界
不是取最大的已读位置
稳定段为[0,40)、[40,80)、[80,120)、[120,160),F=160。四个有效pin为redo90、undo10、backup10、replica70,R=10,首段结束40>10,所以一段也不能删。虽然redo已经不需90之前的记录,其他消费者仍要10或70。
事务已完整结束,undo的需要消失;备份页、所需日志和manifest独立稳定,backup也完成,二者取消。此时R=min(90,70)=70,能删除[0,40),H=40,包含70的[40,80)仍保留。
replica先可靠保存恢复点100,再持久推进pin到100。R=min(90,100)=90,可以再删[40,80),H=80;[80,120)因为含redo需要的90而继续保留。逻辑前沿90与物理回收点80不同。
先报进度,再掉电
另一次错误运行中,replica只在内存处理到100,磁盘恢复点仍70,却把pin先推进100。管理器据此允许H到80;掉电丢失内存后,该副本需要70,而它已不存在。单看注册表的min计算没有算错,错在消费者提前作了它尚不能兑现的承诺。
正确次序是持久进度D→持久pin推进P→回收G。在D之后P之前崩溃,旧pin70仍保护旧段,恢复者可从100继续;在P之后G之前崩溃,进度和pin均支持100,保留多余段也安全。把G挪到P之前只会按旧pin保守回收;把P挪到D之前则会打开上述丢失窗口。
新消费者来得太晚
H=80时,新消费者要求r=50必须返回“所需前缀已回收”,不能登记成功。若它手里已有可恢复到80的新基线,则可以从r=80登记,此后包含80的段又被保护。没有这样的基线,改填80只是把缺口藏起来。
若登记者先看到H40,随后回收者推进H80,最后它才请求r50,原子重检会拒绝。反向次序中,r50先成功登记,回收者看见它后最多推进H40,不能越过50。两种合法次序都安全;将检查和持久登记拆成两次无保护动作才会出错。
推论与应用
安全性与进展是两回事
对有效消费者c,有r_c≥R≥H;删除只发生在R之前,所以它承诺会需要的区间没有被回收。新登记与回收串行化,避免需要先观察、后登记的竞态。推进前持久恢复状态已经支持新位置,重启也不会制造一个更早的合法请求。三个条件共同维持不变量,缺一个都不能只靠min公式补救。
慢消费者可能让磁盘持续增长。本页安全协议没有承诺空间有界或所有消费者都进展。系统可限流、扩容,或明确撤销消费者并要求重新建立基线;不能既强制丢日志,又声称原保留合同仍有效。PostgreSQL的槽状态区分保留中与lost,提醒使用者检查这一失败边界,[1,restart_lsn与wal_status] 但本页不是其槽实现副本。
管理成本与可操作检查
若有c个有效消费者,本附件每次回收顺序扫描pin,求R花O(1+c);登记、令牌检查和推进为期望O(1)字典操作,回收d个完整段再付O(d)。消费者持久状态写入、真实删除字节和注册表底层事务成本另算。维护最小堆可以改变扫描成本,却还需处理陈旧代际记录;本文保留小而可核对的线性接口。
有效状态占O(1+c),每个曾用消费者名另留一个代际计数;取消时删除该代的持久进度条目。回收返回d个已删段的列表,另占O(d)。测试若保存每一步完整注册表快照,还须另付O(c)记录成本;附件核心推进本身不作这项复制。
练习把replica取消后同名重建,向新任务发送旧代际的推进及完成消息;两者都应拒绝。再让redo也完成,核全部pin为空时R=F和整段边界。最后把F置于段中间,解释最后一个未满段为什么仍保留。包含掉电顺序和具体删除集合的完整入口见恢复与保留终点。
参考资料
- PostgreSQL 18,pg_replication_slots,Table53.20的restart_lsn、confirmed_flush_lsn和wal_status;Replication,max_slot_wal_keep_size。字段及失效边界用于产品对照,本文持久pin状态机为教学构造
- C. Mohan et al.,ARIES原论文,§5.2印刷p.120的回滚日志空间、§8印刷pp.131–133的镜像与介质恢复下界;不同恢复用途需要不同旧日志,不能只看最近checkpoint