官网
github
自己加了 comment 的github分支
本文是根据https://blog.youkuaiyun.com/qq_26499321/article/details/78063856 并加上自己看代码的笔记整理的 。
很多地方有源码信息。可以据此快速定位源码查看。
图尽量符合拿来主义原则
.
概览
LevelDB is a fast key-value storage library written at Google that provides an ordered mapping from string keys to string values.
Features
- Keys and values are arbitrary byte arrays.
- Data is stored sorted by key.
- Callers can provide a custom comparison function to override the sort order.
- The basic operations are Put(key,value), Get(key), Delete(key).
- Multiple changes can be made in one atomic batch.
- Users can create a transient snapshot to get a consistent view of data.
- Forward and backward iteration is supported over the data.
- Data is automatically compressed using the Snappy compression library.
- External activity (file system operations etc.) is relayed through a virtual interface so users can customize the operating system interactions.
LevelDB 是由 Google 开发的 key-value 非关系型数据库存储系统,是基于 LSM(Log-Structured-Merge Tree) 的典型实现,LSM 的原理是:当写数据库时,首先纪录写操作到 log 文件中,然后再操作内存数据库,当达到 checkpoint 时,则写入磁盘,同时删除相应的 log 文件,后续重新生成新的内存数据库和 log 文件。
详见
SSTable and Log Structured Storage
整体结构
如图所示,整个结构包含内存中的 Memtable 和 Imutable(immutable Memtable)。他们用来缓存用户的写入操作。
其它的结构是都存在于硬盘中。
sstable(SST) 是一个个数据表,储存着用户数据,它是不可修改的。sstable 作为落盘的存储结构,每个 sstable 最大 2MB,从宏观来看,它属于分层的结构,即:
- level 0:最多存储 4 个 sstable
- level 1:存储不超过 10MB 大小的 sstable
- level 2:存储不超过 100MB 大小的 sstable
- level 3 及之后:存储大小不超过上一级大小的 10 倍
之所以这样分层,是为了提高查找效率,也是 LevelDB 名称的由来。当每一层超过限制时,会进行 compaction(见compaction章节) 操作,合并到上一层。
Log 文件(又叫WAL:write ahead log)用来记录db 的更新日志,系统恢复的时候可以使用它恢复未持久化到 SST 中的更新。是追加式顺序写入,速度很快。
Manifest 文件记录了所有 SST 的元信息,包含文件名称、level、最大最小 key等。同时Manifest也记录了未处理的 Log 文件号。 启动后,从此获取 log 文件名,实施恢复。
Current 用来记录最新的Manifest文件名称。因为在运行的过程中 SST 信息和 log 文件都在改变。而Manifest文件本身是不可变的,因此会不断生成新的Manifest文件,而Current文件则用来追踪最新的Manifest文件名。
SSTables and Log Structured Merge Trees:
- On-disk SSTable indexes are loaded into memory
- All writes go directly to the MemTable index
- Reads check the MemTable first and then the SSTable indexes
- Periodically, the MemTable is flushed to disk as an SSTable
- Periodically, on-disk SSTables are “collapsed together”
Memtable
Memtable是内存中的一个结构(MemTable::Table)。
底层存储结构是 Skiplist。
SkipList<const char*, KeyComparator> Table
Skiplist 的 key(也是 entry) 为
memtable key = {klength}{userkey}{tag}{vlength}{value}
klength = userkey.size()+sizeof(tag)
tag = (SequenceNumber<< 8)|(type)
sizeof(tag) == 8
因此Skiplist的 key 实际上包含了应用插入的 k 和 v 信息。
但是KeyComparator只比较前三项: {klength}{userkey}{tag}
也就是按 user_key升序 - SequenceNumber(数据版本号)降序 - type降序 排列, 同一个 user_key 不同 SequenceNumber 的 entries 会在一起。在查找的时候需要指定user_key 和 SequenceNum,找到的 key 会是小于等于指定 SequenceNum 的最大 SequenceNum的key。也就是符合指定版本条件的最新的 key。这是实现 snapshot 的基础,见 snapshot 章节。
Immutable Memtable
当Memtable大小达到配置的限制时,它就会变成 Immutable Memtable。这两个实际上是一样的数据结构。只是使用了两个不同的指针。Memtable接受写入。 Immutable Memtable不接受写入,只接受读取,并会被后台线程 dump 到 level0的 SSTable。
SSTable 文件(SST)
SSTable is a simple abstraction to efficiently store large numbers of key-value pairs while optimizing for high throughput, sequential read/write workloads.
SSTable是 leveldb 数据库的核心数据结构。每个SSTable都是一个完整的数据表。SSTable 内部最核心的则是数据、索引和过滤器
SSTable的物理结构
SSTable 由 DataBlock, MetaBlock,MetaIndexBlock,IndexBlock,Footer 这5部分构成。这5部分在逻辑上存储不同的内容,但是物理上存储方式只有3种。其中
- DataBlock/MetaIndexBlock/IndexBlock 拥有相同的物理结构 leveldb::Block,构造方法 leveldb::BlockBuilder
- MetaBlock 目前只有一种,物理结构是 FilterBlock, 构造方法 leveldb::FilterBlockBuilder
- Footer 很简单的一个结构。
它们的逻辑内容分别是
- DataBlock(Block物理结构), 存储用户数据,kv 信息
- IndexBlock(Block物理结构), 存储索引信息
- MetaBlock(FilterBlock物理结构), 存储Filter(布隆过滤器)
- MetaIndexBlock(Block物理结构), 存储MetaBlock的索引(偏移量)
- Footer, SST的根部,从这里才能找到其它 Block
下面详细介绍各种结构
Block 物理结构
它对应的是leveldb::Block。
注意这是物理存储结构,Entry并非一定是用户写入的kv。对于DataBlock来说确实是用户写入的kv,但是对于MetaIndexBlock/IndexBlock来说就不是了。比如对于IndexBlock来说,key存储的是索引key,value存储的是DataBlock的偏移量。
分为3个部分:
- 前面是一个个Entry为KV记录,其顺序是根据Key值由小到大排列的。
- 然后是“重启点”(Restart Point)偏移量列表 int32 Restart[N]。为压缩key信息而设置,见下面。Restart[i]是第 i 个重启点Entry的offset,该 Entry.key.shared_bytes=0。Restart[]的末尾是重启点数量。num_restarts=Restart.size()
- 最后是Tailer,包含一个字节的压缩类型,和4个字节的校验和。当前有不压缩和 Snappy 压缩两种 type。
节省 key 占用空间
由于在 SSTable的所有 DataBlock,以及每个 DataBlock中,kv Entry都是按照 key 有序存储的。因此连续的 Entry 的 key 是很有可能有重合的前缀,如果一个key只记录它与前一个key的“重合前缀长度+不重合后缀内容”,则能节省空间。
Restart[i]记录了第 i 个重启点的offset。为了节省 key 空间,对于offset 范围属于[Restart[i],Restart[i+1])的一批 Entry[], Entry[i+1]只记录了和Entry[i].key 一样的前缀长度 shared_bytes、独有的后缀长度 unshared_bytes,和独有的后缀 key_delta。据此就能构造一个完整的 Entry[i+1].key。
添加 key:
void BlockBuilder::Add(const Slice& key, const Slice& value)
在 block 内查找一个 key: 迭代器(Block::Iter)
Block::Iter 迭代器提供了Next(),Prev(),Seek(key) 这三个主要方法。因此支持对所有的 Entry 进行遍历。以及跳到指定的 key。
- Iter::Next() :迭代的过程中保存当前迭代位置的 key。Next() 就能根据key和key_delta构造新的 key 了。
- Iter::Prev():对于Prev()则会麻烦点,需要先找到前面最靠近的一个重启点,再一个个遍历后面的 key,直