当前位置:   article > 正文

技术分享(五)- leveldb源码阅读之概述与LSM-Tree_lsm-tree源码阅读compaction

lsm-tree源码阅读compaction

前言

本文为之前阅读leveldb源码时,结合网上一些博客见解以及自己的理解组合成的一篇文章,本文会从总体以及主要的数据结构去阐述什么是leveldb。文末有部分参考的文章的链接,如不够全面,希望能联系我补全。

概述

  • LevelDB是一个功能上类Redis的key/value存储引擎
  • Redis是一个基于纯内存的存储系统,而LevelDB是基于内存 + SSD的架构,
  • leveldb内存存储最新的修改和热数据(可理解为缓存),SSD作为全量数据的持久化存储,所以LevelDB具备比redis更高的存储量,
  • LevelDB的数据是存储在磁盘上的,采用LSM-Tree的结构实现。LSM-Tree将磁盘的随机写转化为顺序写,从而大大提高了写速度,具备良好的写入性能。磁盘批量的顺序写要远比随机写性能高出很多。
  • 读性能略差,主要原因是由于冷数据需要进行磁盘IO读取

使用场景

写多读少。

谈谈LSM-Tree

LSM-Tree(Log Structured-Merge Tree),简单的说,其牺牲了部分读取的性能,通过批量顺序写来换取了高吞吐的写性能,这种特性在大数据领域得到充分了体现。换句话说,核心思想的核心就是放弃部分读能力,换取写入的最大化能力。除了leveldb,还有其他的NoSQL数据底层数据结构也使用了LSM-Tree,如Hbase,Cassandra,RocksDB(FaceBook基于leveldb的优化版本)。

LSM-Tree里面,核心的数据结构就是SSTable(Sorted String Table),当一个SSTable被打开的时候,index会被加载到内存,然后根据key在内存index里面进行一个二分查找,查到该key对应的磁盘的offset之后,然后去磁盘把响应的块数据读取出来。如果内存足够大的话,可以直接把SSTable直接通过MMap的技术映射到内存中,从而提供更快的查找。

在这里插入图片描述

SSTable分为两部分,一部分在内存中Mentable与Immutable(leveldb使用了跳表来实现),剩下的储存在多级磁盘里面,level0~levelN文件,这也是leveldb名字的由来。具体结构图如下,以一个写的操作为例

在这里插入图片描述

  • 收到写请求,先写进WAL Log(Write-Ahead Logging Log,预写式日志)中,用于故障恢复
  • 接着会写进内存的SSTable中,也就是Memtable,当达到一定的阀值时,就会转化为Immutable Memtable,同时Log文件也转化为fozen log,也就是都转化为不可修改的部分。与此同时,会生成一个新的Memtable
  • 将Immutable Memtable给dump进磁盘中的SSTable文件中,也就是进入了Level 0(可能会有重叠的key),该过程就被称为Minor Compaction。在写入磁盘成功后,fozen log将被删除。
  • 当每层的磁盘上的SSTable的体积超过一定的大小或者个数,也会周期的进行合并。此步骤也称为Major Compaction,这个阶段会真正 的清除掉被标记删除掉的数据以及多版本数据的合并,避免浪费空间,由于SSTable都是有序的,我们可以直接采用merge sort进行高效合并

总的来说,一次写的操作,先后操作为一次顺序写的日志操作,以及一次时间复杂度为O(log n)的内存操作(跳表)。

简单谈一下读的操作

  1. 当收到一个读请求的时候,会直接先在内存里面查询,如果查询到就返回。

  2. 如果没有查询到就会依次下沉,直到把所有的Level层查询一遍得到最终结果。

一些问题点细节

关于WAL Log的故障恢复问题细节

查看源码可发现,在调用open打开一个数据库时,首先回去检查是否有需要恢复的内容,可恢复的,主要为以下四种可能

  1. 写log期间进程异常;

    数据库重启时,发现异常日志数据的话,为了保障一致性,会抛弃该条日志数据,也就是认为用户写入失败

  2. 写log完成,写内存未完成;

  3. write动作完成(即log、内存写入都完成)后,进程异常;

  4. Immutable memtable持久化过程中进程异常;

2~4类情况发生的话,都可以通过日志文件中的记录完成数据库的恢复

注:

事实上,使用WALLog并没有完全消除了写入丢失的问题。写完日志文件之后,操作系统并不是马上将这些数据真正落到磁盘中,而是暂时保存在缓存中,而ubuntu中常用sync来强行将数据落到磁盘中。若用户完成写入操作,操作系统还没有将数据落盘时,系统崩了,就会造成写丢失。如果只是进程异常退出,就不存在这个问题,重启即可以恢复

为什么memtable选用跳表而不是平衡树(如红黑树)?

从查找的资料以及个人的想法来看:

1.跳表其实是利用概率均衡技术,可以实现高效的查询、插入、删除操作,复杂度在大多数情况下为O(logN)。

2.跳表基于链表,在实现上会更简单,简单的结构在后期的维护上更占优势

