写在前面
最近看到一篇面试经验,写到面试官问到LSM,之前对LSM一知半解,今天查了一下相关资料,系统的学习了一下,为了检验学习成果,也为了记录方便温习,在此记录一下。共勉!
在网上看了好几篇文章,还是觉得看图轻松理解数据结构与算法系列(NoSQL存储-LSM树)写的比较好,清晰,易懂。本文主要参考这篇文章。感谢作者。
什么是LSM树
LSM树,即日志结构合并树(Log-Structured Merge-Tree)。其实它并不属于一个具体的数据结构,它更多是一种数据结构的设计思想。
思想
LSM的思想,在于对数据的修改增量保持在内存中,达到指定的限制后将这些修改操作批量写入到磁盘中,相比较于写入操作的高性能,读取需要合并内存中最近修改的操作和磁盘中历史的数据,即需要先看是否在内存中,若没有命中,还要访问磁盘文件。
原理:把一颗大树拆分成N棵小树,数据先写入内存中,随着小树越来越大,内存的小树会flush到磁盘中。磁盘中的树定期做合并操作,合并成一棵大树,以优化读性能。
举个例子
拿update举个例子:
比如有1000万行数据,现在希望update table.a set addr='new addr' where pk = '833',如果使用B-Tree类似的结构操作,就需要:
- 找到该条记录所在的page
- load page到内存(如果恰好该page已经在内存中,则省略该步)
- 如果该page之前被修改过,则先flush page to disk
- 修改数据
上面的动作平均来说有两次disk I/O,如果采用LSM-Tree类似结构,则:
- 将需要修改的数据直接写入内存
可见这里是没有disk I/O的。但是,这样的话读的时候就费劲了,需要merge disk上的数据和memory中的修改数据,这显然降低了读的性能。
确实如此,所以作者其中有个假设,就是写入远大于读取的时候,LSM是个很好的选择。我觉得更准确的描述应该是”优化了写,没有显著降低读“,因为大部分时候我们都是要求读最新的数据,而最新的数据很可能还在内存里面,即使不在内存里面,只要不是那些更新特别频繁的数据,其I/O次数也是有限的。
所以LSM-Tree比较适合的应用场景是:insert数据量大,读数据量和update数据量不高且读一般针对最新数据。
原理
LSM树由两个或以上的存储结构组成,比如在论文中为了方便说明使用了最简单的两个存储结构。一个存储结构常驻内存中,称为C0 tree,具体可以是任何方便健值查找的数据结构,比如红黑树、map之类,甚至可以是跳表。另外一个存储结构常驻在硬盘中,称为C1 tree,具体结构类似B树。C1所有节点都是100%满的,节点的大小为磁盘块大小。
插入步骤
大体思路是:插入一条新纪录时,首先在日志文件中插入操作日志,以便后面恢复使用,日志是以append形式插入,所以速度非常快;将新纪录的索引插入到C0中,这里在内存中完成,不涉及磁盘IO操作;当C0大小达到某一阈值时或者每隔一段时间,将C0中记录滚动合并到磁盘C1中;对于多个存储结构的情况,当C1体量越来越大就向C2合并,以此类推,一直往上合并Ck。
合并步骤
合并过程中会使用两个块:emptying block和filling block。
-
从C1中读取未合并叶子节点,放置内存中的emptying block中。
-
从小到大找C0中的节点,与emptying block进行合并排序,合并结果保存到filling block中,并将C0对应的节点删除。
-
不断执行第2步操作,合并排序结果不断填入filling block中,当其满了则将其追加到磁盘的新位置上,注意是追加而不是改变原来的节点。合并期间如故宫emptying block使用完了则再从C1中读取未合并的叶子节点。
-
C0和C1所有叶子节点都按以上合并完成后即完成一次合并。
插入图解
下面内存数据结构C0 tree用AVL树实现,一步一步图解如何插入数据、如何合并
(1)向LSM树中插入A E L R U,首先会插入到内存中的C0树上,插入“A”,先项磁盘日志文件追加记录,然后再插入C0
(2)插入“E”,"L","R","U",同样先追加日志再写内存
(3)假设此时触发合并,则因为C1还没有树,所以emptying block为空,直接从C0树中依次找最小的节点。filling block长度为4,这里假设磁盘块大小为4。
开始找最小的节点,并放到filling block中
以此类推,填满filling block
开始写入磁盘,C1树
继续插入B F N T
,先分别写日志,然后插入到内存的C0树中
假如此时进行合并,先加载C1的最左边叶子节点到emptying block
接着对C0树的节点和emptying block进行合并排序,首先是“A”进入filling block
合并排序最终结果为
将filling block追加到磁盘的新位置,将原来的节点删除掉
继续合并排序,再次填满filling block
将filling block追加到磁盘的新位置,上一层的节点也要以磁盘块(或多个磁盘块)大小写入,尽量避开随机写。另外由于合并过程可能会导致上层节点的更新,可以暂时保存在内存,后面在适当时机写入
查找操作
查找总体思想是先找内存的C0树,找不到则找磁盘的C1树,然后是C2树,以此类推。
假如要找“B”,先找C0树,没找到
接着找C1树,从根节点开始
找到“B”
删除操作
删除操作为了能快速执行,主要是通过标记来实现,在内存中将要删除的记录标记一下,后面异步执行合并时将相应记录删除。
比如要删除“U”,假设标为#的表示删除,则C0树的“U”节点变为"U(#)"
而如果C0树不存在的记录,则在C0树中生成一个节点,并标为#,查找时就能再内存中得知该记录已被删除,无需去磁盘找了。比如要删除“B”,那么没有必要去磁盘执行删除操作,直接在C0树中插入一个“B”节点,并标为#
总结
-
LSM 是日志和传统的单文件索引(B+ tree,Hash Index)的中立,他提供一个机制来管理更小的独立的索引文件(sstable)。
-
通过管理一组索引文件而不是单一的索引文件,LSM 将B+树等结构昂贵的随机IO变的更快,而代价就是读操作要处理大量的索引文件(sstable)而不是一个,另外还是一些IO被合并操作消耗