来点八股文(十二) LevelDB&RocksDB

LevelDB

架构

LevelDB的架构是什么?
image.png
LevelDB 将数据分为两大部分,分别存放在内存和文件系统中。主要数据模块包括 WAL log,memtable,immutable memtable,sstable。

LevelDB的写入流程是什么?
按照数据流向依次如下:

  1. 当 LevelDB 收到一个写入请求 put(k, v) ,会首先将其操作日志追加到日志文件(WAL)中,以备节点意外宕机恢复。
  2. 写完 WAL 后,LevelDB 将该条 kv 插入内存中的查找结构:memtable。
  3. 在 memtable 积累到一定程度后,会 rotate 为一个只读 memtable,即 immutable memtable;同时生成一个新的 memtable 以供写入。
  4. 当内存有压力后,会将 immutable memtable 顺序写入文件系统,生成一个 level0 层的 sstable(sorted strings table) 文件。该过程称为一次 minor compaction。
  5. 由于查询操作需要按层次遍历 memtable、immutable 和 sstable。当 sstable 文件生成的越来越多之后,查询性能必然越来越差,因此需要将不同的 sstable 进行归并,称为 major compaction。

memtable

image.png

多线程并发跳表操作

LevelDB读写memtable需要加锁吗?
LevelDB 中对 SkipList 的实现增加了多线程并发访问方面的优化的代码,提供以下保证:
Write:在修改跳表时,需要在用户代码侧加锁。
Read:在访问跳表(查找、遍历)时,只需保证跳表不被其他线程销毁即可,不必额外加锁。
这是因为在实现时,LevelDB 做了以下假设(Invariants):
除非跳表被销毁,跳表节点只会增加而不会被删除,因为跳表对外根本不提供删除接口。
被插入到跳表中的节点,除了 next 指针其他域都是不可变的,并且只有插入操作会改变跳表。
跳表的接口有三个:插入、查询和遍历

查找算法

如果参数 prev 不为空,在查找过程中,记下待查找节点在各层中的前驱节点。显然,如果查找操作,则指定 prev = nullptr 即可;若要插入数据,则需传入一个合适尺寸的 prev 参数。

template <typename Key, class Comparator>
typename SkipList<Key, Comparator>::Node*
SkipList<Key, Comparator>::FindGreaterOrEqual(const Key& key, Node** prev) const {
  Node* x = head_;                // 从头结点开始查找
  int level = GetMaxHeight() - 1; // 从最高层开始查找
  while (true) {
    Node* next = x->Next(level);  // 该层中下一个节点
    if (KeyIsAfterNode(key, next)) {
      x = next;                   // 待查找 key 比 next 大,则在该层继续查找
    } else {
      if (prev != nullptr) prev[level] = x;
      
      if (level == 0) {           // 待查找 key 不大于 next,则到底返回
        return next;
      } else {                    // 待查找 key 不大于 next,且没到底,则往下查找
        level--;
      }
    }
  }
}

sst 和 bloom filter

levelDB是怎么减少读放大/写放大的?
为了减小读放大,LevelDB 采取了几方面措施:

  1. 通过 major compaction 尽量减少 sstable 文件
  2. 使用快速筛选的办法,快速判断 key 是否在某个 sstable 文件中

bloom filter的参数应该如何设计?
bloom filter误判率和以下参数有关:

  1. 哈希函数的个数 k
  2. 底层位数组的长度 m
  3. 数据集大小 n

当 k = ln2 * (m/n) 时,Bloom Filter 获取最优的准确率

LRU缓存

LevelDB是怎么加速sst访问的?
lru主要是为了加速对于sst上block的访问。

entry状态机

image.png

双链表

image.png
image.png
LRU缓存中有几种链表?分别作用是什么?

  1. in-use 链表。所有正在被客户端使用的数据条目(an kv item)都存在该链表中,该链表是无序的,因为在容量不够时,此链表中的条目是一定不能够被驱逐的,因此也并不需要维持一个驱逐顺序。
  2. lru 链表。所有已经不再为客户端使用的条目都放在 lru 链表中,该链表按最近使用时间有序,当容量不够用时,会驱逐此链表中最久没有被使用的条目。

RocksDB

架构

rocksdb的整体架构是什么样的?
image.png

compaction流程和优化

STCS和LCS