从以上两点看,作者目的为借助概率平衡来构建一个快速且简单的数据结构来取代平衡树。因为平衡树属于一种非常复杂的数据结构,为了维持树的平衡,获得稳定的查询效率,平衡每次插入都可能涉及到较为复杂的节点旋转等操作。

更直白地说,跳表简单且效率并不比平衡树差很多,取代平衡树是一个极优的选择。

为什么LSM不直接顺序写入磁盘,而是需要在内存中缓冲一下?

主要是提升写的性能,单条写性能肯定没有批量写好,同时对于查找新增的数据可以直接查询,能够避免一定的IO

SSTable分层越多,最坏的情况是将所有分层都扫描一次,leveldb如何去优化这种情况?

  1. 压缩

SSTable 是可以启用压缩功能的,并且这种压缩不是将整个 SSTable 一起压缩,而是根据 locality 将数据分组,每个组分别压缩,这样的好处当读取数据的时候,我们不需要解压缩整个文件而是解压缩部分 Group 就可以读取。

  1. 缓存

因为SSTable在写入磁盘后,除了Compaction之外,是不会变化的,所以我可以将Scan的Block进行缓存,从而提高检索的效率

  1. 索引,Bloom filters

正常情况下,一个读操作是需要读取所有的 SSTable 将结果合并后返回的,但是对于某些 key 而言,有些 SSTable 是根本不包含对应数据的,因此,我们可以对每一个 SSTable 添加 Bloom Filter,因为布隆过滤器在判断一个SSTable不存在某个key的时候,那么就一定不会存在,利用这个特性可以减少不必要的磁盘扫描。

  1. 合并

这个在前面的写入流程中已经介绍过,通过定期合并瘦身, 可以有效的清除无效数据,缩短读取路径,提高磁盘利用空间。但Compaction操作是非常消耗CPU和磁盘IO的,尤其是在业务高峰期,如果发生了Major Compaction,则会降低整个系统的吞吐量,这也是一些NoSQL数据库,比如Hbase里面常常会禁用Major Compaction,并在凌晨业务低峰期进行合并的原因。

B+树 VS LSM-树

从对事务支持的角度来看

B+树会更好一些。

在数据的更新和删除方面,B+Tree可以做到原地更新和删除,这种方式对数据库事务支持更加友好,因为一个key只会出现一个Page页里面,但由于LSM-Tree只能追加写,并且在L0层key的rang会重叠,所以对事务支持较弱,只能在Segment Compaction的时候进行真正地更新和删除。

B+树的特点

优点:主要是支持高效的读(稳定的O(logN))

缺点:在大规模的写请求下(复杂度O(LogN)),效率会变得比较低,因为随着insert的操作,为了维护B+树结构,节点会不断的分裂和合并。操作磁盘的随机读写概率会变大,故导致性能降低。

在这里插入图片描述

简单谈一下B+树的特性

一般关系型数据库的底层数据结构都选用了它,如MySQL。

  1. 节点的子树数和关键字数相同(B 树是关键字数比子树数少一)
  2. 节点的关键字表示的是子树中的最大数,在子树中同样含有这个数据
  3. 叶子节点包含了全部数据,同时符合左小右大的顺序,且相互连接

关于第一点,如关键字2 5 8对应三颗子树。

特点总的来说是,将所有数据都存在叶子节点中,所有非叶子节点均为索引,如果进行IO操作,会比B树的IO操作更少。同时,所有叶子节点都是连起来的,具备了链表的特性,同时使用在范围查询时更加方便。

LSM-树的特点

优点:支持高吞吐的写(可认为O(1)),经过优化后(如索引、缓存等),读的效率大部分情况下能达到O(logN)的复杂度(在读方面B+树更优)

缺点:高吞吐的写会使得系统频繁进行compaction(compare & merge),写入量越大,compaction越频繁,会非常消耗CPU和储存IO,占用大量系统资源,带来整个系统性能的下跌。因此,必要情况下需要禁用自动compaction,可以在低峰期定期触发合并。

一个简单使用例子

leveldb拥有了数据库基本的增删查改操作,以下为一个基本的例子

#include <cassert>
#include <iostream>
#include <string>
#include <leveldb/db.h>

int main()
{
    leveldb::DB* db;
    leveldb::Options options;
    options.create_if_missing = true;
    leveldb::Status status = leveldb::DB::Open(options, "./tmp/testdb", &db);
    assert(status.ok());

    std::string key = "apple";
    std::string value = "B";
    std::string get;
	// 写入
    leveldb::Status s = db->Put(leveldb::WriteOptions(), key, value);
	// 取出
    if (s.ok())
        s = db->Get(leveldb::ReadOptions(), key, &get);
    if (s.ok()) 
        std::cout << "读取到的与(key=" << key << ")对应的(value=" << get << ")" << std::endl;
    else 
        std::cout << "读取失败!" << std::endl;

    delete db;
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

参考链接:

  1. https://blog.csdn.net/u010454030/article/details/90414063
  2. https://leveldb-handbook.readthedocs.io/zh/latest/basic.html
  3. https://zhuanlan.zhihu.com/p/54102723
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/2023面试高手/article/detail/458867
推荐阅读
相关标签
  

闽ICP备14008679号