
文章目录
- 1. 引言:
- 2. 精读
- 2.1 性能指标
- 2.2 B-tree
- 2.3 LSM-tree
- 2.4 性能对比
 
- 3. 写在最后
 
1. 引言:
在本期的技术深度解析中,我们将聚焦于数据领域的两个重要成员——B-Tree和LSM-Tree。这两种数据结构在数据管理系统中最为普遍且广泛采用的数据结构。B-Tree以其在磁盘存储系统中的广泛应用而闻名,而LSM-Tree则因其在写入优化方面的表现而受到青睐。随着数据量的不断增长和数据访问模式的多样化,理解这两种数据结构的优势和局限变得尤为重要。本期精读:B-Tree vs LSM-Tree
通过本文,我们将带领读者:
- 探讨B-Tree和LSM-Tree的起源,它们分别是为了解决什么样的问题而被引入的;
- 深入剖析B-Tree和LSM-Tree的数据结构特点,以及它们如何在不同的应用场景中发挥作用;
- 解析在实际应用中,B-Tree和LSM-Tree如何优化存储和检索效率,以及它们在不同数据库系统中的应用案例;
- 展望B-Tree和LSM-Tree在未来的发展,以及它们在大数据时代中的潜在影响。
欢迎在评论区分享您的观点与见解,期待与您交流讨论!
2. 精读
2.1 性能指标
在评估数据结构的性能时,三个关键指标——写放大、读放大和空间放大——为我们提供了衡量效率和优化程度的量化手段。这些指标不仅帮助我们理解数据结构在实际应用中的表现,还能够指导我们对它们进行针对性的优化。以下是对这些指标重要性的详细阐述:
- 写放大(Write Amplification):写放大是指在数据写入过程中,由于数据结构的维护和管理,实际写入存储介质的次数超过了原始数据量的情况。在B树中,每次插入或更新操作可能涉及多次磁盘I/O操作,因为B树需要保持其平衡特性。LSM树虽然通过批量写入减少了磁盘I/O,但在合并过程中也会产生额外的写入。写放大对于理解数据结构的耐久性和性能至关重要,因为它直接关系到存储介质的寿命和系统的能耗。由于闪存的这种工作方式,必须擦除改写的闪存部分比新数据实际需要的大得多。写入放大的主要有以下几个因素:
| 因素 | 描述 | 类型 | 关系* | 
|---|---|---|---|
| 垃圾回收 | 用来挑选用于擦除和重写的最佳块的算法的效率 | 变量 | 反面(小为好) | 
| 预留空间 | 分配到SSD控制器的物理容量百分比 | 变量 | 反面(小为好) | 
| SATA的TRIM命令 或 SCSI中的UNMAP | 这些命令必须由操作系统(OS)发送,可以通知存储设备哪些扇区含有无效数据。可以处理这些命令的SSD能在擦除块时,将包含这些扇区的页作为空闲空间回收,而不是复制无效的数据到干净的页中。 | 开关 | 正向(小为好) | 
| 空闲用户容量 | 没有实际用户数据的用户容量百分比;需要TRIM,否则SSD不会从任何空闲的用户容量中获得好处 | 变量 | 反面(小为好) | 
| 安全擦除 | 擦除所有的用户数据和相关元数据,将SSD的性能重置到最初状态(直至重新开始垃圾回收) | 开关 | 正向(小为好) | 
| 耗损均衡 | 算法的效率,令每块的写入次数与其他的块尽可能相同 | 变量 | 正面(大为坏) | 
| 静动数据分离 | 将数据按修改频率分组 | 开关 | 正向(小为好) | 
| 顺序写入 | 理论上,顺序写入的写入放大为1,但其他因素仍会影响此值 | 开关 | 正向(小为好) | 
| 随机写入 | 写入到非连续的LBA对写入放大的影响最大 | 开关 | 反向(大为坏) | 
| 数据压缩,包括重复数据删除 | 数据压缩和重复数据删除能消除更多的冗余数据,降低写入放大,同时提升SSD速度。 | 变量 | 反面(小为好) | 
| 以SLC模式使用MLC NAND | 以每单元1位,而不是以设计的每单元位数(通常为每单元2位)写入数据,以加快读取和写入操作。如果临近SLC模式下的NAND容量限制,SSD必须把旧有用SLC模式写入的数据改写为MLC或TLC模式,才能让SLC模式的NAND空间释放出来,以便容纳更多的数据。然而,通过让经常更改的页面维持在SLC模式,而不是以MLC或TLC模式修改,这种做法也可以降低磨损,因为比用SLC模式,用MLC或TLC模式写入对闪存的伤害确实更大。因此,这种做法提高了写入放大,但当写入模式设为向常写页面上写入时,却可以减少磨损。然而,顺序和随机写入却会加剧磨损,因为这样就没有或少有频繁写入的页是SLC模式,迫使旧数据不断地从SLC模式改写到MLC或TLC。 | 开关 | 反向(大为坏) | 
-  读放大(Read Amplification):读放大是指为了获取所需数据,数据结构需要读取的数据量超过了原始请求的数据量。在B树中,为了找到一个键,可能需要访问多个节点,尤其是当树的高度增加时。LSM树在读取数据时,可能需要检查多个层级的SSTable,以找到最新的数据版本。读放大影响了数据访问的效率,尤其是在读取密集型应用中,它可能导致性能瓶颈。 
-  空间放大(Space Amplification):空间放大是指数据结构为了维护其性能特性,如B树的平衡性或LSM树的写入优化,而额外占用的存储空间。B树由于其结构特性,通常有较低的空间放大,因为它不需要额外的空间来维护数据的顺序。相反,LSM树为了优化写入性能,可能会保留多个数据版本,从而导致空间放大。空间放大对于存储成本和数据压缩策略有着直接的影响。 
通过引入这些指标,我们能够更全面地评估和比较不同的数据结构,这意味着一种数据结构不太可能在所有三个方面都比另一种好。例如,B树的读放大比LSM树少,而LSM树的写放大比B树少。这些指标不仅帮助我们理解数据结构的工作原理,还能够指导我们如何针对特定的应用场景进行优化,以达到性能与资源利用之间的最佳平衡。从而在实际应用中做出更明智的选择。
2.2 B-tree
B树是一种自平衡的多路搜索树,它通过有序存储数据,确保查找、插入、删除和顺序访问操作的高效性,其时间复杂度为对数级别。B树节点分为内部节点和叶子节点,内部节点存储数据和子节点指针,而叶子节点仅存储数据。与二叉树不同,B树的每个节点可以拥有多个子节点,这使得它在处理大量数据时更为高效,特别是在磁盘存储系统中。AVL树作为自平衡二叉树的先驱,通过保持树的高度平衡,优化了二叉搜索树的性能。B树继承了这些优点,并在数据库和文件系统中得到广泛应用。

