Leveldb通过WriteBatch批量进行多次操作,将多次操作编码后交由WAL进行日志落盘,最后交给Memtable进行批量写入到内存,每次写入之前会调用MakeRoomForWrite来决定是否需要进行 Compation将Memtable变成SStable,那么SStable到底是什么呢? WriteLevel0Table就是其中最为关键的一个函数,通过这个函数将Memtable变成了SStable。 接下来让我们来看下这个 函数到底做了什么?
Status DBImpl::WriteLevel0Table(MemTable* mem, VersionEdit* edit,
Version* base) {
mutex_.AssertHeld();
const uint64_t start_micros = env_->NowMicros();
// 1. 创建FileMetaData,用来描述产生的sstable文件
FileMetaData meta;
// 1.1 生成一个新的FileNumber,sstable文件的名字是有FileNumber组成的
meta.number = versions_->NewFileNumber();
pending_outputs_.insert(meta.number);
// 1.2 创建Memtable迭代器准备写入到sstable文件中
Iterator* iter = mem->NewIterator();
Log(options_.info_log, "Level-0 table #%llu: started",
(unsigned long long)meta.number);
Status s;
{
mutex_.Unlock();
// 2. 构建Table
s = BuildTable(dbname_, env_, options_, table_cache_, iter, &meta);
mutex_.Lock();
}
Log(options_.info_log, "Level-0 table #%llu: %lld bytes %s",
(unsigned long long)meta.number, (unsigned long long)meta.file_size,
s.ToString().c_str());
delete iter;
pending_outputs_.erase(meta.number);
// Note that if file_size is zero, the file has been deleted and
// should not be added to the manifest.
// 3. 将文件添加到VersionEdit中
int level = 0;
if (s.ok() && meta.file_size > 0) {
const Slice min_user_key = meta.smallest.user_key();
const Slice max_user_key = meta.largest.user_key();
if (base != nullptr) {
level = base->PickLevelForMemTableOutput(min_user_key, max_user_key);
}
edit->AddFile(level, meta.number, meta.file_size, meta.smallest,
meta.largest);
}
CompactionStats stats;
stats.micros = env_->NowMicros() - start_micros;
stats.bytes_written = meta.file_size;
stats_[level].Add(stats);
return s;
}
可以看到,构建sstable的核心在于BuildTable,构建好后会产生一个文件,这个文件的元信息则是通过FileMetaData来描述的,最后会将这个文件的元信息存入到VersionEdit中。然后Apply到Version中。关于Version和VersionEdit后面的重点分析,现在我们重点来看下BuildTable的实现。
Status BuildTable(const std::string& dbname, Env* env, const Options& options,
TableCache* table_cache, Iterator* iter, FileMetaData* meta) {
Status s;
meta->file_size = 0;
iter->SeekToFirst();
// 1. 构造Table文件名
std::string fname = TableFileName(dbname, meta->number);
if (iter->Valid()) {
WritableFile* file;
s = env->NewWritableFile(fname, &file);
if (!s.ok()) {
return s;
}
TableBuilder* builder = new TableBuilder(options, file);
meta->smallest.DecodeFrom(iter->key());
Slice key;
// 2. 遍历Memtable,将key/value存入到Table中
// 因为Memtable是有序的,因此第一个key是最小的key,最后的key是最大的key
// 将最小key和最大key这两个元信息存入到FileMeta中
for (; iter->Valid(); iter->Next()) {
key = iter->key();
builder->Add(key, iter->value());
}
if (!key.empty()) {
meta->largest.DecodeFrom(key);
}
// Finish and check for builder errors
s = builder->Finish();
if (s.ok()) {
meta->file_size = builder->FileSize();
assert(meta->file_size > 0);
}
delete builder;
// Finish and check for file errors
// 3. 完成table构建后,开始进行文件sync,确保文件落盘
if (s.ok()) {
s = file->Sync();
}
if (s.ok()) {
s = file->Close();
}
delete file;
file = nullptr;
if (s.ok()) {
// Verify that the table is usable
Iterator* it = table_cache->NewIterator(ReadOptions(), meta->number,
meta->file_size);
s = it->status();
delete it;
}
}
// Check for input iterator errors
if (!iter->status().ok()) {
s = iter->status();
}
if (s.ok() && meta->file_size > 0) {
// Keep it
} else {
env->RemoveFile(fname);
}
return s;
}
通过上面的代码我们可以知道TableBuilder就是用来构建sstable的核心,而sstable本质上就是一个文件,文件内容是一系列排序好的key/value进行编码后的内容,那么它的格式到底是如何的呢?
为了提高整体的读写效率,一个sstable文件按照固定大小进行块划分,默认每个块的大小为4KiB。每个Block中,除了存储数据以外,还会存储两个额外的辅助字段:压缩类型和CRC校验码,上图就是其整体的物理结构。进一步上面的Data部分又被分为了五类:
首先我们来看下data block的数据格式,如下图:
可以看到一个datablock内部包含了多个Entry,每一个Entry就是一个要存取的数据条目,也就是用户存入的key/value,让我们来看下Entry的编码格式,如下图
一个Entry包含了5个部分,他们分别是:
可以看到,一个Entry并非包含了完整的key,而是和前一个key的共享前缀,因此编码和解码的时候都需要保存上一个key的信息,才能对key进行编码或解码。这种编码方式可以有效的避免key重复内容的存储,因为key总是有序的,所以可以最大程度的通过共享前缀的部分来节省key存储的成本,但是带来的问题也是显而易见意见的,如果要从这个文件中检索一个key的话需要完整的将所有的key都解析出来。为此leveldb设计了Restart point,也就是每间隔若干个keyvalue对,将为该条记录重新存储一个完整的key。重复该过程(默认间隔值为16),每个重新存储完整key的点称之为Restart point。有了Restart point就可以通过这些完整的key快速定位目标key所在的区域。然后通过完整的key就可以还原这个区域中所有的key信息了。为了快速找到每一个Restart point,leveldb会在data block的末尾存储一系列的索引信息,和长度,方便我们在加载data block的时候就可以把这些Restart point加载到内存中进行快速查找,接下来我们通过下面的代码再次验证上面描述到的原理。
这个思想和skiplist有点类似,都是按照一定方式从原始数据进行抽样,典型的统计学原理。
void BlockBuilder::Add(const Slice& key, const Slice& value) {
Slice last_key_piece(last_key_);
assert(!finished_);
assert(counter_ <= options_->block_restart_interval);
assert(buffer_.empty() // No values yet?
|| options_->comparator->Compare(key, last_key_piece) > 0);
size_t shared = 0;
// 1. 没有到Restart point的间隔,因此这里需要计算共享前缀
if (counter_ < options_->block_restart_interval) {
// See how much sharing to do with previous string
const size_t min_length = std::min(last_key_piece.size(), key.size());
while ((shared < min_length) && (last_key_piece[shared] == key[shared])) {
shared++;
}
} else {
// 2. 达到了Restart point的间隔了,因此这里需要存储完整的key/value,还需要记录索引信息(也就是restart poont的offset信息)
// Restart compression
restarts_.push_back(buffer_.size());
// 计数器清零
counter_ = 0;
}
const size_t non_shared = key.size() - shared;
// 3. 开始写入共享部分的长度、非共享部分的长度、value的长度
// Add "<shared><non_shared><value_size>" to buffer_
PutVarint32(&buffer_, shared);
PutVarint32(&buffer_, non_shared);
PutVarint32(&buffer_, value.size());
// 4. 写入共享部分的key内容、写入value的内容
// Add string delta to buffer_ followed by value
buffer_.append(key.data() + shared, non_shared);
buffer_.append(value.data(), value.size());
// 5. 更新last key,用于下一个key计算前缀
// Update state
last_key_.resize(shared);
last_key_.append(key.data() + shared, non_shared);
assert(Slice(last_key_) == key);
counter_++;
}
每次添加key时产生的Restart point都存在了restarts_中,这是一个std::vector<uint32_t>类型,存储了每一个Restart point在data block中的offset。在构建完Restart point后会将这些索引信息写入到data block中的尾部。
Slice BlockBuilder::Finish() {
// Append restart array
for (size_t i = 0; i < restarts_.size(); i++) {
PutFixed32(&buffer_, restarts_[i]);
}
PutFixed32(&buffer_, restarts_.size());
finished_ = true;
return Slice(buffer_);
}
到此为止data block的构建就分析完了,通过Restart point可以让我们快速定位要查询的数据在data block中的位置,但是这一切的前提是我们已经知道这个data block中存在我们要查询的数据。 否则我们依然先需要遍历每一个data block。如果我们能高效的先过滤处包含要查询数据的data block那该多棒啊。Leveldb中设计了filter block帮助我们实现这个能力。接下来让我们看看filter block的格式,以及设计filter block是如何帮助我们快速查询的吧。