leveldb存储格式
一. 数据库文件
leveldb每个数据库对应一个目录并且每个数据库只能被一个进程打开。
*.ldb:数据存储文件
*.log:日志文件
MANIFEST-*:元数据信息
CURRENT:当前版本的元数据文件名
LOCK:文件锁,避免被多个进程打开
LOG:leveldb日志信息
二. 元数据文件
为什么存在
每次打开数据库时,不会加载任何*.ldb,因此需要在内存维持这些文件元数据的信息,这些信息会在Get/Compact时使用。
何时生成
每次compact生成新的sstable文件时,便会将该sstable文件的元数据信息追加写入元数据文件(VersionSet::LogAndApply)
DB::Open: open时会判断是否存在尚未写入sstable的记录,通过log进行重做,如果生成新的sstable文件进行保存
DBImpl::CompactMemTable:major compact
DBImpl::BackgroundCompaction:compact时存在一些情况,能够直接将该level层comapct生成的文件放入level+2层,减少comapct次数
DBImpl::InstallCompactionResults:comapct生成新的sstable文件,减少旧的sstable文件
如何保存
void VersionEdit::EncodeTo(std::string* dst) const {
if (has_comparator_) {
PutVarint32(dst, kComparator);
PutLengthPrefixedSlice(dst, comparator_);
}
if (has_log_number_) {
PutVarint32(dst, kLogNumber);
PutVarint64(dst, log_number_);
}
// 兼容老版本
if (has_prev_log_number_) {
PutVarint32(dst, kPrevLogNumber);
PutVarint64(dst, prev_log_number_);
}
if (has_next_file_number_) {
PutVarint32(dst, kNextFileNumber);
PutVarint64(dst, next_file_number_);
}
if (has_last_sequence_) {
PutVarint32(dst, kLastSequence);
PutVarint64(dst, last_sequence_);
}
// 保存compact_pointers
for (size_t i = 0; i < compact_pointers_.size(); i++) {
PutVarint32(dst, kCompactPointer);
PutVarint32(dst, compact_pointers_[i].first); // level
PutLengthPrefixedSlice(dst, compact_pointers_[i].second.Encode());
}
// 保存可以删除的文件
for (const auto& deleted_file_kvp : deleted_files_) {
PutVarint32(dst, kDeletedFile);
PutVarint32(dst, deleted_file_kvp.first); // level
PutVarint64(dst, deleted_file_kvp.second); // file number
}
// 保存新创建的文件
for (size_t i = 0; i < new_files_.size(); i++) {
const FileMetaData& f = new_files_[i].second;
PutVarint32(dst, kNewFile);
PutVarint32(dst, new_files_[i].first); // level
PutVarint64(dst, f.number);
PutVarint64(dst, f.file_size);
PutLengthPrefixedSlice(dst, f.smallest.Encode());
PutLengthPrefixedSlice(dst, f.largest.Encode());
}
}
三. 日志文件
为什么存在
- 因为为了提高写性能,所有写入的数据都保存在memtable后便会返回成功,通过后端线程进行major comapct操作将内存中的数据持久化到磁盘。如果不持久化日志,宕机会造成数据丢失
- 日志文件是追加写操作,也就是顺序写,比随机写快很多
- 追加写可以进行批量写入操作,提高吞吐量,减少磁盘操作对延迟影响,特别是设置sync=true的情况
如何保存
- 首先所有的单写操作都会转换为批量写操作,以提高写性能,每次批量写的数据为1M
// WriteBatch::rep_ :=
// sequence: fixed64
// count: fixed32
// data: record[count]
// record :=
// kTypeValue varstring varstring
// kTypeDeletion varstring
// varstring :=
// len: varint32
// data: uint8[len]
void WriteBatch::Put(const Slice& key, const Slice& value) {
// 记录个数+1
WriteBatchInternal::SetCount(this, WriteBatchInternal::Count(this) + 1);
// 操作类型
rep_.push_back(static_cast<char>(kTypeValue));
// 将key/value添加到rep_,先保存size,再保存value
PutLengthPrefixedSlice(&rep_, key);
PutLengthPrefixedSlice(&rep_, value);
}
void WriteBatch::Delete(const Slice& key) {
WriteBatchInternal::SetCount(this, WriteBatchInternal::Count(this) + 1);
rep_.push_back(static_cast<char>(kTypeDeletion));
PutLengthPrefixedSlice(&rep_, key);
}
- Log文件按照32k大小的Block对WriteBatch进行保存,每缓存一个Block进行一次write操作,如果WriteBatch不足32k,也会进行write。
enum RecordType {
// Zero is reserved for preallocated files
kZeroType = 0,
kFullType = 1,
// For fragments, 一个Block保存不了一个WriteBatch
kFirstType = 2,
kMiddleType = 3,
kLastType = 4
};
static const int kMaxRecordType = kLastType;
// log文件中每个block的大小为32K
static const int kBlockSize = 32768;
// Header is checksum (4 bytes), length (2 bytes), type (1 byte).
static const int kHeaderSize = 4 + 2 + 1;
如何读取
- 每个log文件都和memtable对应,当memtable进行持久化后,对应的文件就会被删除,因此每个log文件的大小都是有限制的
- VersionEdit中log_number_记录对应的memtable已经持久化的文件,因此所有long_number大于log_number_的日志都需要进行重做
- 重做时,也是按照WriteBatch从log文件中读取数据,通过WriteBatch中的sequnce创建inner_key放入memtable
四. sstable文件
sstable是按照按序保存key-value,并且有以下特点:
- sstable文件加载到内存后无需进行排序等操作,能够直接使用sstable中的元数据信息在内存中进行查找
- 以Block为单位保存数据,每个block为4K,这样避免将整个文件加载到内存,便于缓存的利用
- 通过restart_point, index_block, filter_block的方式加速查找过程
如何存放
组织结构
所有的数据都是以Block的方式保存的,只是Block中存放的数据不一样。
DataBlock::存放key-value信息
DataBlock
...
FilterBlock:存放布隆过滤器,先保存所有的Block对应的Filter,然后保存这些Filter的偏移量
FilterIndexBlock:保存FilterBlock的位置,key为filter.*,value为FilterBlock的偏移量
IndexBlock: DataBlock的位置,key为每个Block中最大的key,value为每个DataBlock的偏移量
Footer
BlockBuilder
- 按序排列,加载到内存后,直接内存操作查找无需转换成特殊的结构
- BlockBuilder设置了restart_point, 用于快速查找和压缩数据
- 快速查找,每block_restart_interval个Entry设置保存一个偏移量,通过restart二分查找Entry所在的block_restart_interval,然后进行遍历查找
- 压缩数据,因为已经进行排序,那么前缀可能相同,因此只有restart_point的节点保存完整的key,之后的block_restart_interval个Entry只保存部分key
// An entry for a particular key-value pair has the form:
// shared_bytes: varint32
// unshared_bytes: varint32
// value_length: varint32
// key_delta: char[unshared_bytes]
// value: char[value_length]
// shared_bytes == 0 for restart points.
//
// The trailer of the block has the form:
// restarts: uint32[num_restarts]
// num_restarts: uint32
// restarts[i] contains the offset within the block of the ith restart point.
TableBuilder
- 数据存放方式按照组织结构进行
- 每个Block会记录是否进行压缩,并记录crc校验码
- Footer保存FilterIndexBlock和IndexBlock的地址信息
如何读取
读取过程
- 将根据FilterIndexBlock和IndexBlock加载index_block,FilterBlock(只有一个Block)
- 通过index_block判断该key所在的DataBlock
- 通过FilterBlock判断该key在DataBlock不存在
- 在DataBlock中通过二分查找快速获取key
具体实现
- Table实现了从sstable文件中加载元数据,通过index_value读取Block到缓存,查找key的功能
- TwoLevelIterator通过index_iter和data_iter实现了对sstable进行迭代的功能

浙公网安备 33010602011771号