**Size-Tiered Compaction Strategy (STCS) **
memtable 逐步刷入到磁盘 sst,刚开始 sst 都是小文件,随着小文件越来越多,当数据量达到一定阈值时,STCS 策略会将这些小文件 compaction 成一个中等大小的新文件。同样的道理,当中等文件数量达到一定阈值,这些文件将被 compaction 成大文件,这种方式不断递归,会持续生成越来越大的文件。总的来说,STCS 就是将 sst 按大小分类,相似大小的 sst 分在同一类,然后将多个同类的 sst 合并到下一个类别。通过这种方式,可以有效减少 sst 的数量。
由于 STCS 策略比较简单,同一份数据在 compaction 期间拷贝的次数相对较少,即写入放大相对小,很多基于 LSM-Tree 的系统将其作为默认的 compaction 策略,如 Lucene、Cassandra、Scylla 等。STCS 逻辑简单、写入放大低,但是它也有很大的缺陷 – 空间放大。其实也存在较大的读放大
为什么会产生空间放大呢?compaction 的过程中,参与 compaction 的 sst 不能立马删除,直到新生成的 sst 写入完毕,这里其实还有一个原因,如果老的 sst 有读操作,由于文件还被引用,也是不能立即删除的。因此,在 compaction 的过程中,磁盘上新老文件共存,产生临时空间放大。即使这种空间放大是临时的,但是对于系统来说,不得不使用比实际数据量更大的磁盘空间,以保证 compaction 正常执行,这产生的代价很昂贵。
这也就是上面一节提到的那个问题。
**Leveled Compaction Strategy (LCS) **

  • sst 的大小可控,默认每个 sst 的大小一致(STCS 在经过多次合并后,层级越深,产生的 sst 文件就越大,最终会形成超大文件)
  • LCS 在合并时,会保证除 Level 0(L0)之外的其他 Level 有序且无覆盖
  • 除 L0 外,每层文件的总大小呈指数增长,假如 L1 最多 10 个,则 L2 为 100 个,L3 为 1000 个…

首先是内存中的 memtable 刷到 L0,当 L0 中的文件数达到一定阈值后,会将 L0 的所有文件及与 L1 有覆盖的文件做合并,然后生成新文件(如果文件大小超过阈值,会切成多个)到 L1,L1 中的文件时全局有序的,不会出现重叠的情况;
当 L1 的文件数量达到阈值时,会选取 L1 中的一个 sst 与 L2 中的多个文件做合并,假设 L1 有 10 个文件,那么一个文件便占 L1 数据量的 1/10,假设每层包含的 key 范围相同,那么 L1 中的一个文件理论上会覆盖 L2 层的 10 个文件,因此会选取 L1 中的一个文件与 L2 中的 10 个文件一起 compaction,将生成的新文件放到 L2;
LCS 不会有超大文件,而且在层与层之间合并时,大体上只选取 11 个 sst 进行 compaction,而这 11 个文件的大小只占整个系统的小部分,因此临时空间放大很小。
LCS 最好的情况是,最后一层数据已经被填满。假设最后一层为 L3,一共有 1000 个 sst。那么 L1 和 L2 一共最多 110 个文件。由于每个 sst 基本大小相同,因此,几乎 90% 的数据在 L3。我们直到每层内的数据不会重复,因此最多,L1 和 L2 的数据包含在 L3 中,重复数据导致的空间放大为 1/0.9=1.11,
相比STCS 8 倍的空间放大好得多。然后,也存在比较差的情况,即当最后一层没有填满时,假设 L3 的文件个数为 100 个,L2 中的文件数也是 100 个,最坏情况下,如果 L2 和 L3 的数据相同,则会产生 2 倍的空间放大。但即使是这样,相对 STCS 还是好得多。
LCS存在写放大问题,写放大指的是IO写带宽的放大
对于 STCS 来说,写入放大和层高强相关,而每层的数据量又是呈指数增长的(即第一层最多 400MB,第二层 1600MB,第三层6400MB,一次类推),很明显层高和数据量的关系为对数关系,而写入放大和层高一致,因此写入放大随数据量增长为 O(logN),N 为数据量大小。
但是对于 LCS 来说,情况会更糟糕,我们来分析下原因:假设需要将 L1 层的数据量 X compaction 到下一层,STCS 的写如放大为 1。而 LCS 需要将这 X 的数据量和 L1 层 10倍 X 数据量,一共 11 * X 一起 compaction,写入放大为 11,是 STCS 的 11 倍。
如果大部分都是写新数据建议stcs 如果更新频繁那么选lcs

rocksdb的LCS(Leveled compaction)

