跳转至

Database Logging 学习笔记

CMU 15-445/645 Intro to Database Systems — Recovery I: Database Logging

关于本笔记

本讲解释数据库系统在正常运行时需要额外记录什么信息,才能在崩溃后恢复到正确状态。下一讲的 ARIES 会使用这里介绍的 WAL、LSN、Checkpoint 等机制完成真正的恢复流程。

1 恢复问题

数据库通常采用 disk-oriented 架构:页面从非易失存储读入 buffer pool,在内存中修改,再异步写回磁盘。问题在于内存会在崩溃时丢失:

  • 已提交事务的修改可能只在内存中,违反 Durability。
  • 未提交事务的部分修改可能已经写入磁盘,违反 Atomicity。

恢复算法包含两部分:

  1. 正常运行时:记录足够的日志和元数据,为崩溃恢复做准备。
  2. 崩溃后:根据日志判断哪些修改需要重做,哪些修改需要撤销。

1.1 UNDO 与 REDO

操作 目标 典型场景
UNDO 移除未成功提交事务的影响 未提交事务的脏页已经落盘
REDO 重新应用已提交事务的影响 已提交事务的脏页尚未落盘

核心判断

是否需要 UNDO/REDO,不取决于事务本身,而取决于 buffer pool 是否允许脏页提前落盘,以及提交时是否强制刷脏页。

2 Buffer Pool 策略

恢复复杂度由两组策略决定:

策略 含义 恢复影响
Steal 允许未提交事务修改过的脏页被写回磁盘 需要 UNDO
No-Steal 未提交事务修改过的脏页不能写回磁盘 不需要 UNDO
Force 事务提交前必须把所有修改页写回磁盘 不需要 REDO
No-Force 提交时只保证日志落盘,不强制刷所有数据页 需要 REDO

组合结果:

组合 需要 UNDO 需要 REDO 特点
No-Steal + Force 否 否 恢复最简单,但运行时开销最大
No-Steal + No-Force 否 是 不能提前回收脏页
Steal + Force 是 否 提交慢,仍要处理未提交修改
Steal + No-Force 是 是 性能最好,现代系统常用

3 Shadow Paging

Shadow Paging 是 No-Steal + Force 的一种实现方式。它不依赖 WAL 来恢复页面,而是通过 copy-on-write 同时维护两个数据库版本:

  • Master Page Table:只指向已提交版本。
  • Shadow Page Table:事务修改时通过 copy-on-write 复制页面,并把新版本挂到 shadow table。

3.1 基本流程

  1. 事务开始时,shadow page table 复制 master page table 的映射关系;此时两者都指向同一批物理页。
  2. 当事务第一次修改某个页面时,DBMS 复制该页,修改复制后的页面,并让 shadow page table 指向新页。
  3. 读事务仍通过 master page table 读取旧的已提交版本;写事务只修改 shadow 版本。
  4. 事务提交时,DBMS 先把 shadow page table 和所有 shadow dirty pages 刷到磁盘。
  5. 最后原子地切换 master pointer,使其指向 shadow page table。切换完成后,shadow 版本成为新的 master 版本。
  6. 如果事务回滚,直接丢弃 shadow page table 和 shadow pages,master pointer 不变。

为什么恢复简单

崩溃后只需要检查 master pointer。若提交时的原子切换已经完成,master 指向新版本;若没有完成,master 仍指向旧版本。因此不需要 REDO,也不需要对未提交事务执行 UNDO。

3.2 树状 page table

直接复制整张 page table 代价很高。实际实现通常把 page table 组织成树,根节点是固定位置上的一个小页面或根指针:

  • master root 指向当前已提交数据库版本。
  • shadow root 指向事务的临时版本。
  • 修改叶子页时,只复制从根到该叶子的路径和被修改的数据页,不需要复制整棵树。
  • 提交时的关键动作是把固定位置的 root 原子更新为 shadow root。

这种设计的本质是把“事务是否提交”压缩成一次 root 切换:root 切换前,所有 shadow 更新都不是数据库的正式状态;root 切换后,这些更新整体生效。

3.3 成本与局限

优点 缺点
回滚和恢复简单 复制 page table 和脏页开销高
不需要复杂日志 提交时必须刷出所有修改
崩溃后只需看 master pointer 会造成磁盘碎片,不利于顺序扫描
适合单写者场景 多事务并发提交难处理

Shadow Paging 的提交开销集中在两件事上:刷出被修改的新页面,以及刷出 page table 或 root 相关页面。由于新页面往往分布在磁盘不同位置,它会破坏原本连续的数据布局,导致顺序扫描退化。旧版本页面还需要后续垃圾回收,否则空间会持续膨胀。

SQLite 早期的 rollback journal 与该思路相近:修改页面前先把原始页面写入 journal,崩溃后用 journal 把未完成事务的页面拷贝回去。

4 Write-Ahead Logging

现代数据库通常使用 Write-Ahead Logging (WAL)。WAL 把随机数据页写入转换成顺序日志追加:事务修改页面前先生成日志记录,提交时只需保证相关日志已写入非易失存储。

WAL 有两条关键规则:

  1. 数据页写回磁盘前,必须先把该页对应的日志记录写入磁盘。
  2. 事务返回 commit 成功前,必须先把该事务的所有日志记录和 commit record 写入磁盘。

典型日志记录包含:

<txn_id, object_id/page_id, before_value, after_value>

其中 before_value 支持 UNDO,after_value 支持 REDO。真实系统还会记录 LSN、校验和、时间戳、页面偏移、slot 等信息。

提交流程

T1 更新 A 和 B 时,系统先把两条 update log record 追加到 log buffer,再修改 buffer pool 中的页面。T1 提交时追加 commit record,并把日志刷到磁盘。即使数据页还没落盘,崩溃后也能通过 WAL 重放 A 和 B 的修改。

