LevelDB 学习02

为此,LevelDB 做了读写分离的设计。它将内存中的数据分为两块,一块叫作 MemTable,它是可读可写的。另一块叫作 Immutable MemTable,它是只读的。这两块数据的数据结构完全一样,都是跳表。那它们是怎么应用的呢?

具体来说就是,当 MemTable 的存储数据达到上限时,我们直接将它切换为只读的 Immutable MemTable,然后重新生成一个新的 MemTable,来支持新数据的写入和查询。这时,将内存索引存储到磁盘的问题,就变成了将 Immutable MemTable 写入磁盘的问题。而且,由于 Immutable MemTable 是只读的,因此,它不需要加锁就可以高效地写入磁盘中

好了,数据的一致性管理问题解决了,我们接着看 C0 树和 C1 树的归并。在原始 LSM 树的设计中,内存索引写入磁盘时是直接和磁盘中的 C1 树进行归并的。但如果工程中也这么实现的话,会有两个很严重的问题:

合并代价很高,因为 C1 树很大,而 C0 树很小,这会导致它们在合并时产生大量的磁盘 IO

合并频率会很频繁,由于 C0 树很小,很容易被写满,因此系统会频繁进行 C0 树和 C1 树的合并,这样频繁合并会带来的大量磁盘 IO,这更是系统无法承受的。

那针对这两个问题,LevelDB 采用了延迟合并的设计来优化。具体来说就是,先将 Immutable MemTable 顺序快速写入磁盘,直接变成一个个 SSTable(Sorted String Table)文件,之后再对这些 SSTable 文件进行合并。这样就避免了 C0 树和 C1 树昂贵的合并代价

SSTable 的分层管理设计我们知道,SSTable 文件是由 Immutable MemTable 将数据顺序导入生成的。尽管 SSTable 中的数据是有序的,但是每个 SSTable 覆盖的数据范围都是没有规律的,所以 SSTable 之间的数据很可能有重叠。

比如说,第一个 SSTable 中的数据从 1 到 1000,第二个 SSTable 中的数据从 500 到 1500。那么当我们要查询 600 这个数据时,我们并不清楚应该在第一个 SSTable 中查找,还是在第二个 SSTable 中查找。最差的情况是,我们需要查询每一个 SSTable,这会带来非常巨大的磁盘访问开销

    因此,对于 SSTable 文件,我们需要将它整理一下,将 SSTable 文件中存的数据进行重新划分,让每个 SSTable 的覆盖范围不重叠。这样我们就能将 SSTable 按照覆盖范围来排序了。并且,由于每个 SSTable 覆盖范围不重叠,当我们需要查找数据的时候,我们只需要通过二分查找的方式,找到对应的一个 SSTable 文件,就可以在这个 SSTable 中完成查询了。

     那为了方便查询,我们要保证每个 SSTable 文件不要太大。因此,LevelDB 还控制了每个 SSTable 文件的容量上限(不超过 2M)。这样一来,两个 SSTable 合并就会生成 1 个到 2 个新的 SSTable

    这时,新的 SSTable 文件之间的覆盖范围就不重合了。当系统再新增一个 SSTable 时,我们还用之前的处理方式,来计算这个新的 SSTable 的覆盖范围,然后和已经排好序的 SSTable 比较,找出覆盖范围有重合的所有 SSTable 进行多路归并。这种多个 SSTable 进行多路归并,生成新的多个 SSTable 的过程,也叫作 Compaction

随着 SSTable 文件的增多,多路归并的对象也会增多。那么,最差的情况会是什么呢?最差的情况是所有的 SSTable 都要进行多路归并。这几乎是一个不可能被接受的时间消耗,系统的读写性能都会受到很严重的影响

    那我们该怎么降低多路归并涉及的 SSTable 个数呢?在第 9 讲中,我们提到过,对于少量索引数据和大规模索引数据的合并,我们可以采用滚动合并法来避免大量数据的无效复制。因此,LevelDB 也采用了这个方法,将 SSTable 进行分层管理,然后逐层滚动合并。这就是 LevelDB 的分层思想,也是 LevelDB 的命名原因。接下来,我们就一起来看看 LevelDB 具体是怎么设计的

    首先,从 Immutable MemTable 转成的 SSTable 会被放在 Level 0 层。Level 0 层最多可以放 4 个 SSTable 文件。当 Level 0 层满了以后,我们就要将它们进行多路归并,生成新的有序的多个 SSTable 文件,这一层有序的 SSTable 文件就是 Level 1 层。

    接下来,如果 Level 0 层又存入了新的 4 个 SSTable 文件,那么就需要和 Level 1 层中相关的 SSTable 进行多路归并了。但前面我们也分析过,如果 Level 1 中的 SSTable 数量很多,那么在大规模的文件合并时,磁盘 IO 代价会非常大。因此,LevelDB 的解决方案就是,给 Level 1 中的 SSTable 文件的总容量设定一个上限(默认设置为 10M),这样多路归并时就有了一个代价上限

    当 Level 1 层的 SSTable 文件总容量达到了上限之后,我们就需要选择一个 SSTable 的文件,将它并入下一层(为保证一层中每个 SSTable 文件都有机会并入下一层,我们选择 SSTable 文件的逻辑是轮流选择。也就是说第一次我们选择了文件 A,下一次就选择文件 A 后的一个文件)。下一层会将容量上限翻 10 倍,这样就能容纳更多的 SSTable 了。依此类推,如果下一层也存满了,我们就在该层中选择一个 SSTable,继续并入下一层。这就是 LevelDB 的分层设计了。

尽管 LevelDB 通过限制每层的文件总容量大小,能保证做多路归并时,会有一个开销上限。但是层数越大,容量上限就越大,那发生在下层的多路归并依然会造成大量的磁盘 IO 开销。这该怎么办呢?

对于这个问题,LevelDB 是通过加入一个限制条件解决的。在多路归并生成第 n 层的 SSTable 文件时,LevelDB 会判断生成的 SSTable 和第 n+1 层的重合覆盖度,如果重合覆盖度超过了 10 个文件,就结束这个 SSTable 的生成,继续生成下一个 SSTable 文件

   通过这个限制,LevelDB 就保证了第 n 层的任何一个 SSTable 要和第 n+1 层做多路归并时,最多不会有超过 10 个 SSTable 参与,从而保证了归并性能。

可以看到,通过这样的一种架构设计,我们就将 SSTable 进行了有序的管理,使得查询操作可以快速被限定在有限的 SSTable 中,从而达到了加速检索的目的

SSTable 文件中的检索加速那在定位到了对应的 SSTable 文件后,接下来我们该怎么查询指定的元素呢?这个时候,前面我们学过的一些检索技术,现在就可以派上用场了。

首先,LevelDB 使用索引与数据分离的设计思想,将 SSTable 分为数据存储区和数据索引区两大部分

在进行精确查找时,我们将数据索引区中的 Index Block 读出,Index Block 中的每条记录都记录了每个 Data Block 的最小分隔 key、起始位置,还有 block 的大小。由于所有的记录都是根据 Key 排好序的,因此,我们可以使用二分查找算法,在 Index Block 中找到我们想查询的 Key

那最后一步,就是将这个 Key 对应的 Data block 从 SSTable 文件中读出来,这样我们就完成了数据的查找和读取

这样一来,我们就可以省去非常耗时的 I/O 操作,从而加速相关的检索操作了

猜你喜欢

转载自blog.csdn.net/kuaipao19950507/article/details/107407234