分析アーキテクチャ - LevelDB

1. LevelDBは何ですか

LevelDB Redisのクラスは、キー/値のストレージエンジンの機能です。Redisの以上のRedisとメモリベースのストレージシステムであって、(キャッシュとして理解される)LevelDBベースメモリ+ SSDアーキテクチャ、最新の変更のメモリストレージおよび熱データ、永続ストレージとしてSSDデータの総量、そうLevelDB高ストレージ容量、および優れた書き込みパフォーマンスと、パフォーマンスを読んだことは主に、わずかに悪化ディスクIOに寒さのデータによるものです。最適化されたRocksDBに基づいてFacebookのlevelDB。

LevelDBは、一般的にマルチクラスタ共通のプロトコル互換性のRedisを構築するための準備、Redisのは、クライアントがアクセス可能なプロキシ+ホストの形をとります。

 

2. LevelDBアーキテクチャ

LevelDBやや類似の構造、基礎及び床は、2つの部分に分かれて、あるディスクメモリ、そして地球の地殻の基礎と構造は、サブレベルの多くのようで、データの異なるレベルがされ、定期的に上から下に移動 -沈降。冷たい基礎となるデータディスクが変更された場合、それはいくつかの時間のためにメモリを再入力し、その後浅いブラシ永続ディスクファイルに返され、その後、ゆっくりと一番下まで移動します。

メモリアーキテクチャ

メモリ内LevelDBは、2つのスキップ・リストを保持し、読み取り専用のrtableは、変更可能wtableです。理解することは簡単で、スキップリストは、レートリストの外観を改善するためのキーを設定し、階層的な方法を介して接続されたキーの順序付きコレクションです。検索と更新時間複雑さのスキップリストは、Log(N)です。

スキップリストは、すべてのキーのリストの一番下を格納し、リストの複数の層で構成され、彼らが注文しています。通常のリストには、高速なバイナリ検索をサポートしていませんが、ジャンプリストの特殊な構造は、特定のノードへのターゲティングバイナリサーチアルゴリズムの効率に近づけるために、リストの一番下を作ることができます。効率的に素早く見つけて注文したアレイのリスト機能を追加したり削除したりする機能とともに、スキップリストを理解するための簡単な。しかし、それは、実装における複雑さのある程度があり、一定の価格を支払うことになります。

データ構造rtable wtable

其中 sequence 为全局自增序列号,LevelDB 遇到一个修改操作,全局序列号自动加一。LevelDB 中的 Key 存储了多个版本的 Value。LevelDB 使用序列号来标记键值对的版本,序列号越大,对应的键值对越新。

type 为数据类型,标记是 Put 还是 Delete 操作,只有两个取值,0 表示 Delete,1 表示 Put。

如果是删除操作,后面的 value_size 字段值 为 0,value 字段值是空的。我们要将 Delete 操作等价看成 Put 操作。同时为了节省存储空间,internal_key_size 和 value_size 都要采用 varint 整数编码。

如果跳跃列表中同一个 key 存在多个修改操作,也就是说有多个「复合 Key」,那么这几个「复合 Key」 肯定会挨在一起按照 sequence 值排序的。当 Get 操作到来时,它会在跳跃列表中定位到 key 所在的位置,选择这几个同样的 key 中 seq 最大的「复合 Key」,提取出其中的 value 值返回。

待 Put 和 Delete 操作日志写到日志文件后,其键值对合并成「复合 Key」插入到 wtable 的指定位置中。

待 wtable 的大小达到一个阈值,LevelDB 将它凝固成只读的 rtable,同时生成一个新的 wtable 继续接受写操作。rtable 将会被异步线程刷到磁盘中。Get 操作会优先查询 wtable,如果找不到就去 rtable 中去找,rtable 如果还找不到,再去磁盘文件里去找。

因为 wtable 要支持多线程读写,所以访问它是需要加锁控制。而 rtable 是只读的,它就不需要,但是它的存在时间很短,rtable 一旦生成,很快就会被异步线程序列化到磁盘上,然后就会被置空。但是异步线程序列化也需要耗费一定的时间,如果 wtable 增长过快,很快就被写满了,这时候 rtable 还没有完成序列化,而wtable 急需变身怎么办?这时写线程就会阻塞等待异步线程序列化完成,这是 LevelDB 的卡顿点之一,也是未来 RocksDB 的优化点。

图中还有个日志文件,记录了近期的写操作日志。如果 LevelDB 遇到突发停机事故,没有持久化的 wtable 和 rtable 数据就会丢失。这时就必须通过重放日志文件中的指令数据来恢复丢失的数据。注意到日志文件也是有两份的,它和内存的跳跃列表正好对应起来。当 wtable 要变身时,日志文件也会跟着变身。待 rtable 落盘成功之后,只读日志文件就可以被删除了。

磁盘结构

LevelDB 在磁盘上存储了很多 sst 文件,sst 表示 Sorted String Table,文件里所有的 Key 都会有序的。每个文件都会对应一个层级,每个层级都会有多个文件。底层的文件内容来源于上一层,最终它们都会来源于 0 层文件,而 0 层的文件又来源于内存里的 rtable 序列化。一个 rtable 会被序列化为一个完整的 0 层文件。这就是我们前面所说的「下沉作用」。

从内存的 rtable 序列化成 0 层 sst 文件称之为「Minor Compaction」,从 n 层 sst 文件下沉到 n+1 层 sst 文件称之为「Major Compaction」。之所以这样区分是因为 Minor 速度很快耗费资源少,将 rtable 完整地序列化为一个 sst 文件就完事了。而 Major 会涉及到多个文件之间的合并操作,耗费资源多,速度慢。层级越深的文件总容量越大,在 LevelDB 源码里有一个层级容量公式,容量和层级呈指数级关系。而通常每个 sst 文件的大小都差不多,区别就成了每一层的文件数量不一样。

