参考

https://wiesen.github.io/post/leveldb-introduction/
http://www.benstopford.com/2015/02/14/log-structured-merge-trees/
https://github.com/google/leveldb/blob/master/doc/impl.md
https://www.slidestalk.com/TiDB/PingCAP_Infra_Meetup_93_linjinhe_A_Study_of_LSM_Tree_18518
http://idning.github.io/leveldb-rocksdb-on-large-value.html

https://segmentfault.com/a/1190000015376602
https://v.youku.com/v_show/id_XNDM0NTM0NzIw.html

LSM Tree 和B+Tree的不同之处

B+树通过将写离散化,将数据组织成有序的形式来提高读性能。但是机械硬盘顺序写IO性能好于离散写几个数量级,。所以一个提高写吞吐量的思路就是将离散写改为顺序写,同时尽可能保证读的性能。LSM树就是这么做的。
LSM树写只有顺序写,没有原地更新,即便是同一个数据的更新操作,也是做数据日志的append。这使得LSM树写具有很高的吞吐量。

读方面,B+树因为其数据结构有序, 读可以顺序查找。LSM树因为只有顺序写,没有对数据的in place update,同一条数据可能存在多个,只不过时间不同。这种情况下,原始LSM 树的读只能反向遍历数据的文件,从后往前找到数据。

LSM Tree 基本算法

看上面的参考吧,懒得再写一遍了

合并操作

本质上来说,这种存储结构是延迟了写入刷盘的操作,把所有的随机写入转换为顺序追加操作,同时使用后台任务批量将数据写入磁盘,避免了传统存储引擎由于大量随机写频繁刷脏页带来的写入放大的问题。这种结构对大量写入是非常友好的,使得我们在提升数据库写入吞吐上有机会做大量的优化,比起基于固定大小page的传统存储引擎来说,写入速度有了量级的提升;但是伴随而来的还有必然付出的代价:读的路径变长,以及比单纯刷脏页更重量级的Compaction操作。
诸多开源系统(如LevelDB)将SST文件逻辑上分为多个层级的结构,每一层的SST控制在一定大小,并按层级深度逐步按比例放大,Level0层是MemTable flush到磁盘的数据,范围会有重叠,L1往下的层级中的SST每层都是完全排序的,范围没有重叠,层与层之间范围是重叠的。 多个逻辑层级的设计是基于一个假设:最近写入的数据一般也是读取的热点,而新近写入的数据往往会在逻辑层级中处于比较上层(年轻)的位置,可以采取不同的缓存机制和存储策略;但是太多的层级对读性能会造成影响,在单条查询时,这一影响或许还不明显,如果假定属实,查询会在上层命中快速返回;对于scan来说,就没有这么幸运了,请求的范围往往与每个层级的数据都有交集,因此需要在每个层级进行查找,LevelDB本身作为Key-Value store,scan也许并不是重点考虑的操作;而对于数据库系统来说就很难接受了。另外,多个层级的Compaction操作从总量上来说是做了很多重复操作,因为需要从低层往高层逐级Compaction。

LSM Tree的问题

  1. 读放大:由于LSM Tree compact本身的算法问题,读在最坏情况下,使用base compact算法,要遍历每一个日志文件,在level compact算法下,要遍历level-0的所有文件和其他level的一个文件才能确认。读可能实际发到为多次。
  2. 写放大:涉及到日志文件合并,一次写可能触发多个日志文件的合并操作,引起连锁反应。
  3. 空间放大:如果合并文件算法 时机、合并策略选择的不好,合并的文件太慢可能会比较大进而引起空间放大和读放大,合并的太快则写放大。
  4. 合并文件有性能问题:文件的合并理论上因为有序可以归并,但实际上合并是多个文件合并成一个,性能是有影响的,会直接影响磁盘使用率和CPU使用率
  5. SSD硬盘 对写合并的磁盘损耗:机械硬盘上顺序写相比于随机写有数量级上的优势,SSD硬盘上没有这么夸张,相反的是SSD硬盘写太多有硬件损耗,所以如果合并文件次数太多对SSD反而是一种伤害。

针对上述问题 LSM Tree工程实践上的优化

  1. Level compact本身的读优化:Level Compact保证,Level-1以上等级的文件,每一层内,每个文件的key不重叠,进而, 对于读,最坏情况需要查询Level-0所有文件,和每一个Level的一个文件就够了。不需要遍历每一个文件。一般情况下,利用操作系统页缓存,和日志结构,读都在level-0和level1,能命中页缓存,进而不影响性能
  2. 阿里 tair的一个对合并文件性能问题和写放大的解法:使用FPGA 并行处理合并文件带来的CPU高占用和写性能问题
  3. LevelDB 对读放大的解法:在每个SSTable结尾增加index部分,允许对文件进行更快的查询,提高读性能;其中,还会使用Bloom Filter来计算 数据是否在某个文件内。
  4. RocksDB对读放大的解法:使用Prefix Bloom Filter,解决key的范围查询问题,传统Bloom Filter只支持point query(点查询),不支持rang query(范围查询)。Prefixx Bloom Filter对此作了改进。
  5. 使用SuRF,一种改良版的混合字典树,来过滤掉没有数据的SSTable,提高读性能。SuRF过滤器相比于Bloom Filter,可以做区间查询的过滤,比Bloom Filter更强大。
  6. 针对compact ,file之间使用特殊排序,不产生重叠overlap,让底层level file 直接晋级
  7. 最重要的是尽可能减少file 之间的overlap,进而无论读性能,还是compact都会最优
  8. 主动减少compact次数,减少因为随机写带来的大量compact,待放开后批量compact
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