Checkpoint 与 Recovery:当前恢复语义的实现方式

基于当前仓库实现分析 checkpoint sidecar、active log verified recovery scan 以及残缺/过旧 checkpoint 的保守恢复逻辑。

先说结论

当前项目里的恢复逻辑,不是“周期性 checkpoint + 从 checkpoint 增量扫描”的那一套通用 WAL 教材实现。

真实情况更接近:

  1. active log 始终是恢复事实来源
  2. checkpoint 是一个 sidecar,加速和补充恢复状态
  3. 如果 checkpoint 残缺或过旧,恢复逻辑会保守回退到 verified scan 结果
  4. 恢复目标优先是“不误裁有效数据,也不把损坏尾巴继续保留”

这和很多理想化设计图差别很大,先把这个边界说清楚很重要。

当前 checkpoint 里保存什么

项目当前 checkpoint 保存的是一个很小的状态:

CheckpointState{
    .logical_size = _append_writer.logical_size(),
    .sequence = _sequence,
    .rotation_index = _rotation_index,
}

也就是说,核心字段只有:

  • logical_size
  • sequence
  • rotation_index

对应文件格式是简单的 key=value 文本。

checkpoint 什么时候写

当前实现里没有 checkpoint_interval_seconds,也没有周期性 checkpoint timer。

实际写入时机主要是:

1. 启动时

如果:

truncate_on_start == true && checkpoint_enabled == true

则启动后会立刻持久化一次 checkpoint。

2. rotate 后

当 active log 发生 rotate,重新打开新 segment 后,如果启用了 checkpoint,就会写一次 checkpoint。

3. stop 时

writer 停止流程中,在最终 flush_tail(true) 之后,如果启用了 checkpoint,会写一次最终 checkpoint。

4. 恢复完成后

如果是 truncate_on_start=false 的恢复启动,恢复逻辑完成后也会重新落一次 checkpoint。

Rendering diagram...

启动恢复的入口

当前启动逻辑是:

truncate_on_start = true

  • 不做恢复
  • 如启用了 checkpoint,则写入一个新的 checkpoint

truncate_on_start = false

  • 调用 recover_from_checkpoint()
  • 其内部进一步走 LogManager::recover_active_file(...)

也就是说,“是否恢复”首先由 truncate_on_start 决定。

当前恢复的核心思路

AsyncWriter::recover_from_checkpoint() 当前很薄:

  1. 调用 recover_active_file()
  2. 拿到恢复后的:
    • logical_size
    • tail_buffer
    • sequence
    • rotation_index
  3. 调用 AppendWriter::truncate_to(...)
  4. 如启用 checkpoint,再重新持久化一次

真正复杂的判断在 LogManager 中。

Rendering diagram...

verified scan 的作用

当前 active log 恢复并不是“盲信 checkpoint logical_size”,而是会对 active file 做 verified scan。

这个 scan 的目标是:

  1. 找到当前 active log 中有效记录的逻辑终点
  2. 判断尾部是否存在损坏 / 残缺记录
  3. 产出恢复后应保留的 logical_size
  4. 必要时保留 tail buffer 供 AppendWriter 恢复内部状态

恢复逻辑的原则是:

active log 本身是第一事实来源,checkpoint 只能在与 verified 结果不冲突时提供辅助信息。

残缺 checkpoint 如何处理

这是当前项目恢复语义中最关键的一点之一。

以前的危险做法是:

  • 只要能打开 checkpoint 文件,就当它可用

但这会导致部分写入或截断 checkpoint 把默认值 0 带进恢复逻辑,进而错误裁掉有效 active log。

当前修正后的语义是:

  1. 只有当 logical_size / sequence / rotation_index 三个 key 都存在时,checkpoint 才视为完整
  2. 若缺失任意字段,则直接忽略 checkpoint
  3. 恢复回退到 active log 的 verified scan 结果

也就是说,当前是“宁可不用 checkpoint,也不允许残缺 checkpoint 伤害有效日志”。

过旧 checkpoint 如何处理

另一个关键点是“完整但过旧”的 checkpoint。

危险场景是:

  1. checkpoint 文件结构完整
  2. 但它只覆盖到更早的 active log 状态
  3. 若直接采用它的 logical_size 或 sequence,可能把已经有效落盘的新记录裁掉

