文章目录
内容简介
主要介绍leveldb从内存到磁盘的基本的数据结构,主要包括memtable,immutable memtable,manifest文件,current 文件,log文件,sstable文件。
memtable
以上Table定义为SkipList<const char*, KeyComparator> Table
从提供的功能来看,主要支持get和add;从结构来看,其包含了skiplist,area,keycompare,即可以理解对skiplist和内存分配和内部编码做了个组合形成了memtable。下面进行分别描述:
skiplist
跳表基础知识可见:https://15721.courses.cs.cmu.edu/spring2018/papers/08-oltpindexes1/pugh-skiplists-cacm1990.pdf
对leveldb中跳表做以下分析:
1.本身的概率均衡和内存使用考虑
根据 RandomHeight 的实现,leveldb 将每个元素高度增长的概率设置为 1/4
template <typename Key, class Comparator>
int SkipList<Key, Comparator>::RandomHeight() {
// Increase height with probability 1 in kBranching
static const unsigned int kBranching = 4;
int height = 1;
while (height < kMaxHeight && rnd_.OneIn(kBranching)) {
height++;
}
assert(height > 0);
assert(height <= kMaxHeight);
return height;
}
2.并发访问考虑
leveldb的skiplist支持一写多读的访问,对于写来说,存在写入队列,保证同时只有一个线程在写,获取到锁,写入队列,然后根据条件变量等待。对于读写并发,基于以下假设:
1.除非跳表被销毁,跳表节点只会增加而不会被删除,因为跳表对外根本不提供删除接口。
2.被插入到跳表中的节点,除了 next 指针其他域都是不可变的,并且只有插入操作会改变跳表。
其内部节点Node利用atomic来保证原子性,还利用了内存屏障,防止指令重排,代码见:
Node* Next(int n) {
assert(n >= 0);
// Use an 'acquire load' so that we observe a fully initialized
// version of the returned Node.
return next_[n].load(std::memory_order_acquire);
}
void SetNext(int n, Node* x) {
assert(n >= 0);
// Use a 'release store' so that anybody who reads through this
// pointer observes a fully initialized version of the inserted node.
next_[n].store(x, std::memory_order_release);
}
// No-barrier variants that can be safely used in a few locations.
Node* NoBarrier_Next(int n) {
assert(n >= 0);
return next_[n].load(std::memory_order_relaxed);
}
void NoBarrier_SetNext(int n, Node* x) {
assert(n >= 0);
next_[n].store(x, std::memory_order_relaxed);
}
arena
arena在leveldb中分析过,是一个内存池,支持普通和对齐方式分配
keycompare
负责从 memkey 中提取出 internalkey,最终排序逻辑是使用 InternalKeyComparator 进行比较,排序规则如下:
1.优先按照 user key 进行排序。
2.User key 相同的按照 seq 降序排序。
3.User key 和 seq 相同的按照 type 降序排序
所以,在一个 MemTable 中,相同的 user key 的多个版本,新的排在前面,旧的排在后面。
下面来看一下各种key
组织形式可以参见(部分):
void MemTable::Add(SequenceNumber s, ValueType type, const Slice& key,const Slice& value) {
// Format of an entry is concatenation of:
// key_size : varint32 of internal_key.size()
// key bytes : char[internal_key.size()]
// tag : uint64((sequence << 8) | type)
// value_size : varint32 of value.size()
// value bytes : char[value.size()]
size_t key_size = key.size();
size_t val_size = value.size();
size_t internal_key_size = key_size + 8;
const size_t encoded_len = VarintLength(internal_key_size) +
internal_key_size + VarintLength(val_size) +
val_size;
char* buf = arena_.Allocate(encoded_len);
char* p = EncodeVarint32(buf, internal_key_size);
std::memcpy(p, key.data(), key_size);
p += key_size;
EncodeFixed64(p, (s << 8) | type);
p += 8;
p = EncodeVarint32(p, val_size);
std::memcpy(p, value.data(), val_size);
assert(p + val_size == buf + encoded_len);
table_.Insert(buf);
}
Immutable memtable
一个只读memtable,数据达到一定数量write_buffer_size,memtable就会转换为immutable memtable,同时新建一个memtable,此时后台线程会将immutable memtable组织为sstable格式写入磁盘。
manifest文件
Manifest 文件存储的信息是leveldb的元数据信息,其中存放着Version信息和versionedit信息,前面存放version信息,后面存放versionedit信息,这样方便恢复,比如已经到了version10,然后修改过程宕机,不能从version1开始恢复,要从version10开始。
versionedit
versionedit记录元数据变更,其内部数据如下:
versionedit就可以看作元数据的变更日志
version
Version 是 数据库某一次应用versionedit后的元数据状态
versionset
VersionSet 是一个 Version 的集合。
leveldb内部数据库状态变化会产生VersionEdit,然后产生新的version,此时,旧version可能仍然在使用,所以会存在多个version,versionset就是一个链表将这些version维护起来,为了避免宕机丢失数据,需要持久化,就是manifest文件。
这样恢复时我们就能根据versionedit来获取下一个版本
current 文件
保存最新的manifest文件名称,从中读取后,读取manifest文件恢复
log文件-WAL
目的:防止突然断电、宕机等各种异常场景导致的数据丢失
基本组成
wal日志按照block为单位进行存储的,每个block大小为32k。而每个block是由一系列record组成的,每个预写日志大小为4M,和memtable默认大小相同,格式如下:
我们可以看到有个recordtype的字段,其值为以下:
enum RecordType {
kZeroType = 0, /* 预留给预分配文件 */
kFullType = 1, /* 当前 Record 包含完整的日志记录 */
kFirstType = 2, /* 当前 Record 包含日志记录的起始部分 */
kMiddleType = 3, /* 当前 Record 包含日志记录的中间部分 */
kLastType = 4 /* 当前 Record 包含日志记录的结束部分 */
};
用来处理超出record大小超出32k场景。这么做一个明显的好处就是,当日志文件发生数据损坏的时候,这种定长块的模式可以很简单地跳过有问题的块,而不会导致局部的错误影响到整个文件。
清理时机
1.opendb时会清理不需要的文件
2.Compaction 后清理不需要的文件
关于存储空间的考虑:InnoDB 存储的 redo log 就是一种典型的 WAL 实现,由于预写日志不可能任其无限增长,所以 InnoDB 使用了“环形数组”的方式进行覆盖写入,单个 redo log 的默认大小为 48MB。leveldb 并没有采用循环写入的方式实现,而是使用创建新的日志文件并删除旧有的日志文件实现。
sstable文件
整体结构:
footer
footer包含magic number,padding(补齐48字节),index offset,index size,metadata offset,metadata size。这样通过footer就能找到其他的元数据块。
block
Block
Index block、meta index block、data block 都是用 BlockBuilder 创建,使用 Block 读取,为了节省空间,LevelDB 在 block 的内部实现了前缀压缩,后续流程里介绍。