一棵m阶的B树是一种自平衡的树形数据结构,具有以下关键特性:
- 节点子节点限制:每个节点最多可以有m个子节点。
- 非叶子节点子节点最小值:除了根节点外,每个非叶子节点至少有m/2个子节点,如果m是偶数,则取m/2的上界,确保树的平衡性。
- 根节点子节点要求:如果根节点不是叶子节点,它至少需要有两个子节点,以维持树的结构。
- 键的有序性:在非叶子节点中,如果有k个子节点,则存在k-1个键,这些键按升序排列,即对于任意的i,都有 k[i] < k[i+1]。
- 键的数量限制:每个节点包含的键的数量最多为2k-1,这是由子节点数量和键的有序性共同决定的。
- 叶子节点的同层性:所有的叶子节点都位于树的同一层,这有助于保持树的平衡和提高访问效率。

如上图展示了树的根节点位于顶部,在本例中恰好包含一个
pivot(20),这表明键k满足k ≤ 20的记录存储在第一个子节点中,而键k满足k > 20的记录存储在第二个子节点中。第一个子节点包含两个pivot键(11和15),这表明键k满足k ≤ 11的记录存储在第一个子节点中,11 < k ≤ 15的记录存储在第二个子节点中,而k > 15的记录存储在第三个子节点中。最左边的叶子节点包含三个值(3, 5, 和 7)。
B+树是B树的增强版本,它在实际应用中,特别是在操作系统的文件索引和数据库索引方面,展现出比B树更高的适用性和效率。在现代关系型数据库中,B+树被广泛采用作为其索引结构的核心,这得益于其优化的数据存储和访问机制,使其成为处理大规模数据检索的首选数据结构。以下是B树和B+树的区别
| 特性 | B树 | B+树 | 
|---|---|---|
| 数据存储位置 | 内部节点和叶子节点都存储数据 | 只有叶子节点存储数据,内部节点仅存储索引键 | 
| 叶子节点链接 | 叶子节点之间没有链接 | 叶子节点之间通过指针相连,形成链表 | 
| 查询效率 | 适合随机访问,但范围查询和顺序访问效率较低 | 适合范围查询和顺序访问,效率较高 | 
| 磁盘I/O操作 | 可能需要多次磁盘I/O操作来访问数据 | 由于叶子节点形成链表,可以减少磁盘I/O操作次数 | 
| 内部节点结构 | 内部节点存储数据和子节点指针 | 内部节点仅存储索引键和子节点指针 | 
| 适用场景 | 适用于需要随机访问和更新的场景 | 适用于数据库和文件系统中的索引结构,特别是范围查询和顺序访问 | 
| 节点分裂和合并 | 当节点达到最大容量时分裂,容量过低时合并 | 同样在节点达到最大容量时分裂,容量过低时合并 | 
| 平衡性保持 | 自平衡,通过旋转和重新分配节点来保持平衡 | 同样自平衡,通过旋转和重新分配节点来保持平衡 | 
2.3 LSM-tree
LSM树(Log-Structured Merge Tree)是一种高效的数据结构,专为优化数据的存储和检索而设计。它通过创新的数据组织方式,实现了高吞吐量的写入操作和高效的读取性能。LSM树的诞生旨在解决传统B树在面对大量写入操作时的性能限制,尤其适用于高写入负载的环境,如分布式数据库系统、日志记录系统和缓存机制等。
 