4.1 Group Commit

如果每个事务提交都单独 fsync,磁盘延迟会成为瓶颈。Group commit 会把多个事务在短时间窗口内的日志一起刷盘:

  • 第一个事务可能多等几毫秒。
  • 多个提交共享一次顺序写和一次 flush。
  • 吞吐量提高,但提交延迟可能略微增加。

5 日志粒度

类型 记录内容 优点 缺点
Physical Logging 页面/字节级 before/after image 恢复确定,易重放 日志量大
Logical Logging 原始 SQL 或逻辑操作 日志量小 需要确定性重放,恢复可能很慢
Physiological Logging 指定 page,再记录 page 内逻辑位置或 slot 折中,最常见 仍需维护页面级元数据

多数磁盘型 DBMS 采用 physiological logging:跨页面时保持物理定位,页面内部允许通过 slot array 等结构重新组织。

6 Checkpoint

WAL 不能无限增长,否则崩溃后可能要重放多年日志。Checkpoint 的作用是缩短恢复起点:

6.1 为什么需要 Checkpoint

如果没有 checkpoint,恢复时必须从日志开头扫描到崩溃点:

  • 找出哪些事务已经提交,哪些事务没有完成。
  • REDO 已提交但可能尚未落盘的修改。
  • UNDO 未提交但可能已经落盘的修改。

长期运行的系统会产生大量 WAL。即使很多老事务的修改早已刷到数据页,恢复仍要重复扫描这些无用历史,恢复时间不可控。Checkpoint 的目标就是把“确定已经安全的历史”变成恢复时可以跳过的边界。

6.2 Consistent Checkpoint

最直接的方案是 consistent checkpoint,也就是阻塞式检查点:

  1. 阻止新事务开始。
  2. 等待当前活跃事务结束,或暂停所有事务更新。
  3. 将 log buffer 刷入稳定存储。
  4. 将 buffer pool 中所有 dirty pages 刷到磁盘。
  5. 写入 checkpoint record,其中记录 checkpoint 时仍活跃的事务集合。
  6. 允许事务继续执行。

如果某个事务在 checkpoint 之前已经提交或中止,并且它的脏页也已经在 checkpoint 中落盘,那么恢复时不需要再处理它。恢复只需要关注:

  • checkpoint record 中仍活跃的事务。
  • checkpoint 之后才开始的事务。

对于这些事务:

  • 如果日志中没有 COMMIT 或 ABORT,崩溃恢复时需要 UNDO。
  • 如果日志中有 COMMIT 或 ABORT,恢复时需要按日志 REDO,保证历史状态完整。

日志截断边界

如果 checkpoint 记录中有活跃事务集合 L,那么早于 L 中最早事务 BEGIN 的历史日志通常不再参与恢复。系统可以在确认没有备份、复制或审计需求后回收这部分日志空间。

Consistent checkpoint 的问题是停顿时间不可控。只要有长事务未结束,checkpoint 就必须等待;如果系统强行暂停事务,在线请求会被阻塞。

6.3 Fuzzy Checkpoint

Fuzzy checkpoint 的目标是减少 stop-the-world 时间。它允许 checkpoint 进行时事务继续运行,但必须额外记录系统状态:

结构 记录内容 用途
Active Transaction Table (ATT) 活跃事务、状态、最近日志位置 恢复时判断哪些事务需要提交完成或回滚
Dirty Page Table (DPT) 尚未落盘的脏页及其 recLSN 恢复时确定 REDO 的最早起点

典型流程:

  1. 写入 BEGIN_CHECKPOINT。
  2. 快速复制当前 ATT 和 DPT 的快照。
  3. 后台继续刷出 dirty pages;普通事务可以继续产生新日志和新脏页。
  4. 写入 END_CHECKPOINT,其中包含 ATT/DPT 快照。
  5. END_CHECKPOINT 安全落盘后,更新 master record,让它指向这次 checkpoint 的起点。

Fuzzy checkpoint 不保证磁盘上的数据页在 checkpoint 结束时形成一个完全一致的快照,因为 checkpoint 期间事务仍可能修改页面。因此恢复不能简单地从 checkpoint 后开始重放,而要利用 DPT 的 recLSN 找到可能最早未落盘的修改,再从那里开始 REDO。

WAL 约束仍然存在

即使是 fuzzy checkpoint,刷出某个 dirty page 之前,也必须先保证导致该页变脏的相关日志已经落盘。否则崩溃后无法判断该页上的修改应该保留还是撤销。

6.4 Checkpoint 频率

Checkpoint 是运行时开销和恢复时间之间的权衡:

  • 太频繁:后台刷页和日志写入压力变大,正常事务吞吐下降。
  • 太稀疏:WAL 变长,崩溃后需要扫描和重放更多日志。
  • 长事务越多:日志截断越困难,因为必须保留能撤销这些事务的历史。

因此系统通常会结合日志大小、dirty page 数量、目标恢复时间和当前负载来决定 checkpoint 时机,而不是只依赖固定时间间隔。

7 WAL 的额外用途

WAL 不只用于恢复,也常用于 Change Data Capture (CDC) 和复制。系统可以把 WAL 发送到 Kafka、数据仓库或备库,由下游按顺序重放变更。这类机制复用了数据库为恢复已经生成的变更流。

8 小结

WAL + Steal + No-Force 是现代数据库的主流选择:运行时可以灵活回收 buffer pool 页面,提交时只需顺序刷日志;代价是崩溃后必须同时支持 REDO 和 UNDO。Checkpoint 则用于限制恢复要扫描的日志范围。

参考资料