capacity=level>0&&10^(level+1)M

每个文件里面的 Key 都是有序的,也就是说它内部的 Key 取值会有一个确定的范围。0 层文件和其它层文件有一个明显的区别那就是其它层内部的文件之间范围不会重叠,它们按照 Key 的顺序严格做了切分。而 0 层文件的内容是直接从内存 dump 下来的,所以 0 层的多个文件的 Key 取值范围会有重叠。

当内存出现读 miss 要去磁盘搜寻时,会首先从 0 层搜寻,如果搜不到再去更深层次搜寻。

如果是其它层级,搜寻速度会很快,因为可以根据 Key 的范围快速确定它可能会位于哪个文件中。但是对于 0 层,因为文件 Key 范围会重叠,所以它可能存在于多个文件中,那就需要对这多个文件进行搜寻。正因如此,LevelDB 限制了 0 层文件的数量,如果数量超出了默认的 4 个,就需要「下沉」到 1 层,这个「下沉」操作就是 Major Compaction。

所有文件的 Key 取值范围、层级和其它元信息会存储在数据库目录里面的 MANIFEST 文件中。数据库打开时,读取一下这个文件就知道了所有文件的层级和 Key 取值范围。

MANIFEST 文件也有版本号,它的版本号体现在文件名上如 MANIFEST-000361。每一次重新打开数据库,都会生成一个新的 MANIFEST 文件,具有不同的版本号,然后还需要将老的 MANIFEST 文件删除。

数据库目录中还有另外一个文件 CURRENT,它里面的内容很简单,就是当前 MANIFEST 的文件名。LevelDB 首先读取 CURRENT 文件才知道哪个 MANIFEST 文件是有效文件。在遇到断电时,会存在一个小概率中间状态,新旧 MANIFEST 文件共存于数据库目录中。

我们知道 LevelDB 的数据库目录不允许多进程同时访问,那它是如何防止其它进程意外对这个目录文件进行读写操作呢?仔细观察数据库目录,你还会发现一个名称为 LOCK 的文件,它就是控制多进程访问数据库的关键。当一个进程打开了数据库时,会在这个文件上加上互斥文件锁,进程结束时,锁就会自动释放。

还有最后一个不那么重要的操作日志文件 LOG,它记录了数据库的一系列关键性操作日志,例如每一次 Minor 和 Major Compaction 的相关信息。

多路归并

Compaction 是比较耗费资源的操作,为了不影响线上的读写操作,LevelDB 将 Compaction 工作交给一个单一的异步线程来完成。如果工作量巨大,这个单一的异步线程也会有点吃不消。当异步线程吃不消的时候,线上内存的读写操作也会收到影响。因为只有 rtable 沉到磁盘里了,wtable 才可以变身。只有 wtable 变身了,才会有新的 wtable 被创建来容纳后续更多的键值对。总之就是一环套一环,环环相扣。

下面我们来研究一下 Compaction 。Minor Compaction 很好理解,就是内容空间有限,所以需要将 rtable 中的数据 dump 到磁盘 0 层文件。那为什么需要从 0 层文件 Compact 下沉到 1 层文件呢?因为 0 层文件如果过多,就会影响查找效率。前面我们提到 0 层文件之间的 Key 范围会有重叠,所以单个 Key 可能存在于多个文件中,IO 读次数将会被文件的数量放大。通过 Major Compaction 可以减少 0 层文件的数量,提升读效率。那是不是只需要下沉到 1 层文件就可以了呢?那 LevelDB 究竟是什么原因需要这么多层级呢?

假设 LevelDB 只有 2 层( 0 层和 1 层),那么时间一长,1 层肯定会累计大量的文件。当 0 层的文件需要下沉时,也就是 Major Compaction 要来了,假设只下沉一个 0 层文件,它不是简简单单地将文件元信息的层数从 0 改成 1 就可以了。它需要继续保持 1 层文件的有序性,每个文件中的 Key 取值范围要保持没有重叠。它不能直接将 0 层文件中的键值对分散插入或者追加到 1 层的所有文件中,因为 sst 文件是紧凑存储的,插入操作肯定涉及到磁盘块的移动。再说还有删除操作,它需要干掉 1 层文件中的某些已删除的键值对,避免它们持续占用空间。

那 LevelDB 究竟是怎么做的呢?它采用多路归并算法,将相关的 0 层文件和 1 层 sst 文件作为输入,进行多路归并,生成多个新的 1 层 sst 文件,再将老的 sst 文件干掉,同时还会生成新的 MANIFEST 文件。对于每个 0 层文件,它会根据 Key 的取值范围搜寻 1 层文件中和它的范围有重叠部分的 sst 文件。如果 1 层文件数量过多,每次多路归并涉及到的文件数量太多,归并算法就会非常耗费资源。所以 LevelDB 同样也需要控制 1 层文件的数量,当 1 层容量满时,就会继续下沉到 2 层、3 层、4 层等。

非 0 层的多路归并资源消耗要少一些,因为单个文件的 Key 取值范围有限,能覆盖到下一层的文件数量有限,参与多路归并的输入文件就少了很多。但是这个逻辑有个漏洞,那就是上下层的文件数量有 10 倍的差距,按照平均范围间隔来算,意味着上层平均一个文件的取值范围会覆盖到下一层的 10 个文件。所以说非 0 层的多路归并资源消耗其实也不低,Major Compaction 就是一个比较消耗资源的操作。

发布了12 篇原创文章 · 获赞 5 · 访问量 515

おすすめ

転載: blog.csdn.net/xk_xx/article/details/104115629