本文首先介绍了 LSM Tree,跳表,然后介绍 memtable
和 LevelDB 的源码实现,memtable
是 LSM Tree 放在内存的数据结构,它保存了最近发生的写入操作,且具有 O(logn) 的查找和插入性能。
Why LSM Tree
The Pathologies of Big Data 提到,硬盘随机读写与顺序读写吞吐率至少相差三个数量级。而且,对硬盘的顺序读写甚至比主存的随机访问还快。
硬盘随机读写与顺序读写吞吐率对比:
如果仅用一个日志顺序记录发生的写操作,那么就可以充分利用硬盘顺序读写吞吐率高的特性。但实际环境中,往往要支持随机读查询和范围查询。
为了支持这些操作,就要引入诸如 B+ 树的数据结构。将数据按 B+ 树的结构组织起来然后持久化,这引入了硬盘的随机读写,失去了硬盘顺序读写的优势。而且,进行数据更新时,要顺着 B+ 树索引到目标数据,这就需要多次 random IO。
另一种方法是使用哈希表对键进行映射,将一个键映射到与其相关的、最近的 Log Entry 在硬盘上的位置。这时查找一个键只需要一次 random IO。但这种方法会有扩展性问题,如果数据流中有许多很小的键,那么可能内存中的哈希表比在硬盘上的所有键值对加起来还大。
由此,我们引出 LSM Tree。LSM Tree 数据写入充分利用了硬盘顺序 IO 速度快,随机 IO 速度慢的特性,并且它也提供了不俗的读取性能。
What is LSM Tree
在 LSM tree 中,一批批写操作被顺序保存到一个个索引文件中,所以每个文件覆盖了一小段时间内发生的修改。每个文件在写到硬盘前,会先对其进行排序,于是,在这些文件中进行查找只需 O(logn)。这些文件是不可变的,它们不会再被更新,新发生的更新会被写到新的文件中。读操作会检查所有的文件,直至找到关于特定键的最新的写操作。写到硬盘的文件会被定期合并,减少空间占用。
当发生一次更新操作,会将这个更新放到内存中的 memtable
中缓存。为了维护有序性,这些更新操作在内存中一般被组织成红黑树或跳表。同时,这些更新操作被顺序写到硬盘上的日志文件中,以供 crash 时恢复之用。当写入的内容达到阈值,这个 memtable
将被刷到硬盘上,并创建新的 memtable
,继续处理新的写操作。
因为对一个键的写操作会被写到多个有序文件中,我们可以对这些写操作进行合并,比如键 x 发生了三个写操作:
- x = 1
- x = 2
- x = 3
只需要 x = 3 这条记录就足以保存键 x。于是,LSM Tree 会定期进行压缩。LSM Tree 会选择多个有序文件,并将它们合并到一起,将一个键的多次更新、删除操作合并。压缩操作有两个用处:一是节约存储空间,二是防止因为文件增多而造成读性能降级。
当发生一个读请求,系统先检查 memtable
,如果没有找到,将会按时间逆序逐个查找硬盘上的文件,直至找到要找的键。
这种方法在文件越来越多时,读操作会越来越慢。有几种方法可以加速读操作。最常用的就是将一个页索引放在内存中,这个索引能够让系统定位到目标键的附近,系统能从索引指示的位置开始扫描。LevelDB 通过在每个文件中保存块缓存,进一步加速了一个文件内的查找。这种方法比二分查找更好,因为一个键值对的大小不一定是固定的,且这种方法更适合压缩后的数据。
Size-tiered compaction
LSM Tree 通用的 Compaction 策略叫做 size-tiered compaction。size-tiered compaction 的思路是:每层允许的 SST 文件最大数量都有个相同的阈值,随着不断将 memtable 写为 SST,当某层的 SST 数达到阈值时,就把该层所有 SST 全部合并成一个大的新 SST,并放到较高一层去。
这种策略下,同一层的各个 SST 有可能包含相同的 key。
在存在大量更新操作的工作流中,size-tiered compaction 会有以下问题:
- 性能不稳定,因为不能保证一个 entry 出现在几个 sstable,这就导致 Read Amplification;
- 空间浪费严重,因为同一层也存在许多重复的键,这个问题在删除操作较多时更为突出;
Leveled compaction
Leveled compaction 的思路是:对于 L1 层及以上的数据,将 size-tiered compaction 中原本的大 SST 拆开,成为多个 key 互不相交的 SST 的序列,这样的序列叫做“run”。L0 层是从 memtable flush 过来的新 SST,该层各个 SST 的 key 是可以相交的,并且其数量阈值单独控制(如4)。从L1层开始,每层都包含恰好一个 run,并且 run 内包含的数据量阈值呈指数增长。Leveled compaction 可以有效地缓解 size-tiered compaction 的 Space Amplification 和 Read Amplification。
Leveled compaction 带来的问题是 Write Amplification。假设 L2 中的一个 sstable 与 L3 中的所有 sstable 都有交集,为了将 L2 中这个 sstable 合并到 L3,必须读取 L3 的所有 sstable。
Hybrid
前 k 层使用 size-tiered compaction,之后的层用 leveled compaction。
LSM Tree in LevelDB
LevelDB 是一个 google 实现的非常高效的 KV 数据库。
LevelDB 采用了 LSM Tree 实现了高效的写操作和不错的读性能。LSM tree 主要由两部分数据结构组成,其一是驻留内存的 memtable
,其二是保存到磁盘上的 sstable
。
- LevelDB 在内存中维护
memtable
记录最近的写操作(kTypeValue
)和删除操作(kTypeDeletion
); - 当内存中的
memtable
达到一定大小后,会将其保存为硬盘上的sstable
,并放到 Level-0.
LevelDB 的 memtable
是通过 Skip List 对键进行组织的。本文分析 LevelDB 的 memtable
实现。
LevelDB 中 L0 的 sstable 之间 key 有重叠,L1-L6 每一层的 sstable 间键都是有序的,通过 compaction 机制来保证的
Skip List
memtable
维护最近发生在 LevelDB 的写操作。其底层采用的是一种具有 O(logn) 查找和插入时间复杂度的数据结构:Skip List,以下简单介绍其概念。
Skip List 有多个层,其最底层是一个排好序的链表。其结构可以由下图表示。每个节点以某一概率 p 向上延伸。
通过级数求和可以得出每个结点平均有 1/(1-p) 层中。
Skip list 的查找过程
- 如上图所示,假设要查找 9,查找时,从顶层开始,找到第一个恰小于 9 的结点,这里为 1;
- 因为结点 1 后没有更多结点,向下走一层,重复上述过程,到达结点 6;
- 继续往下走一层,然后往前找,到达 9,即找到目标结点。
Skip list 的插入
Skip list 的插入过程与查找类似。找到插入位置后,将新节点插入链表中,新节点以某一概率 p 向上延伸。
memtable
实现
以下分析 LevelDB 的 memtable
源码实现。
MemTable
类的 API 总览
MemTable
位于 db/memtable.h,其 API 如下:
// 自增引用计数。
void Ref() { ++refs_; }
// 引用计数减一,如果引用计数为零则将这个 memtable 删除。
void Unref() {
--refs_;
assert(refs_ >= 0);
if (refs_ <= 0) {
delete this;
}
}
// 返回此 memtable 大约占用的内存空间大小。
size_t ApproximateMemoryUsage();
// memtable 的迭代器。
// 调用者需要保证在迭代器存活时 memtable 也存活。
// 此迭代器返回的是编码后的内部键(internal key)。
Iterator* NewIterator();
// 往 memtable 中添加一个键值对,指定序列号 SequenceNumber 和类型。
// 类型为 kTypeValue 表明是插入键值对,类型为 kTypeDeletion 表明是删除键值对。
void Add(SequenceNumber seq, ValueType type, const Slice& key,
const Slice& value);
// 根据键从 memtable 获取值,值将被存到 value 变量中,并返回 true。
// 如果 memtable 包含一个 key 的删除标记,存一个 NotFound() 到变量 s 中并返回 true。
// 其它情况返回 false。
bool Get(const LookupKey& key, std::string* value, Status* s);
MemTable
的成员变量如下:
// 为保存数据的 skip list 取一个别名
typedef SkipList<const char*, KeyComparator> Table;
// 键的比较器
KeyComparator comparator_;
// 引用计数器
int refs_;
// 内存分配器
Arena arena_;
// 底层的 skip list
Table table_;
可以看到,在 memtable.h 中,保存数据的 Table
类型实际上是一个 Skip List。有了 Skip List,memtable
只需处理键值对内存的分配、键值对 entry 的编码、解码等工作。
Add()
Add
将一个entry插入到memtable
中。
一个entry的内存布局如下:
此处 key 类型为 Internal Key。LevelDB 包含三种 key:
- Lookup Key
- Internal Key
- User Key
User Key 是读写数据库直接用到的 key,一般用一个 Slice
表示,如:
db->Put(leveldb::WriteOptions(), "hello", "LevelDB"); // 此处 "hello" 即 User Key.
Internal Key 则是在 User Key 的基础上扩充了两个字段。Lookup Key 又在 Internal Key 的首部添加了一个字段。它们三者的关系如下图所示:
Add
代码实现如下:
void MemTable::Add(SequenceNumber s, ValueType type, const Slice& key,
const Slice& value) {
// 入参 key 是上述 User Key,编码后的 entry 内存布局如下:
// 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()]
// +------------+-------+-------+--------------+---------+
// | key_size | key | tag | value_size | value |
// +------------+-------+-------+--------------+---------+
size_t key_size = key.size();
size_t val_size = value.size();
// Internal Key 占用内存大小为 User Key + 8 bytes,详见上图
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;
// 给 entry 分配空间。
char* buf = arena_.Allocate(encoded_len);
// 将 key_size 写到此 entry 中。
char* p = EncodeVarint32(buf, internal_key_size);
// 将 key 写到此 entry 中。
std::memcpy(p, key.data(), key_size);
p += key_size;
// 将 tag 写到此 entry 中,tag 包含序列号的低 56 位和操作类型(插入/删除,八位)。
EncodeFixed64(p, (s << 8) | type);
p += 8;
// 将 val_size 写到此 entry 中。
p = EncodeVarint32(p, val_size);
// 将 value 写到此 entry 中。
std::memcpy(p, value.data(), val_size);
assert(p + val_size == buf + encoded_len);
// entry 构建完成,将它插入到 skip list 中。
table_.Insert(buf);
}
Add
实际上分配了一块内存,然后通过 EncodeVarint32
和 EncodeFixed64
函数将键大小、键、tag、值大小、值编码到这块内存中,然后调用其 skip list 成员的方法 table_.Insert(buf)
将构造的 entry 插入到 skip list 中。
我们看 EncodeFixed64
是怎么对数据进行编码的:
inline void EncodeFixed64(char* dst, uint64_t value) {
uint8_t* const buffer = reinterpret_cast<uint8_t*>(dst);
// Recent clang and gcc optimize this to a single mov / str instruction.
// 低地址存低位,高地址存高位
buffer[0] = static_cast<uint8_t>(value);
buffer[1] = static_cast<uint8_t>(value >> 8);
buffer[2] = static_cast<uint8_t>(value >> 16);
buffer[3] = static_cast<uint8_t>(value >> 24);
buffer[4] = static_cast<uint8_t>(value >> 32);
buffer[5] = static_cast<uint8_t>(value >> 40);
buffer[6] = static_cast<uint8_t>(value >> 48);
buffer[7] = static_cast<uint8_t>(value >> 56);
}
实际上,就是将 64 位的 unsigned int 的内存放到了 buffer 中。假设有以下 uint64_t:
10000000 11000000 11100000 11110000 11111000 11111100 11111110 11111111
那么编码后的 dst
的内存分布为:
dst = [11111111, 11111110, 11111100, 11111000, 11110000, 11100000, 11000000, 10000000]
所以,LevelDB 中,EncodeFixed64
使用小端存数据。
再看 EncodeVarint32
是怎么编码数据的:
char* EncodeVarint32(char* dst, uint32_t v) {
// 将 dst 解释为 uint8 数组
uint8_t* ptr = reinterpret_cast<uint8_t*>(dst);
// 以
static const int B = 128;
if (v < (1 << 7)) {
*(ptr++) = v;
} else if (v < (1 << 14)) {
*(ptr++) = v | B;
*(ptr++) = v >> 7;
} else if (v < (1 << 21)) {
*(ptr++) = v | B;
*(ptr++) = (v >> 7) | B;
*(ptr++) = v >> 14;
} else if (v < (1 << 28)) {
*(ptr++) = v | B;
*(ptr++) = (v >> 7) | B;
*(ptr++) = (v >> 14) | B;
*(ptr++) = v >> 21;
} else {
*(ptr++) = v | B;
*(ptr++) = (v >> 7) | B;
*(ptr++) = (v >> 14) | B;
*(ptr++) = (v >> 21) | B;
*(ptr++) = v >> 28;
}
return reinterpret_cast<char*>(ptr);
}
EncodeVarint32
与 EncodeFixed64
不同,它使用的是大端存储(高地址存低位,低地址存高位),它会以能够容纳 v
的内存大小编码 v
,这个内存大小可以为 7, 14, 21, 28, 32 位。
Get()
Get
根据入参的 LookupKey
在 memtable
中查找一个 entry。
Get
代码及解析如下:
bool MemTable::Get(const LookupKey& key, std::string* value, Status* s) {
Slice memkey = key.memtable_key();
// 声明了一个 skip list 上的迭代器。
Table::Iterator iter(&table_);
iter.Seek(memkey.data());
if (iter.Valid()) {
// entry format is:
// klength varint32
// userkey char[klength]
// tag uint64
// vlength varint32
// value char[vlength]
// 检查它属于同一个 user key。
// 不检查序列号,因为 Seek() 已经跳过了序列号过大的 entry。
// 迭代下一个键。
const char* entry = iter.key();
// 从 entry 中读出 key_length。
uint32_t key_length;
// GetVarint32Ptr 从 entry 读出 key_length,并返回读取后指针的位置,即 key 的地址。
const char* key_ptr = GetVarint32Ptr(entry, entry + 5, &key_length);
// 对 entry 中的 user key 和 lookup key 中的 user key 进行比较。
if (comparator_.comparator.user_comparator()->Compare(
Slice(key_ptr, key_length - 8), key.user_key()) == 0) {
// 利用比较器比较相等,找到了正确的 user key.
// 取出紧跟 user key 后的 tag,即序列号和值类型。
const uint64_t tag = DecodeFixed64(key_ptr + key_length - 8);
// 取出 tag 低 8 位的值,即 value type.
switch (static_cast<ValueType>(tag & 0xff)) {
case kTypeValue: {
// key_ptr + key_length 即值长度字段的位置,GetLengthPrefixedSlice 从 entry 里面取出了值 v。
Slice v = GetLengthPrefixedSlice(key_ptr + key_length);
// 将值 v 赋给 value 后返回 true.
value->assign(v.data(), v.size());
return true;
}
case kTypeDeletion:
// tag 指示键已经被删除,返回 not found.
*s = Status::NotFound(Slice());
return true;
}
}
}
return false;
}
将 memtable
写到 sstable
的过程
当一个 memtable
超出一定的大小(默认4MB)时,会创建一个新的 memtable
和日志文件,并将以后的更新定向到此新的 memtable
和日志文件。
memtable
大小触及阈值后发生的后台工作
在后台进行以下工作:
- 将先前的
memtable
内容写到sstable
; - 丢弃旧的
memtable
; - 删除旧的日志文件和
memtable
; - 将新的
sstable
添加到 level-0。
同步工作流
首先,用户调用 Put()
,将键值对存入数据库:
// Default implementations of convenience methods that subclasses of DB
// can call if they wish
Status DB::Put(const WriteOptions& opt, const Slice& key, const Slice& value) {
WriteBatch batch;
batch.Put(key, value);
return Write(opt, &batch);
}
上述 WriteBatch
表示一批写操作,LevelDB 支持原子地将一批数据写入。WriteBatch::Put
方法如下:
void WriteBatch::Put(const Slice& key, const Slice& value) {
// 设置这个 batch 的写操作数量
WriteBatchInternal::SetCount(this, WriteBatchInternal::Count(this) + 1);
// 将数据类型、键值对存到 rep_ 中
rep_.push_back(static_cast<char>(kTypeValue));
PutLengthPrefixedSlice(&rep_, key);
PutLengthPrefixedSlice(&rep_, value);
}
// write_batch.h
class LEVELDB_EXPORT WriteBatch {
...
std::string rep_;
}
设置好 WriteBatch
后,DB::Put
调用 DBImpl::Write
。该方法创建了一个 Writer
,将它压入 writers_
队列中,然后通过信号量等待排在前面的写操作完成。
// 1. 通关 MakeRoomForWrite 为写操作让出空间
// 2. 将相应的日志写入并刷盘
// 3. 将新的键值对写入 memtable
Status DBImpl::Write(const WriteOptions& options, WriteBatch* updates) {
Writer w(&mutex_);
w.batch = updates;
w.sync = options.sync;
w.done = false;
MutexLock l(&mutex_);
writers_.push_back(&w);
// 如果前面还有写操作,阻塞等待唤醒
while (!w.done && &w != writers_.front()) {
w.cv.Wait();
}
if (w.done) {
return w.status;
}
// 当前在 writers_ 队头,开始执行写操作
// 为写操作让出空间,这可能触发
Status status = MakeRoomForWrite(updates == nullptr);
uint64_t last_sequence = versions_->LastSequence();
Writer* last_writer = &w;
if (status.ok() && updates != nullptr) { // nullptr batch is for compactions
WriteBatch* write_batch = BuildBatchGroup(&last_writer);
WriteBatchInternal::SetSequence(write_batch, last_sequence + 1);
last_sequence += WriteBatchInternal::Count(write_batch);
// 将键值对添加到日志中,并应用到 memtable 上。
{
mutex_.Unlock();
status = log_->AddRecord(WriteBatchInternal::Contents(write_batch));
bool sync_error = false;
if (status.ok() && options.sync) {
// 将日志刷盘
status = logfile_->Sync();
if (!status.ok()) {
sync_error = true;
}
}
if (status.ok()) {
// 将键值对插入到 memtable 中
status = WriteBatchInternal::InsertInto(write_batch, mem_);
}
mutex_.Lock();
if (sync_error) {
// The state of the log file is indeterminate: the log record we
// just added may or may not show up when the DB is re-opened.
// So we force the DB into a mode where all future writes fail.
RecordBackgroundError(status);
}
}
if (write_batch == tmp_batch_) tmp_batch_->Clear();
versions_->SetLastSequence(last_sequence);
}
// 为什么前面还有不是 &w 的 writer?
while (true) {
Writer* ready = writers_.front();
writers_.pop_front();
if (ready != &w) {
ready->status = status;
ready->done = true;
ready->cv.Signal();
}
if (ready == last_writer) break;
}
// 唤醒下一个阻塞的写
if (!writers_.empty()) {
writers_.front()->cv.Signal();
}
return status;
}
DBImpl::MakeRoomForWrite
意即为写操作腾出空间,它会根据当前 Level-0 的文件数、memtable
的大小、压缩工作的进展等作出相应的反应,其代码如下:
Status DBImpl::MakeRoomForWrite(bool force) {
mutex_.AssertHeld();
assert(!writers_.empty());
bool allow_delay = !force;
Status s;
while (true) {
if (!bg_error_.ok()) {
// Yield previous error
s = bg_error_;
break;
} else if (allow_delay && versions_->NumLevelFiles(0) >=
config::kL0_SlowdownWritesTrigger) {
// 已经接近 L0 文件数的硬上限,为了不让某一个触及上限的写操作产生很长的延迟,让当前线程休眠一会儿。
// 如果当前写线程和压缩线程共用一个 CPU,这个延迟可以将 CPU 让出给压缩线程。
mutex_.Unlock();
env_->SleepForMicroseconds(1000);
allow_delay = false; // Do not delay a single write more than once
mutex_.Lock();
} else if (!force &&
(mem_->ApproximateMemoryUsage() <= options_.write_buffer_size)) {
// memtable 还有空间,跳出循环返回。
break;
} else if (imm_ != nullptr) {
// memtable 满了,但前一个 memtable 还在压缩当中,阻塞等待后台线程完成压缩。
Log(options_.info_log, "Current memtable full; waiting...\n");
background_work_finished_signal_.Wait();
} else if (versions_->NumLevelFiles(0) >= config::kL0_StopWritesTrigger) {
// Level-0 文件数量太多,阻塞等待后台线程完成合并。
Log(options_.info_log, "Too many L0 files; waiting...\n");
background_work_finished_signal_.Wait();
} else {
// 换到一个新的 memtable,并触发对旧 memtable 的压缩。
assert(versions_->PrevLogNumber() == 0);
uint64_t new_log_number = versions_->NewFileNumber();
WritableFile* lfile = nullptr;
s = env_->NewWritableFile(LogFileName(dbname_, new_log_number), &lfile);
if (!s.ok()) {
// Avoid chewing through file number space in a tight loop.
versions_->ReuseFileNumber(new_log_number);
break;
}
// 删除旧日志对象,这里只是删除内存中的日志对象,真正的 WAL 已经被刷盘。
delete log_;
s = logfile_->Close();
if (!s.ok()) {
// We may have lost some data written to the previous log file.
// Switch to the new log file anyway, but record as a background
// error so we do not attempt any more writes.
//
// We could perhaps attempt to save the memtable corresponding
// to log file and suppress the error if that works, but that
// would add more complexity in a critical code path.
RecordBackgroundError(s);
}
delete logfile_;
logfile_ = lfile;
logfile_number_ = new_log_number;
log_ = new log::Writer(lfile);
// 将旧 memtable 赋给 imm_,然后让后台线程将它写到 sstable.
imm_ = mem_;
has_imm_.store(true, std::memory_order_release);
// 创建新的 memtable
mem_ = new MemTable(internal_comparator_);
mem_->Ref();
force = false; // Do not force another compaction if have room
MaybeScheduleCompaction();
}
}
return s;
}
// size_t write_buffer_size = 4 * 1024 * 1024;
由上述函数,当 memtable
的大小达到 write_buffer_size
就触发 Compaction,MakeRoomForWrite
实际只是将旧的 memtable
放到自己的成员 imm_
上,然后就创建新的 memtable
。说明将 memtale
写到 Level-0 并不是由写线程完成的。注意到最后调用了 MaybeScheduleCompaction()
,该方法能够调度后台线程进行 Compaction.
异步工作流
进入了 MaybeScheduleCompaction
之后,将会把 Compaction 工作交给相应的后台线程。下面分析 Write
是怎样将 Compaction 工作交给后台线程的,以及 Compaction 完成后进行了哪些操作。
void DBImpl::MaybeScheduleCompaction() {
mutex_.AssertHeld();
if (background_compaction_scheduled_) {
// Already scheduled
} else if (shutting_down_.load(std::memory_order_acquire)) {
// DB is being deleted; no more background compactions
} else if (!bg_error_.ok()) {
// Already got an error; no more changes
} else if (imm_ == nullptr && manual_compaction_ == nullptr &&
!versions_->NeedsCompaction()) {
// No work to be done
} else {
background_compaction_scheduled_ = true;
env_->Schedule(&DBImpl::BGWork, this); // 调度运行 BGWork
}
}
DBImpl::BGWork
实际调用 DBImpl::BackgroundCall
:
void DBImpl::BGWork(void* db) {
reinterpret_cast<DBImpl*>(db)->BackgroundCall();
}
BackgroundCall()
进一步调用了 BackgroundCompaction()
对数据进行压缩。
void DBImpl::BackgroundCall() {
MutexLock l(&mutex_);
assert(background_compaction_scheduled_);
if (shutting_down_.load(std::memory_order_acquire)) {
// No more background work when shutting down.
} else if (!bg_error_.ok()) {
// No more background work after a background error.
} else {
BackgroundCompaction();
}
background_compaction_scheduled_ = false;
// 前一个 Compaction 可能在下一层产生太多文件,再调度一次 Compaction.
MaybeScheduleCompaction();
// 压缩完成,唤醒所有等待信号的线程。
background_work_finished_signal_.SignalAll();
}
上述调度方法实际上是将工作放到一个队列 background_work_queue_
中,后台线程再从队列中取出工作执行,实际上是一个生产者-消费者模型。
DBImpl::BackgroundCompaction
里面调用了 DBImpl::CompactMemTable
对 memtable
进行压缩:
void DBImpl::BackgroundCompaction() {
mutex_.AssertHeld();
if (imm_ != nullptr) {
CompactMemTable();
return;
}
...
}
DBImpl::CompactMemTable()
实现如下:
void DBImpl::CompactMemTable() {
mutex_.AssertHeld();
assert(imm_ != nullptr);
// 将 memtable 的内容存到一个 sstable 文件
VersionEdit edit;
Version* base = versions_->current();
base->Ref();
Status s = WriteLevel0Table(imm_, &edit, base);
//
base->Unref();
if (s.ok() && shutting_down_.load(std::memory_order_acquire)) {
s = Status::IOError("Deleting DB during memtable compaction");
}
// Replace immutable memtable with the generated Table
if (s.ok()) {
edit.SetPrevLogNumber(0);
edit.SetLogNumber(logfile_number_); // Earlier logs no longer needed
s = versions_->LogAndApply(&edit, &mutex_);
}
if (s.ok()) {
// 已经将 memtable 写入到 sstable,Unref 将导致旧 memtable 的释放。
imm_->Unref();
imm_ = nullptr;
has_imm_.store(false, std::memory_order_release);
// 移除废弃的文件,包括已经写入到 sstable 后不需要的 WAL。
RemoveObsoleteFiles();
} else {
RecordBackgroundError(s);
}
}
它调用了 WriteLevel0Table
,将 memtable
写到 Level-0:
Status DBImpl::WriteLevel0Table(MemTable* mem, VersionEdit* edit,
Version* base) {
mutex_.AssertHeld();
const uint64_t start_micros = env_->NowMicros();
FileMetaData meta;
meta.number = versions_->NewFileNumber();
pending_outputs_.insert(meta.number);
// 拿到 memtable 的一个迭代器
Iterator* iter = mem->NewIterator();
Log(options_.info_log, "Level-0 table #%llu: started",
(unsigned long long)meta.number);
Status s;
{
mutex_.Unlock();
// 用 memtable 的迭代器及一些元数据构造 sstable
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);
...
}
上述代码中,BuildTable
完成了由 memtable
构造 sstable
。
小结
memtable
大小触及阈值后发生的几件事情都分析到了:
- 将旧
memtable
交给后台线程写到 Level-0sstable
,调用最终流转到了WriteLevel0Table
; - 丢弃并创建新的
memtable
,这发生在MakeRoomForWrite
中; - 删除日志文件主要发生在
CompactMemTable
中调用的RemoveObsoleteFiles
,旧memtable
在压缩完成后通过Unref
释放。
参考资料
introduction to LSM
LevelDB Documentation
Skip List Wikipedia