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());
  }
}

三. 日志文件

为什么存在
  1. 因为为了提高写性能,所有写入的数据都保存在memtable后便会返回成功,通过后端线程进行major comapct操作将内存中的数据持久化到磁盘。如果不持久化日志,宕机会造成数据丢失
  2. 日志文件是追加写操作,也就是顺序写,比随机写快很多
  3. 追加写可以进行批量写入操作,提高吞吐量,减少磁盘操作对延迟影响,特别是设置sync=true的情况
如何保存
  1. 首先所有的单写操作都会转换为批量写操作,以提高写性能,每次批量写的数据为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);
}
  1. 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;
如何读取
  1. 每个log文件都和memtable对应,当memtable进行持久化后,对应的文件就会被删除,因此每个log文件的大小都是有限制的
  2. VersionEdit中log_number_记录对应的memtable已经持久化的文件,因此所有long_number大于log_number_的日志都需要进行重做
  3. 重做时,也是按照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
  1. 按序排列,加载到内存后,直接内存操作查找无需转换成特殊的结构
  2. 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
  1. 数据存放方式按照组织结构进行
  2. 每个Block会记录是否进行压缩,并记录crc校验码
  3. Footer保存FilterIndexBlock和IndexBlock的地址信息
如何读取
读取过程
  1. 将根据FilterIndexBlock和IndexBlock加载index_block,FilterBlock(只有一个Block)
  2. 通过index_block判断该key所在的DataBlock
  3. 通过FilterBlock判断该key在DataBlock不存在
  4. 在DataBlock中通过二分查找快速获取key
具体实现
  1. Table实现了从sstable文件中加载元数据,通过index_value读取Block到缓存,查找key的功能
  2. TwoLevelIterator通过index_iter和data_iter实现了对sstable进行迭代的功能
posted @ 2022-07-23 08:37  nhj11  阅读(434)  评论(0)    收藏  举报