当前项目的保守语义是:

  1. 先跑 verified scan 得到 active log 的真实有效终点
  2. 只有当 checkpoint 的 logical_size 与 verified 结果完全一致时,才采用 checkpoint 的 sequence / rotation_index
  3. 否则一律以 verified scan 为准

这意味着 checkpoint 不再有权“向后回退有效边界”。

恢复后的文件状态如何落地

恢复完成后,AppendWriter::truncate_to() 会:

  1. 设置 _logical_size
  2. 清空旧 _tail_chunks
  3. 根据恢复结果重新挂载 tail_buffer
  4. _write_offset 设置为 logical_size - tail_bytes
  5. 如文件已打开,则 truncate(_logical_size)

这样做的结果是:

  1. 逻辑长度回到 verified 的有效终点
  2. 仍允许未满对齐块的尾巴以 tail buffer 形式继续存在
  3. 后续 append 可以自然续写,不需要重新从头编码整个文件

与 rotate / checkpoint 的关系

当前 rotate 之后会:

  1. flush 当前 tail
  2. close active file
  3. rotate active segment
  4. reset AppendWriter
  5. 打开新的 active segment
  6. 如启用 checkpoint,写入新的 checkpoint

因此 checkpoint 和 rotate 的关系更像:

  • rotate 之后 checkpoint 用于记录“新 active segment 的当前状态”

而不是“每次 rotate 前后都做复杂快照链管理”。

故障场景下的当前保证

1. active log 尾部残缺

恢复时 verified scan 会把有效前缀找出来,并把尾部坏数据排除掉。

2. checkpoint 文件残缺

直接忽略 checkpoint,回退到 verified scan。

3. checkpoint 文件完整但过旧

不会让旧 checkpoint 把有效 active log 回退到更小的 logical size。

4. gzip / archive 问题

archive 侧问题不应破坏 active log 恢复;查询链路会以保守读语义处理损坏 gzip。

当前实现里没有的内容

为了更清楚地界定恢复路径的实现边界,这里把当前未纳入的机制单独列出来:

  • 没有 checkpoint_interval_seconds
  • 没有周期性 checkpoint timer
  • 没有 checkpoint_max_age_seconds
  • 没有 recovery_trailing_capacity
  • 没有“从 checkpoint offset 开始流式增量扫描”的独立恢复子系统
  • 没有多级 checkpoint 选择器

这些都是可能的未来演进方向,但不是现在的代码。

当前最值得强调的恢复哲学

当前这套恢复逻辑最有价值的地方,不在于“功能很多”,而在于它的保守性。

可以概括成三条:

  1. checkpoint 只能帮助恢复,不能伤害恢复
  2. active log verified scan 的优先级高于 sidecar 推断
  3. 不完整、过旧、冲突的 checkpoint 一律宁可丢弃

这比“checkpoint 越多越强”更符合当前项目的工程现实。

配置上该怎么理解

当前和恢复直接相关的核心开关其实只有:

bool truncate_on_start = true;
bool checkpoint_enabled = false;

典型组合

1. 开发/压测场景

truncate_on_start = true
checkpoint_enabled = false

每次启动都从空状态开始。

2. 可恢复场景

truncate_on_start = false
checkpoint_enabled = true

启动时执行恢复,并在恢复后保留 checkpoint sidecar。

3. 仅恢复但不保留 checkpoint

truncate_on_start = false
checkpoint_enabled = false

仍然会按 active log 做恢复,但不会重新写 checkpoint。

总结

当前项目里的 checkpoint/recovery 机制,最准确的描述不是“定时 checkpoint 驱动的增量恢复”,而是:

  1. active log verified scan 为核心
  2. checkpoint 作为 sidecar 提供辅助状态
  3. 残缺和过旧 checkpoint 都会被保守忽略
  4. 恢复后的 logical size、tail buffer、sequence、rotation_index 会重新对齐到可信状态

如果用一句话概括:

当前恢复模型的核心目标不是“最大化利用 checkpoint”,而是“在 checkpoint 不可靠时,仍然优先保住 active log 的有效前缀”。


下一篇:《Rotate 与 Archive:日志生命周期管理》