问题的起点:如何组织磁盘上的数据

存储引擎要解决的核心问题很朴素:数据落在磁盘上,如何组织才能让读写都足够快。围绕这个问题,形成了两条主要技术路线:以 B-Tree 为代表的原地更新结构,与以 LSM-Tree 为代表的追加写结构。它们的差异源于对磁盘特性的不同假设。

B-Tree:原地更新,读友好

B-Tree 及其变体是传统关系型数据库的主力结构。它的特点是:

  • 数据按序组织在页中,通过多层索引逐级定位,查找路径短;
  • 更新采用原地修改,找到目标页后直接改写;
  • 树的高度通常很低,三到四层即可覆盖海量数据,因此点查需要的磁盘访问次数少且稳定。

它的优势在读:无论是点查还是范围扫描,路径清晰、延迟可预测。代价在写:一次更新可能需要读出整个页、修改、再写回,若发生页分裂还要额外的写操作。对于机械盘而言,随机写是性能杀手。

LSM-Tree:追加写,写友好

LSM-Tree 的思路是把随机写转成顺序写:

  1. 写入先追加到内存中的有序结构,并同时记录预写日志用于故障恢复;
  2. 内存结构写满后,整体顺序刷写到磁盘,形成一个不可变的有序文件;
  3. 后台按层级把多个有序文件合并,淘汰被覆盖或删除的旧版本。

由于所有磁盘写入都是顺序追加,写入吞吐显著提高,且顺序写对 SSD 的寿命也更友好。这正是许多分布式 KV 存储选择 LSM-Tree 变体(如 RocksDB)作为底层引擎的原因。

代价一:读放大

一个键的最新版本可能位于内存或某一层文件中,读取时需要从新到旧逐层查找,最坏情况要探查多个文件才能确认键不存在。为了缓解,通常引入布隆过滤器快速判断某个文件是否可能包含目标键,以及利用层级大小比例控制层数。

代价二:写放大

一次逻辑写入会在合并过程中被反复读写多次:数据从上层合并到下层时,会被读出、归并、再写回。层级越多、合并越频繁,实际产生的磁盘写入量相对逻辑写入量的倍数就越高。这个倍数就是写放大,它直接影响磁盘寿命与可用带宽。

两种引擎的对比

  • 写路径:B-Tree 原地改写页,可能触发页分裂;LSM-Tree 顺序追加,后台合并;
  • 读路径:B-Tree 路径确定、层数少;LSM-Tree 可能需要跨多层查找,依赖过滤器优化;
  • 空间回收:B-Tree 删除后空间可直接复用;LSM-Tree 需要等合并完成才真正释放;
  • 空间放大:B-Tree 相对紧凑;LSM-Tree 在合并间隙可能同时存在多个版本,占用更高;
  • 写放大:B-Tree 较低;LSM-Tree 明显较高,是调优的主要目标。

为什么分布式存储偏爱 LSM-Tree

在分布式 KV 存储场景下,LSM-Tree 的优势更容易被放大:

  1. 写入量大:分布式系统的写入通常来自大量并发的随机键,顺序写带来的吞吐优势明显;
  2. 写多读少或读写均衡:许多场景下写入占比高,读放大的影响相对可控;
  3. 批量导入场景:数据迁移和批量写入天然适合顺序追加;
  4. 可调优空间大:层级比例、合并策略、压缩算法都可通过参数调节,便于针对负载特征优化。

实践中的调优方向

  • 控制写放大:调整层级比例与合并触发条件,避免小文件频繁合并;
  • 缓解读放大:启用布隆过滤器,合理设置块大小与缓存;
  • 抑制空间放大:及时触发合并,监控多版本堆积情况;
  • 关注压缩:压缩能显著降低存储与 IO,但会消耗 CPU,需要权衡;
  • 监控实际放大倍数:把逻辑读写量与物理读写量对比,是判断引擎是否健康最直接的指标。

小结

B-Tree 与 LSM-Tree 没有绝对优劣,只有与负载是否匹配。B-Tree 优化读取路径,LSM-Tree 优化写入路径,两者都在读写放大、空间放大之间做取舍。选型时应先明确业务的读写比例、延迟要求与数据规模,再决定哪一类引擎更契合;选定之后,调优的重点也自然落在该类引擎的主要放大项上。

两类引擎对比
更新方式B-Tree 原地修改页;LSM-Tree 追加写入并后台合并
读性能B-Tree 层数少、路径确定;LSM-Tree 需跨层查找,依赖过滤器
写性能B-Tree 随机写代价高;LSM-Tree 顺序写吞吐高
空间回收B-Tree 可即时复用;LSM-Tree 需等合并完成才释放
主要放大项B-Tree 写放大较低;LSM-Tree 写放大较高,是调优核心