LSM树的核心理念是将随机写入操作转换为顺序写入,这一策略显著提升了写入效率。在LSM树中,所有新的数据首先被写入内存中的写入缓冲区,随后定期批量写入到一个有序的日志文件中。由于这些写入操作主要在内存中完成,因此大大减少了对磁盘的随机写入需求,从而极大提高了整体写入性能。随着时间的积累,内存中的数据会通过合并过程逐步迁移到磁盘上的多个层级,这样做不仅保持了数据的有序性,也确保了数据的持久化存储。
内存中的数据结构
- Memtable
- 描述:内存中的可变数据结构,存储最新的写入数据。
- 实现:常用跳表、红黑树或AVL树,以支持快速的插入、删除和查找操作。
- Immutable Memtable
- 描述:当Memtable达到设定的大小限制后,转变为不可变状态,等待刷写到磁盘的SSTable中。
- 特点:不再接受新的写入,保持数据的稳定性。
磁盘上的数据结构
- SSTable
- 描述:Sorted String Table,磁盘上的持久化数据结构,存储有序的键值对。
- 特点:一旦写入,SSTable是只读的,按时间顺序组织。
- 布隆过滤器(Bloom Filter)
- 描述:一种概率性数据结构,用于快速判断某个键是否可能存在于SSTable中。
- 特点:高效减少不必要的磁盘I/O操作。
- 写前日志(WAL,Write-Ahead Log)
- 描述:用于故障恢复的日志结构,记录所有写入操作。
- 特点:提供持久性保障,以确保数据在崩溃后可恢复。
LSM-tree 写操作
 
 在写入数据到 LSM 树时,主要流程包括 Memtable、Immutable Memtable、SSTable 的生成以及 Compaction 操作。
Memtable:内存中的可变部分,用于快速写入新的键值对。
Immutable Memtable:当Memtable达到大小限制后,转换为不可变状态,用于后续的持久化操作。
SSTable:磁盘上的持久化文件,存储有序的键值对,通常伴随索引和布隆过滤器等辅助数据结构。
Compaction:定期将多个SSTable合并,以优化存储和查询性能。
-  写入数据到 Memtable- 新的键值对首先写入内存中的可变 Memtable。
- Memtable通常是一个有序的数据结构,例如跳表或红黑树。
 
- 新的键值对首先写入内存中的可变 
Memtable:
key1 -> value1
key2 -> value2
key3 -> value3
-  Memtable达到大小限制并转换为Immutable Memtable- 当 Memtable达到预设的大小限制(如 1MB),会被转换为Immutable Memtable。
- 当前的 Memtable被清空,并创建一个新的可变Memtable,以接收新的写入。
 
- 当 
Memtable 达到大小限制:
- 当前 Memtable 被转换为 Immutable Memtable:
  Immutable Memtable:
  key1 -> value1
  key2 -> value2
  key3 -> value3
