LSM Tree是一种用于高写入吞吐量的数据库存储引擎,广泛应用于现代分布式数据库系统。其核心思想是将写入操作缓存在内存中,并定期批量写入磁盘,减少磁盘 I/O 操作,提高写入性能。因其高效的写入性能和适应大规模数据的能力,成为现代数据密集型应用的关键技术之一。
LSM-tree主要由三部分组成:Memtable、SSTable和WAL(Write-Ahead Log)。数据首先写入内存中的Memtable,当Memtable满了,就会将其转化为只读的SSTable存储在磁盘上。同时,WAL 记录了每次写操作,以确保数据的可靠性。
WAL是一种非常重要的数据库技术,在很多数据库中我们都可以见到其身影,核心思想是将事务操作先记录到磁盘的日志文件中,然后在批量写入物理存储,我一直觉得,LSM-tree中连续批量写入数据到SSTable的思想,本身就跟WAL的思想有很相似的地方。
"The log-structured merge-tree (LSM-tree)" by Patrick O'Neil是LSM Tree的开创性论文,介绍了LSM Tree的基本结构和设计理念。
论文首先将LSM树和数据库中传统使用的B树索引结构进行的对比,LSM树大大减少了磁盘臂的移动,从而提高了数据写入的成本性能,其最大优势在于写入性能。通过批量写入和合并操作,它能够高效地处理大量写请求,适用于写密集型应用场景。
传统的B树就像是将每本书都按顺序放到书架上,这样查找起来很方便,但每次放书都要找到合适的位置,费时费力。而 LSM树则不同,它更像是先把书随手放到一个临时箱子里(Memtable),等箱子满了再一次性分类整理到书架上(SSTable)。
LSM树的核心组建和运作机制
LSM树由两个或多个类似树形的数据结构组成,其中较小的结构(C0)完全驻留在内存中,而较大的结构(C1及其以上)则存储在磁盘上。
当新的历史记录插入时,首先将其索引信息插入到C0树中,然后通过一个称为“滚动合并”的算法将索引信息迁移到C1树。滚动合并过程中,C0树中的部分索引信息被删除并与C1树中的索引信息合并,最终将合并后的数据写入磁盘。
滚动合并算法的步骤如下:首先读取包含 C1 树叶子节点的多页块,然后读取该块中的磁盘页,将其与 C0 树叶子层中的条目合并,并创建一个新的合并后的 C1 树叶子节点。
每次合并步骤都会产生一个空块(包含尚未合并的条目)和一个满块(包含合并后的条目)。满块会被写入磁盘,而空块则保留在内存中,以备后续合并步骤使用,磁盘上所有块都是满的,这是LSM树和B树非常不同的地方。
C1 树的目录节点也存储在内存中,用于高效地访问叶子节点。目录节点会定期刷新到磁盘,以确保数据持久性。
class LSMTree:
def __init__(self):
self.memtable = Memtable()
self.wal = WAL()
self.sstables = []
def insert(self, key, value):
# 写入WAL以确保数据可靠性
self.wal.append((key, value))
# 写入 Memtable
self.memtable.put(key, value)
# 检查Memtable是否已满
if self.memtable.is_full():
# 将Memtable持久化为SSTable
new_sstable = self.memtable.to_sstable()
self.sstables.append(new_sstable)
self.memtable.clear()
LSM树的更多操作
在进行查找时,系统会首先在C0树中查找,如果找不到,则会在 C1 树中查找。
def get(self, key):
# 首先在Memtable中查找
value = self.memtable.get(key)
if value is not None:
return value
# 如果未找到,再依次在SSTable中查找
for sstable in reversed(self.sstables):
value = sstable.get(key)
if value is not None:
return value
return None
删除操作通过标记删除状态实现,实际删除会在合并过程中进行:
def delete(self, key):
# 在WAL中记录删除操作
self.wal.append((key, None))
# 在Memtable中标记删除
self.memtable.put(key, None)
LSM树的更新操作可以通过先删除再插入来实现。
成本性能和数据模型
文章中提到的 “The Five Minute Rule” 是一种评估内存和磁盘之间数据访问成本效益的规则。它指出,当页面引用频率超过每 60 秒一次时,使用内存缓存页面可以减少磁盘IO,从而降低系统开销。
论文中深入的比较了B树和LSM树的性能差异,本文略过这个部分,更多关注点放在LSM树的工作机制上。
数据温度反映了数据被访问的频率,根据数据温度,可以决定数据最佳的存储方式。将磁盘模型划分为冷数据、温数据和热数据三个区
冷数据是访问频率极低,几年以上才会访问的旧数据,磁盘容量是主要瓶颈,用磁盘存储,可以最大化空间利用率;
温数据的访问频率适中,几天时间访问一次,需要平衡磁盘臂利用率和磁盘空间,用内存缓冲区来缓存频繁使用的数据页,可以减少磁盘IO;
最后是访问频率极高的热数据,每秒几十次访问,内存成本是主要瓶颈,应该放在内存中,最大化访问速度。
我们在日常业务应用开发中,设计缓存和选择数据存储引擎的时候,也应该参考数据温度,针对不同访问频率,决定如何存储和查询特定的数据。
多页块IO
LSM树使用滚动合并的方式将内存中的索引条目迁移到磁盘上的索引中。滚动合并过程中,每次合并多个索引条目到磁盘上的一个页面中,从而实现批量读写。它的磁盘索引结构采用 100% 填充的页面节点,并使用连续的多页块存储,进一步优化磁盘访问效率。
多页块I/O是LSM树的核心优势之一,它通过批量读写、减少磁盘访问成本和提高磁盘访问效率等方式,有效地提升了LSM树的性能。
降低磁盘访问成本的原理,是因为多页块 I/O 可以一次性读写多个页面,从而减少磁盘臂的移动次数,降低寻道时间。一次性读写多个连续页面,减少旋转次数,从而降低旋转延迟。多页块I/O还可以利用磁盘的连续存储空间,减少磁盘碎片
然而,多页块I/O也存在一些局限性,例如内存占用和写放大问题。
多组件LSM树
以上我们谈论的LSM树都是单组件的,当C0组件相对于C1组件变得非常大时,会导致内存成本过高。为了解决这个问题,可以引入多组件LSM树,其中包含C0、C1、C2、…、Ck-1和Ck等多个组件,组件大小依次递增。每个组件都是一个索引树结构,其中C0组件是内存驻留的,而所有其他组件都是磁盘驻留的。
多组件LSM树中,每个组件都有一个大小参数S(Ci);根据一个恒定的插入率R,向C0组件中插入字节;当某个组件的大小超过阈值时,该组件的条目会通过滚动合并迁移到下一个更大的组件,直到所有条目迁移到Ck。
多组件LSM树有几个优势,通过增加组件数量,将C0组件的大小进一步扩大,每个合并步骤中合并到C1组件的条目数量会更多,就提高了批处理效率,减少了磁盘的IO;通过将C0组件的大小最大化,可以减少所需的内存容量,从而降低内存成本;通过合理分配组件大小,确保磁盘IO速率始终保持在较高水平。
▲ LevelDB中LSM-tree的实现
论文中还花很多篇幅,对比了LSM树和B树还有其他存储结构,并且讨论了LSM树的并发恢复机制,本文重点放在工作机制,所以没有展开讨论相关的内容。
总结
LSM-tree在处理大规模、高并发数据写入方面表现优异,为现代数据库管理系统提供了强大的支持,在很多知名的互联网数据库中有着广泛的应用;例如LevelDB、RocksDB、Apache Cassandra。
随着新型硬件的发展,LSM树的研究也在不断推进。例如,结合NVMe SSD等高性能存储介质,可以显著提高 LSM树的读写性能。
深入了解其工作机制,有助于我们更好地理解如何优化存储性能的思想,在业务开发中,也可以运用这些思想,解决存储性能问题。
论文解读:LSM Tree 的魔力,提升写入吞吐量的高效数据存储结构https://mp.weixin.qq.com/s/7iD6JdrmG8jBi1QC4pbUHQ
标签:魔力,写入,self,LSM,Tree,内存,组件,磁盘 From: https://blog.csdn.net/liuwill/article/details/140917859