Append-only btree 是如何工作的(2010)

Append-only 和 copy-on-write B-tree 设计通过把数据库本身当作日志、从不原地修改页面,来实现简单的崩溃恢复、低成本快照和强读并发。评论者在重写放大、垃圾页堆积以及复杂的压缩或空闲页管理之间权衡这些优点,并比较了传统预写日志、LMDB 的 CoW 实现、ZFS 的 intent log,以及日志结构化或带缓冲的树变体。讨论强调,存储硬件特性(SSD、原始闪存、分区设备)和工作负载模式(读多写多、多线程访问)会强烈影响不可变树结构在真实系统中的可行性。

Append-only 与 mutable/WAL 的性能

  • 几位评论者认为,append-only B-tree 因写放大严重且每次操作都要生成快照,对通用场景来说过于低效。
  • 另一些人反驳说,在顺序写比随机写更便宜的介质上,日志结构或 append-only 布局可能更快,而且在 SSD 上所谓的“原地写入”在很大程度上只是幻觉。
  • 传统数据库通常使用预写日志(WAL)或 undo log,相当于把数据写两遍,但可以通过把日志与数据放在不同硬件上来缓解这一点。
  • LMDB 的基准测试被引用为证据,说明设计良好的 copy-on-write(CoW)B-tree 可以与基于 WAL 的系统相匹配甚至更快,并且存在一个与数据规模相关的临界点。

不可变性、并发与快照

  • 不可变/CoW 树天然支持多个并发读者而无需锁,并且能以较低成本提供快照和克隆。
  • 它们简化了正确性与并发性的推理,但让写入路径更复杂、成本更高。
  • 单写者设计很常见;元页争用是并发写者的瓶颈。

垃圾、压缩与空闲空间管理

  • append-only 和不可变 B+tree 会产生许多过时页面。讨论中的技术包括:
    • 按事务跟踪正在使用的页面,并在没有事务引用它们时回收。
    • 通过追加的“空闲列表”页面维护 free-page list,并更新元页。
    • 使用两个固定位置的元页轮换角色,避免从文件末尾扫描。
  • 纯 append-only 设计在实践中可能最终出现约 99% 的过时页面;LMDB 已转向带页面复用的 CoW,而不是纯 append-only。
  • 有些人认为额外的“垃圾”与闪存磨损均衡相契合;另一些人则强调,尽管采用 append-only 行为,也应尽量减少垃圾生成。

硬件与文件系统背景

  • 原始闪存、zoned NVMe 和 SMR HDD 被强调为实际上强制采用 append-only 或分区写入模式的环境。
  • 嵌入式/原始闪存场景不同于具有不透明 FTL 的企业级 SSD,在后者上,用户层面的调整可能效果有限。

替代树设计与优化

  • 讨论中的想法包括:用 delta records 代替完整节点拷贝(被拒绝,因为这只是用空间换取更多读取/计算)、冷热双树(类似 WAL+btree),以及在内部节点中使用写缓冲(buffer trees、Bε-trees、Fractal trees、Hitchhiker tree、Bw-tree),以在略微降低查询速度的代价下摊销 CoW 写入成本。
  • ZFS 的 intent log 和 CoW 树、CouchDB 的带后台压缩的 append-only B+tree,以及 Datomic 风格的分段树,都被提及为相关架构。

术语与语言层面的类比

  • “Persistent” 用于指代不可变、版本化的数据结构;但它与“persistent”表示“在磁盘上”这一含义之间常有混淆。
  • 在 Scala、Clojure 和 JS 库(例如 transients/Immer)中的经验表明,惯用法和批处理对性能的影响,远大于基本的不可变 vs 可变选择。