- 创建一个新的空的 Memtable:
  Memtable: (空)
-  将 Immutable Memtable数据写入到SSTable- Immutable Memtable的数据会被刷新到磁盘,生成新的- SSTable。
- 数据从内存中按顺序写入 SSTable文件,同时生成元数据(如索引和布隆过滤器)。
 
Immutable Memtable 数据被写入到新的 SSTable:
- 从 Immutable Memtable 读取数据
- 将数据以有序方式写入 SSTable 文件:
  SSTable 0:
  key1 -> value1
  key2 -> value2
  key3 -> value3
-  继续写入新数据到新的 Memtable- 清空后的 Memtable继续接收新的数据写入。
- 上一个 Immutable Memtable已经开始持久化为SSTable。
 
- 清空后的 
写入操作:
应用程序 -> Memtable:(key4, value4)
应用程序 -> Memtable:(key5, value5)
Memtable:
key4 -> value4
key5 -> value5
-  定期执行 Compaction操作- 随着 SSTable文件的增加,系统定期执行Compaction操作,以合并多个SSTable文件。
- Compaction有助于移除重复或过期的数据,提升查询效率。
 
- 随着 
Compaction 之前:
SSTable 0:
key1 -> value1
key2 -> value2
key3 -> value3
SSTable 1:
key4 -> value4
key5 -> value5
Compaction 操作:
1. 选择 SSTables 0 和 1 进行合并。
2. 读取数据进行合并:
   合并后的数据:
   key1 -> value1
   key2 -> value2
   key3 -> value3
   key4 -> value4
   key5 -> value5
3. 写入合并后的数据到新的 SSTable:
   SSTable 2:
   key1 -> value1
   key2 -> value2
   key3 -> value3
   key4 -> value4
   key5 -> value5
4. 删除旧的 SSTables 0 和 1。
LSM-tree 读操作
LSM树的读操作依赖于有序查找和布隆过滤器的结合,逐级递进以提高查找效率并减少I/O操作。在读取某个键对应的值时,LSM树会从最新的数据开始查找,以确保返回最新的值。这个过程通常包括以下几个步骤:
- 检查 Memtable(C0)
首先,查找内存中的 Memtable(C0),因为它包含所有最新的写入。
value = C0.get(key)
if (value exists):
    return value
- 检查 Immutable Memtable
如果在 Memtable 中未找到对应的键,接着检查 Immutable Memtable。该 Memtable 是在最近写操作后达到一定大小而转为不可变状态,等待刷入磁盘。
value = ImmutableMemtable.get(key)
if (value exists):
    return value
- 检查 SSTables
如果上述步骤均未找到所需的值,则依次检查存储在磁盘上的多个层级 SSTables(C1, C2, …)。采用按时间顺序从最早到最近的方式进行搜索,以确保最新的更新覆盖旧值。
- 布隆过滤器(Bloom Filter):在每个 SSTable上使用布隆过滤器,可以快速判断一个值是否不在该文件中,从而减少不必要的磁盘 I/O 操作。
for level in SSTable_levels:
    for sstable in level:
        if sstable.BloomFilter.mightContain(key):
            value = sstable.get(key)
            if (value exists and not tombstoned(value)):
                return value
-  数据合并与删除标记 - 删除标记(Tombstone):如果有删除操作,某些值会带有特殊标记。继续检查以下层级以确保数据删除的幂等性与正确性。
- 合并读取(Merge Read):当查询经过多个 SSTable文件时,需要实时合并以确保返回的值是最新的更新。
 