rocksdb的lcs是默认的compaction策略,也可以说它综合了STCS(L0)和LCS(其它层)的策略。
levelDB/rocksDB的compaction流程是怎样的?怎么进行优化?
compaction分为minor和major compaction。
minor compaction / flush的基本流程如下:
1.遍历immutable-list,如果没有其它线程flush,则加入队列
2.通过迭代器逐一扫描key-value,将key-value写入到data-block
3.如果data block大小已经超过block_size(比如16k),或者已经key-value对是最后的一对,则触发一次block-flush
4.根据压缩算法对block进行压缩,并生成对应的index block记录(begin_key, last_key, offset)
5.至此若干个block已经写入文件,并为每个block生成了indexblock记录
6.写入index block,meta block,metaindex block以及footer信息到文件尾
7.将变化sst文件的元信息写入manifest文件
major compaction的主要流程如下:
1.首先找score最高的level,如果level的score>1,则选择从这个level进行compaction
2.根据一定的策略,从level中选择一个sst文件进行compact,对于level0,由于sst文件之间(minkey,maxkey)有重叠,所以可能有多个。
3.从level中选出的文件,我们能计算出(minkey,maxkey)
4.从level+1中选出与(minkey,maxkey)有重叠的sst文件
5.多个sst文件进行归并排序,合并写出到sst文件
6.根据压缩策略,对写出的sst文件进行压缩
7.合并结束后,利用VersionEdit更新VersionSet,更新统计信息

rocksdb的STCS

RocksDB中提供的stcs策略叫做universal compaction。
1.如果空间放大超过一定的比例,则所有sst进行一次compaction,所谓的full compaction,通过参数max_size_amplification_percent控制。
2.如果前size(R1)小于size(R2)在一定比例,默认1%,则与R1与R2一起进行compaction,如果(R1+R2)*(100+ratio)%100<R3,则将R3也加入到compaction任务中,依次顺序加入sst文件
3.如果第1和第2种情况都没有compaction,则强制选择前N个文件进行合并。

选择参与compaction的文件

如何选择参与compaction的文件?

  1. 根据每一层的分数选择要compaction的层{Li}以及输出的层{Lo}.打分的默认策略是选择文件size较大,包含delete记录较多的sst文件,这种文件尽快合并有利于缩小空间。
  2. 根据compaction优先级的设置选择优先级最高的文件。如果这个文件或者和他key范围重叠的下一层文件正在参与另一个compaction,那么就选择优先级第二高的文件,以此类推。将选中的文件加入compaction的输入文件集合(inputs set)。
  3. 重复第一步,直到选中文件的key范围与其周围sst文件的key范围严格不重叠。(这一步是对于minor来说的)
  4. 将input set的key范围与当前正在被compact的文件进行检查确保他们不重叠。如果重叠就终止这次compaction。
  5. 在{Lo}中找到与input set的key范围重叠的所有文件,并将其放入输出文件集合中(output level inputs set)
  6. 将inputs set与output level ivnputs set进行compaction。

优化compaction

  1. 优化长时间没有更新的值

如果一个文件长时间没有被更新,那么它就会一直常驻在系统中。一个常见的例子是一个key被设为空值而并不是真正的删除,随后也不会再有针对这个key的更新,那么这个key就会永久驻留,造成空间浪费。RocksDB中可以针对一个CF设置ttl时间,对所有旧于这个时间的数据调度一个compaction。

  1. 优化L0堆积问题

RocksDB引入了intra-L0 compaction,即在发生上述情况时在L0层内部进行compact,将多个sst file合并为1个大的sst file,以此减少L0层文件数量,在一定程度上能够提升L0层的读性能和减少write stall的发生。

  1. 并行compaction

image.png
Sub-Compaction的思想其实很简单,就是将一个compaction的任务划分成多个子任务,通过并行执行的方式提高compaction的速度。
Sub-Compaction触发的时机有如下几个:

  1. 对于Leveled-Compaction,如果触发compaction的是L0或者手动触发compaction,并且output level大于第0层,则构建sub-compaction。

  2. 对于Universal-Compaction,如果总共的层数大于1,且output level大于0,则构建sub-compaction。

  3. 用户手动compaction

通过begin和end两个参数来确定想要compact的key范围。在不同的compaction类型下这两个参数也有不同的效果。对于universal和fifo类型的compaction,不管begin和end是什么数值,都会进行全量的compaction,并把output的文件放到同一层。对于leveled compaction,会将所有包含符合条件的key(begin < key < end)的文件compact到非空的最后一层。

  1. 使用移动compaction

image.png
首先,这会造成很多没有被compact过的小文件(因为直接被移动了);其次因为同样的原因,compaction filter没办法作用在这些文件上。可以通过结合manual compaction的方式来手动compact解决问题。

  1. 使用远端compaction

