leveldb 中的 format
1 概述
本文讲述 leveldb 中文件层次(多级 sst + memtalbe),sst 文件格式,log 文件格式,Block 格式,比较器 comparetor。
2 leveldb Block 格式
leveldb 中最细粒度的是 Key,包括 userkey,InternalKey,ParsedInternalKey,详见[“leveldb精读系列-key”][1],此文介绍了它们各自的定义,以及它们存在的意义。最终存储的形式就是 Key-Value 的形式。为了减少存储空间,使用了 Block:每个 Block 有多组 Key-Value。
为了提高查询效率和减少存储空间,Block 有如下设计:
- 1.每个 Block 中存储着它里面所有 Key 的范围以便于快速决定一个键是否在它里面
- 2.每个 Block 中的键值对是有序排列的,于是可以利用这点,将相邻的 Key 共用的前缀串压缩存储。具体的算法是:每 block_restart_interval 个键值对作为一个组,此组中每一个key在存储时,只存储它与前一个 Key 相同前缀的长度和差异部分。采用分组的方式是为了提高随机解析的效率:解析第 n 个时只需要从它所在的组第一个开始解析,而不是从整个Block 第一个开始。
下图展示的是 block 的格式。
2.1 Block.entry
每个 Block 有多个 entry,每个 entry 就是一个 Key-Value:
- 1.shared_unshard_bytes 即是当前 entry 独有的字节数,value_bytes 即是当前 entry 的值的长度
- 2.unshared_key_data 是当前 entry 独有的 key 内容,value_data 即是值内容
从 entry 的定义来看,存储了 key 和 value 的长度及字节序列,它可以存储任意的字节流,哪怕是图片,这一点很棒!
2.2 Block
Block 包括多组 entry。各组是一个独立“共用前缀存储”的单元。restarts 是一个数组,存储着各组的开始位置。num_of_restarts 是 restarts 的元素个数。
更多的细节见于代码注释。
//block_builder.cc 文件
BlockBuilder::BlockBuilder(const Options* options)
: options_(options),
restarts_(),
counter_(0),
finished_(false) {
assert(options->block_restart_interval >= 1);
restarts_.push_back(0); // First restart point is at offset 0
}
void BlockBuilder::Reset() {
buffer_.clear();
restarts_.clear();
restarts_.push_back(0); // First restart point is at offset 0
counter_ = 0;
finished_ = false;
last_key_.clear();
}
size_t BlockBuilder::CurrentSizeEstimate() const {
return (buffer_.size() + // Raw data buffer
restarts_.size() * sizeof(uint32_t) + // Restart array
sizeof(uint32_t)); // Restart array length
}
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_);
}
/************************************************************************/
/*
lzh: 加入 (key, value) 到 block 中 ->
1. 一个 block 包含多个 entry
2. 一个 entry 包含一个 (key, value), 但是为了压缩, 后面的 entry 与前面的 entry 采用 diff 方式记录.
3. 具体而言: leveldb对key的存储进行前缀压缩,每个entry中会记录key与前一个key前缀相同的字节(shared_bytes)以及自己独有的字节(unshared_bytes)
读取时,对block进行遍历,每个key根据前一个key以及shared_bytes/unshared_bytes可以构造出来
4. 按 block 内的 entry 完全采用 diff 方式记录, 则像链表一样: 要随机地解析任意一个 entry 都需要从第一个开始, 所以优化为: 每 block_restart_interval
个 entry 采用 diff 方式记录.
5. 由于 Add 进来的 key 是按递增顺序加入的, 所以后一个 key 与前一个有较高的重叠概率
6. block 最后的五个字节(1字节表示是否压缩后4字节为 crc 校验码)是在完成一个 block 时写入的. 在 table_build.cc 中
lzh: 结构图详见本工程的资源文件: sst_and_block.bmp
*/
/************************************************************************/
void BlockBuilder::Add(const Slice& key, const Slice& value) {
//lzh: last_key 即是上一个 key
Slice last_key_piece(last_key_);
assert(!finished_);
//lzh: counter_ 一旦为 block_restart_interval 将会重新开始新一轮, 被设置为 0
assert(counter_ <= options_->block_restart_interval);
assert(buffer_.empty() // No values yet?
|| options_->comparator->Compare(key, last_key_piece) > 0);
size_t shared = 0;
//lzh: 当前处于一个 diff 记录中
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());
//lzh: last_key_piece 指上一个 entry 的 key. 计算当前的 key 与它有多长的重叠
while ((shared < min_length) && (last_key_piece[shared] == key[shared])) {
shared++;
}
} else {
//lzh: 完成一个区间的 diff 记录, 记录当前一轮的 entry 的大小
// Restart compression
restarts_.push_back(buffer_.size());
counter_ = 0;
}
const size_t non_shared = key.size() - shared;
// Add "<shared><non_shared><value_size>" to buffer_
//lzh: buffer_ 的格式为: 相同的字节数 | 不同的字节数 | value 字节数 | key 中不同的内容 | value 内容
PutVarint32(&buffer_, shared);
PutVarint32(&buffer_, non_shared);
PutVarint32(&buffer_, value.size());
// Add string delta to buffer_ followed by value
buffer_.append(key.data() + shared, non_shared);
buffer_.append(value.data(), value.size());
// Update state
//lzh: 下面两步计算完全是为了验证 last_key_ 是否和 key 一样
last_key_.resize(shared); //lzh: last_key_ 此时的值是上一个 key, 而 last_key_ 的前 shared 个字节与 key 的前 shared 个字节是一样的
last_key_.append(key.data() + shared, non_shared); //lzh: 将 key 的后面 non_shared 字节附到 last_key_ 后面,
assert(Slice(last_key_) == key); //lzh: last_key 将与 key 一样
counter_++;
}
3 leveldb sst 格式
sstable 是更高层次的存储,它组合了多个 Block,同时维持着它们的索引信息,结构如下图:
sst 中多个 Block 是 Key 有序的。
- 1.meta_block 对应于一个 data_block,保存data_block中的key size/value size/kv counts之类的统计信息,当前版本未实现。
- 2.metaindex_block: 保存meta_block的索引信息。当前版本未实现。
- 3.index_block: 每个 data_block 的 offset/size 都会写入到这个 index_block 中,起到索引的作用。
- 4.footer: 文件末尾的固定长度的数据。保存着metaindex_block和index_block的索引信息(位置和block内容大小), 为达到固定的长度,添加padding_bytes。最后有8个字节的magic校验。 代码如下:
/************************************************************************/
/*
lzh: 将 footer 编码到 dst 中, 包含 metaindex 数据和 index 数据, 对齐后再加入 8 字节 magic
*/
/************************************************************************/
void Footer::EncodeTo(std::string* dst) const {
#ifndef NDEBUG
const size_t original_size = dst->size();
#endif
//压入 metaindex_handle_ 和 index_handle_, 也即压入 meta 和 index 的 offset/size 信息 (这两个都是变长编码的)
metaindex_handle_.EncodeTo(dst);
index_handle_.EncodeTo(dst);
dst->resize(2 * BlockHandle::kMaxEncodedLength); // Padding //lzh: 对齐到 2 * BlockHandle::kMaxEncodedLength
//lzh: 再压入 8 字节的 magic 数据
PutFixed32(dst, static_cast<uint32_t>(kTableMagicNumber & 0xffffffffu));
PutFixed32(dst, static_cast<uint32_t>(kTableMagicNumber >> 32));
//lzh: 校验总长度
assert(dst->size() == original_size + kEncodedLength);
}
所有生成 sst 的细节都在 table_builder.cc 中,我们全部贴下来:
namespace leveldb {
struct TableBuilder::Rep {
Options options;
Options index_block_options;
WritableFile* file;
uint64_t offset;
Status status;
BlockBuilder data_block;
BlockBuilder index_block;
std::string last_key;
int64_t num_entries;
bool closed; // Either Finish() or Abandon() has been called.
// We do not emit the index entry for a block until we have seen the
// first key for the next data block. This allows us to use shorter
// keys in the index block. For example, consider a block boundary
// between the keys "the quick brown fox" and "the who". We can use
// "the r" as the key for the index block entry since it is >= all
// entries in the first block and < all entries in subsequent
// blocks.
//
// Invariant: r->pending_index_entry is true only if data_block is empty.
//lzh: 有一个 data_block 刚刚完成了 Flush (此时 pending_index_entry 被设置为 true),现在需要为它生成一个索引条目
bool pending_index_entry;
//lzh: 生成那个完成了 Flush 的 data_block 对应的索引条目并加入到 index_block 中
BlockHandle pending_handle; // Handle to add to index block
std::string compressed_output;
Rep(const Options& opt, WritableFile* f)
: options(opt),
index_block_options(opt),
file(f),
offset(0),
data_block(&options),
index_block(&index_block_options),
num_entries(0),
closed(false),
pending_index_entry(false) {
index_block_options.block_restart_interval = 1;
}
};
TableBuilder::TableBuilder(const Options& options, WritableFile* file)
: rep_(new Rep(options, file)) {
}
TableBuilder::~TableBuilder() {
assert(rep_->closed); // Catch errors where caller forgot to call Finish()
delete rep_;
}
Status TableBuilder::ChangeOptions(const Options& options) {
// Note: if more fields are added to Options, update
// this function to catch changes that should not be allowed to
// change in the middle of building a Table.
if (options.comparator != rep_->options.comparator) {
return Status::InvalidArgument("changing comparator while building table");
}
// Note that any live BlockBuilders point to rep_->options and therefore
// will automatically pick up the updated options.
rep_->options = options;
rep_->index_block_options = options;
rep_->index_block_options.block_restart_interval = 1;
return Status::OK();
}
/************************************************************************/
/*
lzh: 往 sst 当前处理的块/Block 中加入 kv,若此块满了则 Flush(写入到磁盘),
且设置此块的偏移和大小, 生成此块的索引条目加入到 index_block
*/
/************************************************************************/
void TableBuilder::Add(const Slice& key, const Slice& value) {
Rep* r = rep_;
assert(!r->closed);
if (!ok()) return;
//lzh: 注意因为 r->last_key 可能是 r->num_entries 为零时通过 FindShortestSeparator 计算出来的, 所以要判断 r->num_entries
if (r->num_entries > 0) {
//lzh: 保证 TableBuilder 以递增的顺序 Add
assert(r->options.comparator->Compare(key, Slice(r->last_key)) > 0);
}
//lzh: pending_index_entry 的意思是: 当前块 r->data_block 满了, 已经 Flush 掉(写入磁盘), pending_index_entry 被设置为 true,当前需要写入那个块的 index 数据
//lzh: 现在将刚刚已经 Flush 的 Block 的 index 信息写入 index_block
if (r->pending_index_entry) {
assert(r->data_block.empty());//lzh: Flush 完一个 data_block 后立即生成一个 index_block.
//lzh: 暂时停止生成 data_block 所以这里断定 data_block 为空
//lzh: index_block 的每个条目是一个键值对: key -> index_handle。定位 k 所在的 Block 时,在 index_block 里面使用二分法
//lzh: 搜索到最后一个键比 k 小的条目,再取出 index_handle,这样就知道了目标 Block 的偏移和大小。
//lzh: r->last_key 是刚 Flush 的 Block 的最后一个键。函数参数 key 是当前要加入到下一个 Block 中的首个键。
//lzh: 我们寻找比 r->last_key 大但比 key 小的最小值(长度比较短) 设置到 r->last_key 中
//lzh: 作为 index_block 条目的键,这样可以在逻辑正确的情况下减少 index_block 占用的空间
r->options.comparator->FindShortestSeparator(&r->last_key, key);
std::string handle_encoding;
//lzh: r->pending_handle 中的 offset/size 会在 WriteBlock 中被设置
r->pending_handle.EncodeTo(&handle_encoding);
r->index_block.Add(r->last_key, Slice(handle_encoding));
r->pending_index_entry = false;
}
r->last_key.assign(key.data(), key.size());
r->num_entries++;
r->data_block.Add(key, value);
const size_t estimated_block_size = r->data_block.CurrentSizeEstimate();
//lzh: 当前块 data_block 的大小到达阈值, 执行 Flush.
if (estimated_block_size >= r->options.block_size) {
Flush();
}
}
/************************************************************************/
/*
lzh:
1.将当前 data_block 写入到磁盘
2.设置 r->pending_index_entry=true, 并记录当前块的偏移和大小, 下一次需要添加此块的 index 条目到 index_block
*/
/************************************************************************/
void TableBuilder::Flush() {
Rep* r = rep_;
assert(!r->closed);
if (!ok()) return;
if (r->data_block.empty()) return;
//lzh: 当前块还没有执行 Flush, 此块的 pending_index_entry 必然还是 false
assert(!r->pending_index_entry);
WriteBlock(&r->data_block, &r->pending_handle);
if (ok()) {
r->pending_index_entry = true; //lzh: 写入块完成后, 后续需要收集此块的 index 数据(在 TableBuilder::Add 做)
r->status = r->file->Flush();
}
}
/************************************************************************/
/*
lzh:
1.将 block 写入到磁盘(可能会压缩)
2.将 block 的偏移大小写入到 handle 中,(注意首个 block 的 offset是0,且 block 的 size 不含 trailer 大小)
*/
/************************************************************************/
void TableBuilder::WriteBlock(BlockBuilder* block, BlockHandle* handle) {
// File format contains a sequence of blocks where each block has:
// block_data: uint8[n]
// type: uint8
// crc: uint32
assert(ok());
Rep* r = rep_;
Slice raw = block->Finish();
Slice block_contents;
CompressionType type = r->options.compression;
// TODO(postrelease): Support more compression options: zlib?
switch (type) {
case kNoCompression:
block_contents = raw;
break;
case kSnappyCompression: {
std::string* compressed = &r->compressed_output;
if (port::Snappy_Compress(raw.data(), raw.size(), compressed) &&
compressed->size() < raw.size() - (raw.size() / 8u)) {
block_contents = *compressed;
} else {
// Snappy not supported, or compressed less than 12.5%, so just
// store uncompressed form
block_contents = raw;
type = kNoCompression;
}
break;
}
}
//lzh: 记录当前块的 index 数据.
handle->set_offset(r->offset);
handle->set_size(block_contents.size());
r->status = r->file->Append(block_contents);
if (r->status.ok()) {
//lzh: 每个block后面都会有5个字节的trailer,第1个字节表示 block 内的数据是否采用了压缩, 后面 4个字节是 block 数据的 crc 校验码. 参见 资源文件 sst_and_block.bmp 文件
char trailer[kBlockTrailerSize];
trailer[0] = type;
uint32_t crc = crc32c::Value(block_contents.data(), block_contents.size());
crc = crc32c::Extend(crc, trailer, 1); // Extend crc to cover block type
EncodeFixed32(trailer+1, crc32c::Mask(crc));
r->status = r->file->Append(Slice(trailer, kBlockTrailerSize));
if (r->status.ok()) {
r->offset += block_contents.size() + kBlockTrailerSize; //下一个 block 的 offset 应该是上个块的 offset 加上上个块的大小和 trailer 大小
}
}
r->compressed_output.clear();
block->Reset();
}
Status TableBuilder::status() const {
return rep_->status;
}
/************************************************************************/
/*
lzh: 最后调用 Finish 完成这些事情:
1.Flush 掉还没有写入磁盘的 data_block
2.写入一个 metaindex_block,当前的实现是直接写入一个空的块
3.生成最后一个 Flush 的块的索引条目并加入到 index_block 中
4.将 index_block 写入到磁盘
注意当前版本没有写入 meta_block
回顾一下 sst 的格式:
1. meta_block:每个data_block对应一个meta_block ,保存data_block中的key size/value size/kv counts之类的统计信息,当前版本未实现
2. metaindex_block: 保存meta_block的索引信息。当前版本未实现。
3. index_block: 每个 data_block 的 offset/size 都会写入到这个 index_block 中
4. footer: 文件末尾的固定长度的数据。保存着metaindex_block和index_block的索引信息, 为达到固定的长度,添加padding_bytes。最后有8个字节的magic校验
*/
/************************************************************************/
Status TableBuilder::Finish() {
Rep* r = rep_;
//lzh: 当前块还没有处理完,写入磁盘
Flush();
assert(!r->closed);
r->closed = true;
BlockHandle metaindex_block_handle;
BlockHandle index_block_handle;
if (ok()) {
//lzh: 下面写入一个空的 block
BlockBuilder meta_index_block(&r->options);
// TODO(postrelease): Add stats and other meta blocks
//lzh: 因为没有对 meta_index_block 调用过 Add 函数,这个 block 中没有 entry
//lzh: 此处写入一个空的 block, 只含有 num_of_restarts(0) 及 trailer
WriteBlock(&meta_index_block, &metaindex_block_handle);
}
if (ok()) {
//lzh: 最后一个处理的块的 index 还没有加入到 index_block 里,现在处理
if (r->pending_index_entry) {
//lzh: 最后一个块, 找到大于 r->last_key 的最小值. 目的同 FindShortestSeparator, 减少 index_block 中键的长度
r->options.comparator->FindShortSuccessor(&r->last_key);
std::string handle_encoding;
r->pending_handle.EncodeTo(&handle_encoding);
r->index_block.Add(r->last_key, Slice(handle_encoding));
r->pending_index_entry = false;
}
//lzh: 接下来写入 index_block
WriteBlock(&r->index_block, &index_block_handle);
}
//lzh: 最后写入 footer: metaindex_block 的偏移大小和 index_block 的偏移大小
if (ok()) {
Footer footer;
footer.set_metaindex_handle(metaindex_block_handle);
footer.set_index_handle(index_block_handle);
std::string footer_encoding;
//lzh: 压入 metaindex_block_handle 和 index_block_handle (对齐后再加入 magic)
footer.EncodeTo(&footer_encoding);
r->status = r->file->Append(footer_encoding);
if (r->status.ok()) {
r->offset += footer_encoding.size();
}
}
return r->status;
}
void TableBuilder::Abandon() {
Rep* r = rep_;
assert(!r->closed);
r->closed = true;
}
uint64_t TableBuilder::NumEntries() const {
return rep_->num_entries;
}
uint64_t TableBuilder::FileSize() const {
return rep_->offset;
}
}
4 leveldb 文件层次
leveldb 存储数据的形式是 sst 文件,为了查找/写入数据的效率,对这些文件分总共7层进行管理(第0到6层)。第1层到第6层:每层的文件
5 leveldb 限制
3.1 第0层文件的限制
leveldb::config 中定义了第 0 层的文件有如下限制
- 1.leveldb 多层存储结构最多 7 层
- 2.第 0 层的 sst 文件在达到 4 个即触发 compaction
- 3.第 0 层的 sst 文件达到 8 个时就会降低写入的速度
- 4.第 0 层的 sst 文件达到 12 个时会暂停写入
3.2 memtable 的限制
还定义了 memtalbe 在 dump 到磁盘上的限制:memtable 最高作为第 2 层的 sst 写到磁盘。
//block_builder.cc
namespace config {
static const int kNumLevels = 7;
// Level-0 compaction is started when we hit this many files.
//lzh: 第 0 层文件最多4个文件,到达此限制则开始 compaction
static const int kL0_CompactionTrigger = 4;
// Soft limit on number of level-0 files. We slow down writes at this point.
//lzh: 第 0 层文件到达了 8 个文件时,则开始限制写入的速度
static const int kL0_SlowdownWritesTrigger = 8;
// Maximum number of level-0 files. We stop writes at this point.
//lzh: 第 0 层文件到达了 12 个文件时开始暂停写入
static const int kL0_StopWritesTrigger = 12;
// Maximum level to which a new compacted memtable is pushed if it
// does not create overlap. We try to push to level 2 to avoid the
// relatively expensive level 0=>1 compactions and to avoid some
// expensive manifest file operations. We do not push all the way to
// the largest level since that can generate a lot of wasted disk
// space if the same key space is being repeatedly overwritten.
//lzh: 在将 memtable 写到磁盘上时,并非一定会写到第 0 层,因为这可能导致
//lzh: 第 0 层文件与第 1 层文件有较大的重叠,增加后面 compaction 负担,
//lzh: 所以可以跳过一些层次,直接写到更高层次的文件中。
//lzh: kMaxMemCompactLevel 就指示了,从 memtable 写入到最高什么层次
static const int kMaxMemCompactLevel = 2;
}
[1]: leveldb精读系列-key