故障处理与缓存优化
- WAL(Write-Ahead Log):用于故障恢复,保证数据在崩溃后的正确恢复。
- 缓存机制:频繁访问的键会缓存在内存中,提高查询性能。有效的缓存策略有助于减少读取操作的延迟。
LSM-tree 更新和删除操作
LSM树的更新和删除操作实质上是写入操作的特例。在LSM树中,更新一个键值对意味着添加一个新的键值对,而删除则是添加一个带有删除标记的键值对
更新操作
- 写入Memtable:新的键值对首先被写入内存中的Memtable。如果键已经存在,旧的键值对会被新的键值对替换。
C0.put(key, newValue)
- 触发Compaction:当Memtable达到一定大小后,它会被转换为Immutable Memtable,并最终被写入磁盘上的SSTable。Compaction过程会合并包含相同键的SSTable,确保最新的值被保留。
删除操作
- 标记删除:删除操作不是从Memtable或SSTable中物理移除键值对,而是在Memtable中添加一个带有删除标记的键值对。
C0.put(key, tombstone)
- Compaction过程中的处理:在- Compaction过程中,带有删除标记的键值对会被识别并忽略,从而实现数据的逻辑删除。
更新和删除的
Compaction过程
在Compaction过程中,LSM树会合并多个SSTable,包括Memtable和Immutable Memtable。这个过程会处理所有的更新和删除标记:
-  合并数据: Compaction会合并所有包含特定键的键值对,确保最新的更新被保留,删除标记的键值对会被忽略。
-  生成新的 SSTable:合并后的数据会生成新的SSTable,旧的SSTable会被标记为可以被删除。
-  优化存储:通过 Compaction,LSM树可以优化存储空间的使用,移除过时的数据和删除标记,同时保持数据的有序性。
2.4 性能对比
为了解释如何得出B+树和基于级别的LSM树的写放大和读放大的具体计算过程,我们需要考虑它们的数据结构和操作特性。
B+树的读写放大
写放大(Write Amplification):
 B+树的写放大主要来自于节点分裂和合并操作。当一个节点达到其最大容量时,它需要分裂成两个节点,这涉及到写入两个新节点的数据。因此,写放大与节点的大小B有关,即每次写入操作可能涉及到多个块的写入。在最坏的情况下,写放大可以达到Θ(B),即每个写入操作都需要写入整个节点的数据。
读放大(Read Amplification):
 B+树的读放大取决于树的高度。在最坏的情况下,读取一个数据项可能需要从根节点遍历到叶节点,这涉及到树的高度的磁盘I/O操作。如果每个内部节点包含Θ(B)个孩子,那么树的高度是O(log_B N / B),其中N是数据库的大小。因此,读放大是树的高度,即O(log_B N / B)。
LSM树的读写放大
写放大(Write Amplification):
 LSM树的写放大较高,因为数据首先写入内存中的Memtable,然后定期刷新到磁盘上的SSTable。随着时间的推移,SSTable会经历多次合并(Compaction)操作,以优化存储和查询性能。每次合并操作都会涉及到写入多个SSTable的数据。如果每个级别的数据大小是前一个级别的k倍,那么写放大包括将数据从每个级别移动到下一个级别,以及在每个级别中重复合并数据。因此,写放大是Θ(k log_k N / B),其中k是每个级别的增长因子,N是数据库的大小。
读放大(Read Amplification):
 LSM树的读放大涉及到从多个SSTable中读取数据,以确保获取到最新的数据项。在最坏的情况下,可能需要从所有级别中读取数据。对于最高级别,数据大小是O(N),因此需要O(log N / B)次磁盘I/O操作。对于前一个级别,数据大小是O(N/k),因此需要O(log (N/kB))次磁盘I/O操作。以此类推,对于第i个级别,需要O(log (N/k^i B))次磁盘I/O操作。将所有级别的磁盘I/O操作相加,总的读放大是Θ((log^2 N / B) / log_k)。
综上所述可以得到:
| Data Structure | Write Amplification | Read Amplification | 
|---|---|---|
| B+ tree | Θ(B) | O(log_B N / B) | 
| Level-Based LSM-tree | Θ(k log_k N / B) | Θ((log^2 N / B) / log_k) | 
其中,Θ表示大O记号中的紧确界(Theta notation),O表示大O记号,用于描述算法的渐进上界,log_B N表示以B为底N的对数,log_k N表示以k为底N的对数。
3. 写在最后
通过比较B+树和基于级别的LSM树在各种性能方面的表现,我们可以得出结论:基于级别的LSM树在写入性能上优于B+树,而在读取性能上则不如B+树。但是大多数组件选择使用LSM树而不是B树作为其底层存储引擎的主要原因是,利用缓存技术来提升读取性能要比提升写入性能容易得多。
如果你想参与讨论,请 点击这里👉https://github.com/hiszm/BigDataWeekly,每周都有新的主题,周末或周一发布。
大数据精读,探索知识的深度。
关注 大数据精读周刊
版权声明:自由转载-非商用-非衍生-保持署名(创意共享 3.0 许可证)




