Remote compaction分为下面几步:
Schedule:rocksdb会将通过回调的方式将compaction的信息(哪些文件参与,哪一层,什么key range之类的)发给远端compaction worker。
Compact:worker会以只读的方式拿到数据,并且将output文件放到临时目录里。
Result send:类似于前面的schedule,worker会将compact之后的元数据信息发回给rocksdb。
Install & Purge:rocksdb这个时候会将临时目录里的文件进行重命名,然后再进行purge,整个流程就结束了。

memtable

rocksdb的memtable结构有几种?
image.png
image.png

wal

rocksDB的wal文件格式是什么样的?
image.png

sst

rocksdb有哪些SST结构?对于leveldb的sst结构有什么改进?
image.png
第一种结构:每块的K-V都是有序的,而多块也是有序的。文件中包含元数据相关的信息,包括数据压缩字典、过滤器等。会按照数据块所属的K-V范围来创建索引,为提升查询性能会给索引分片。

image.png
另外一种结构是每个K-V来存储。它的索引比较特殊,由hash结构和二进制查找缓存两部分组成。依然按照key的前缀做hash,如果桶对应的K-V记录很少,则直接指向第一个key(有多个key属于该桶)的记录位置。如果属于桶的K-V记录多于16条,或者包含多于一个前缀的记录,则先指向二进制查找缓存(先二分查找),而后指向第一个key的记录位置。
改进:
Indexing SST 引入了四个下标指针,分别是下层比smallest大/小 和 largest大/小的第一个文件下标

优化方法汇总

  1. 使用LRU加速SST访问
  2. memtable – 哈希的memtable 列式SST(游程编码 字典压缩 位图编码 增量编码 ) – 哈希的列式SST
  3. bloom filter
  4. 优化compact(优化长时间未更新的值 L0内部compact 并行compact 用户手动compact 移动compact)
  5. 使用多个memtable而不是一个

其它问题

对比 b+ 树,lsm树

  1. 结构和特点:
    • B+树是一种平衡树结构,用于实现有序的数据存储和检索。它具有良好的顺序访问性能和范围查询性能,适用于随机读写和范围查询的场景。
    • LSM树(Log-Structured Merge-Tree)是一种基于日志结构的树状数据结构,将写操作追加到日志中,然后周期性地将日志合并成更大的文件,以优化写入性能。LSM树具有高写入性能和压缩优化,适用于写入密集型工作负载。
  2. 写入性能:
    • B+树在每次写入时需要更新内部节点和叶子节点,因此写入操作的开销相对较高,特别是在频繁写入的场景下。
  3. 读取性能:
    • B+树由于具有良好的有序性,对于范围查询和顺序访问具有较好的性能。在频繁的范围查询和顺序扫描场景下,B+树的读取性能较好。
    • LSM树的读取性能相对较差,特别是在数据量较大时。由于数据分散在多个文件中,需要进行多次查找和合并操作,导致读取延迟较高。
  4. 空间利用和存储效率:
    • B+树存储的数据较为紧凑,对于索引和元数据的存储效率较高。
    • LSM树由于数据存储在多个文件中,可能存在较大的空间浪费和碎片化,但通过合并操作可以实现数据的压缩和优化。

如何减少写放大?

  1. kv分离
  2. 移动compaction/手动compaction

kv分离时value的存储方案?

  1. WiscKey:Value和WAL合并,叫做vLog形成环,GC时从后往前读
  2. HashKV:改进WiscKey,Value和WAL合并,通过一个固定的 hash function 分配到多个 segment 里面去,冷热分离,如果上次 GC 之后有两次 update 就算是热 key,冷数据单独写vLog
  3. Titan:顺序存放Value文件,提高范围读性能

image.png

如何优化cache?

  1. 降低元素移动频率
  2. threadlocal

如何优化delete?

  1. 提前(不必等到最后一层)清理 delete tombstone,如果确认更高的层没有对应的 KV 的话
  2. 优先 compaction 「含 tombstone 率」高的文件
  3. 直接清理 delete tomstone 只要它遇上「一」个 KV(不必等到最后一层),前提是用户能保证 KV 插入后不会更新
  4. range delete 会把 range tombstone 写到 SST 文件中一个专门的区域。读请求在读的时候都要过一遍 range tombstone,以过滤掉删除的数据。RocksDB 的分段 + 排序是在 open SST 文件的时候实时做的,做完之后会把结果缓存下来供后续使用。
  5. 优化 time-bounded delete SST 文件过期时,compact 到下一层。在所有层中的总时间和为 time-bounded delete时间
  6. 优化range delete 按delete key排序,感觉不太实用
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值