leveldb 基本概念

基本概念

ref: leveldb实现解析

Slice

数据的长度信息和内容信息被包装成一个整体结构,叫做Slice
include/leveldb/slice.h

class Slice {
// ... other
private:
    const char* data_;
    size_t size_;
};

Option

leveldb的配置文件,包括启动时的配置,读写数据的配置
include/leveldb/option.h

// Options to control the behavior of a database (passed to DB::Open)
struct LEVELDB_EXPORT Options {
  // Create an Options object with default values for all fields.
  Options();

  // -------------------
  // Parameters that affect behavior

  // Comparator used to define the order of keys in the table.
  // Default: a comparator that uses lexicographic byte-wise ordering
  //
  // REQUIRES: The client must ensure that the comparator supplied
  // here has the same name and orders keys *exactly* the same as the
  // comparator provided to previous open calls on the same DB.
  // 传入的比较器
  const Comparator* comparator;

  // If true, the database will be created if it is missing.
  bool create_if_missing = false;

  // If true, an error is raised if the database already exists.
  bool error_if_exists = false;

  // If true, the implementation will do aggressive checking of the
  // data it is processing and will stop early if it detects any
  // errors.  This may have unforeseen ramifications: for example, a
  // corruption of one DB entry may cause a large number of entries to
  // become unreadable or for the entire DB to become unopenable.
  // 是否保存中间的错误状态(RecoverLog/compact),compact 时是否读到的 block 做检验。
  bool paranoid_checks = false;

  // Use the specified object to interact with the environment,
  // e.g. to read/write files, schedule background work, etc.
  // Default: Env::Default()
  // 传入的ENV
  Env* env;

  // Any internal progress/error information generated by the db will
  // be written to info_log if it is non-null, or to a file stored
  // in the same directory as the DB contents if info_log is null.
  // 传入的打印日志的Logger
  Logger* info_log = nullptr;

  // -------------------
  // Parameters that affect performance

  // Amount of data to build up in memory (backed by an unsorted log
  // on disk) before converting to a sorted on-disk file.
  //
  // Larger values increase performance, especially during bulk loads.
  // Up to two write buffers may be held in memory at the same time,
  // so you may wish to adjust this parameter to control memory usage.
  // Also, a larger write buffer will result in a longer recovery time
  // the next time the database is opened.
  // memtable的最大大小
  size_t write_buffer_size = 4 * 1024 * 1024;

  // Number of open files that can be used by the DB.  You may need to
  // increase this if your database has a large working set (budget
  // one open file per 2MB of working set).
  // db 中打开的文件最大个数
  // db 中需要打开的文件包括基本的 CURRENT/LOG/MANIFEST/LOCK, 以及打开的 sstable 文件。
  // sstable 一旦打开,就会将 index 信息加入 TableCache,所以把
  // (max_open_files - 10)作为 table cache 的最大数量.
  int max_open_files = 1000;

  // Control over blocks (user data is stored in a set of blocks, and
  // a block is the unit of reading from disk).

  // If non-null, use the specified cache for blocks.
  // If null, leveldb will automatically create and use an 8MB internal cache.
  // 传入的 block 数据的 cache 管理
  Cache* block_cache = nullptr;

  // Approximate size of user data packed per block.  Note that the
  // block size specified here corresponds to uncompressed data.  The
  // actual size of the unit read from disk may be smaller if
  // compression is enabled.  This parameter can be changed dynamically.
  // sstable 中 block 的 size
  size_t block_size = 4 * 1024;

  // Number of keys between restart points for delta encoding of keys.
  // This parameter can be changed dynamically.  Most clients should
  // leave this parameter alone.
  // block 中对 key 做前缀压缩的区间长度
  int block_restart_interval = 16;

  // Leveldb will write up to this amount of bytes to a file before
  // switching to a new one.
  // Most clients should leave this parameter alone.  However if your
  // filesystem is more efficient with larger files, you could
  // consider increasing the value.  The downside will be longer
  // compactions and hence longer latency/performance hiccups.
  // Another reason to increase this parameter might be when you are
  // initially populating a large database.
  size_t max_file_size = 2 * 1024 * 1024;

  // Compress blocks using the specified compression algorithm.  This
  // parameter can be changed dynamically.
  //
  // Default: kSnappyCompression, which gives lightweight but fast
  // compression.
  //
  // Typical speeds of kSnappyCompression on an Intel(R) Core(TM)2 2.4GHz:
  //    ~200-500MB/s compression
  //    ~400-800MB/s decompression
  // Note that these speeds are significantly faster than most
  // persistent storage speeds, and therefore it is typically never
  // worth switching to kNoCompression.  Even if the input data is
  // incompressible, the kSnappyCompression implementation will
  // efficiently detect that and will switch to uncompressed mode.
  // 压缩数据使用的压缩类型(默认支持 snappy,其他类型需要使用者实现)
  CompressionType compression = kSnappyCompression;

  // EXPERIMENTAL: If true, append to existing MANIFEST and log files
  // when a database is opened.  This can significantly speed up open.
  //
  // Default: currently false, but may become true later.
  bool reuse_logs = false;

  // If non-null, use the specified filter policy to reduce disk reads.
  // Many applications will benefit from passing the result of
  // NewBloomFilterPolicy() here.
  const FilterPolicy* filter_policy = nullptr;
};

// Options that control read operations
struct LEVELDB_EXPORT ReadOptions {
  // If true, all data read from underlying storage will be
  // verified against corresponding checksums.
  // 是否对读到的 block 做校验
  bool verify_checksums = false;

  // Should the data read for this iteration be cached in memory?
  // Callers may wish to set this field to false for bulk scans.
  // 读到的 block 是否加入 block cache
  bool fill_cache = true;

  // If "snapshot" is non-null, read as of the supplied snapshot
  // (which must belong to the DB that is being read and which must
  // not have been released).  If "snapshot" is null, use an implicit
  // snapshot of the state at the beginning of this read operation.
  // 指定读取的 SnapShot
  const Snapshot* snapshot = nullptr;
};

// Options that control write operations
struct LEVELDB_EXPORT WriteOptions {
  WriteOptions() = default;

  // If true, the write will be flushed from the operating system
  // buffer cache (by calling WritableFile::Sync()) before the write
  // is considered complete.  If this flag is true, writes will be
  // slower.
  //
  // If this flag is false, and the machine crashes, some recent
  // writes may be lost.  Note that if it is just the process that
  // crashes (i.e., the machine does not reboot), no writes will be
  // lost even if sync==false.
  //
  // In other words, a DB write with sync==false has similar
  // crash semantics as the "write()" system call.  A DB write
  // with sync==true has similar crash semantics to a "write()"
  // system call followed by "fsync()".
  // write 时,记 binlog 之后,是否对 binlog 做 sync。
  bool sync = false;
};

编译时常量

namespace config {  //  db/dbformat.h
// level 的最大值
static const int kNumLevels = 7;

// Level-0 compaction is started when we hit this many files.
// level-0 中 sstable 的数量超过这个阈值,触发 compact
static const int kL0_CompactionTrigger = 4;

// Soft limit on number of level-0 files.  We slow down writes at this point.
// level-0 中 sstable 的数量超过这个阈值, 慢处理此次写(sleep1ms)
static const int kL0_SlowdownWritesTrigger = 8;

// Maximum number of level-0 files.  We stop writes at this point.
 // level-0 中 sstable 的数量超过这个阈值, 阻塞至 compact memtable 完成。
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.
// memtable dump 成的 sstable,允许推向的最高 level
static const int kMaxMemCompactLevel = 2;

// Approximate gap in bytes between samples of data read during iteration.
static const int kReadBytesPeriod = 1048576;

}  // namespace config

// db/version_set.cc
// compact 过程中,level-0 中的 sstable 由 memtable 直接 dump 生成,不做大小限制
// 非 level-0 中的 sstable 的大小设定为 TargetFileSize
static size_t TargetFileSize(const Options* options) {
  return options->max_file_size;
}

// Maximum bytes of overlaps in grandparent (i.e., level+2) before we
// stop building a single file in a level->level+1 compaction.
// compact level-n 时,与 level-n+2 产生 overlap 的数据 size (参见 Compaction)
static int64_t MaxGrandParentOverlapBytes(const Options* options) {
  return 10 * TargetFileSize(options);
}

ENV

include/leveldb/env.h util/env_posix.h
考虑到移植以及灵活性,leveldb 将系统相关的处理(文件/进程/时间之类)抽象成 Env,用户可以自己实现相应的接口,作为 Option 传入。默认使用自带的实现。

varint

util/coding.h
leveldb 采用了 protocalbuffer 里使用的变长整形编码方法,节省空间

ValueType

db/dbformat.h
leveldb 更新(put/delete)某个 key 时不会操控到 db 中的数据,每次操作都是直接新插入一份 kv 数据,具体的数据合并和清除由后台的 compact 完成。所以,每次 put,db 中就会新加入一份 KV 数据,即使该 key 已经存在;而 delete 等同于 put 空的 value。为了区分真实 kv 数据和删除操作的 mock 数据,使用 ValueType 来标识:\

// Value types encoded as the last component of internal keys.
// DO NOT CHANGE THESE ENUM VALUES: they are embedded in the on-disk
// data structures.
enum ValueType { kTypeDeletion = 0x0, kTypeValue = 0x1 };

SequenceNumber

db/dbformat.h
leveldb 中的每次更新(put/delete)操作都拥有一个版本,由 SequnceNumber 来标识,整个 db 有一个全局值保存着当前使用到的 SequnceNumber。SequnceNumber 在 leveldb 有重要的地位,key 的排序,compact 以及 snapshot 都依赖于它。typedef uint64_t SequenceNumber;存储时,SequnceNumber 只占用 56 bits, ValueType 占用 8 bits,二者共同占用 64bits(uint64_t).

typedef uint64_t SequenceNumber;

// We leave eight bits empty at the bottom so a type and sequence#
// can be packed together into 64-bits.
static const SequenceNumber kMaxSequenceNumber = ((0x1ull << 56) - 1);

user key

用户层面传入的 key,使用 Slice 格式。

ParsedInternalKey

db/dbformat.h
db 内部操作的 key。db 内部需要将 user key 加入元信息(ValueType/SequenceNumber)一并做处理。

struct ParsedInternalKey {
  Slice user_key;
  SequenceNumber sequence;
  ValueType type;

  ParsedInternalKey() {}  // Intentionally left uninitialized (for speed)
  ParsedInternalKey(const Slice& u, const SequenceNumber& seq, ValueType t)
      : user_key(u), sequence(seq), type(t) {}
  std::string DebugString() const;
};

InternalKey

db/dbformat.h
db 内部,包装易用的结构,包含 userkey 与 SequnceNumber/ValueType

LookupKey

db/dbformat.h
db 内部在为查找 memtable/sstable 方便,包装使用的 key 结构,保存有 userkey 与SequnceNumber/ValueType dump 在内存的数据。

// A helper class useful for DBImpl::Get()
class LookupKey {
 public:
  // Initialize *this for looking up user_key at a snapshot with
  // the specified sequence number.
  LookupKey(const Slice& user_key, SequenceNumber sequence);

  LookupKey(const LookupKey&) = delete;
  LookupKey& operator=(const LookupKey&) = delete;

  ~LookupKey();

  // Return a key suitable for lookup in a MemTable.
  Slice memtable_key() const { return Slice(start_, end_ - start_); }

  // Return an internal key (suitable for passing to an internal iterator)
  Slice internal_key() const { return Slice(kstart_, end_ - kstart_); }

  // Return the user key
  Slice user_key() const { return Slice(kstart_, end_ - kstart_ - 8); }

 private:
  // We construct a char array of the form:
  //    klength  varint32               <-- start_
  //    userkey  char[klength]          <-- kstart_
  //    tag      uint64
  //                                    <-- end_
  // The array is a suitable MemTable key.
  // The suffix starting with "userkey" can be used as an InternalKey.
  const char* start_;
  const char* kstart_;
  const char* end_;
  char space_[200];  // Avoid allocation for short keys
};
// 对 memtable 进行 lookup 时使用 [start,end], 对 sstable lookup 时使用[kstart, end]。

Comparator

include/leveldb/comparator.h util/comparator.cc
对 key 排序时使用的比较方法。leveldb 中 key 为升序。用户可以自定义 user key 的 comparator(user-comparator),作为 option 传入,默认采用 bytecompare(memcmp)。comparator 中有 FindShortestSeparator()/ FindShortSuccessor()两个接口,FindShortestSeparator(start,limit)是获得大于 start 但小于 limit 的最小值。FindShortSuccessor(start)是获得比 start 大的最小值。比较都基于 user-commparator,二者会被用来确定 sstable 中 block 的 end-key。

InternalKeyComparator

db/dbformat.h
db 内部做 key 排序时使用的比较方法。排序时,会先使用 user-comparator 比较 user-key,如果user-key 相同,则比较 SequnceNumber,SequnceNumber 大的为小。因为 SequnceNumber 在 db 中全局递增,所以,对于相同的 user-key,最新的更新(SequnceNumber 更大)排在前面,在查找的时候,会被先找到。InternalKeyComparator 中 FindShortestSeparator()/ FindShortSuccessor()的实现,仅从传入的内部 key 参数,解析出 user-key,然后再调用 user-comparator 的对应接口。

WriteBatch

db/write_batch.cc
对若干数目 key 的 write 操作(put/delete)封装成 WriteBatch。它会将 userkey 连同SequnceNumber 和 ValueType 先做 encode,然后做 decode,将数据 insert 到指定的 Handler(memtable)上面。上层的处理逻辑简洁,但 encode/decode 略有冗余。

  1. SequnceNumber: WriteBatch 中开始使用的 SequnceNumber。
  2. count: 批量处理的 record 数量
  3. record:封装在 WriteBatch 内的数据。
    如果 ValueType 是 kTypeValue,则后面有 key 和 value
    如果 ValueType 是 kTypeDeletion,则后面只有 key。

Memtable

db/memtable.cc db/skiplist.h
db 数据在内存中的存储格式。写操作的数据都会先写到 memtable 中。memtable 的 size 有限制最大值(write_buffer_size)。memtable 的实现是 skiplist。当一个 memtable size 到达阈值时,会变成只读的 memtable(immutable memtable),同时生成一个新的 memtable 供新的写入。后台的 compact 进程会负责将 immutable memtable dump 成 sstable。所以,同时最多会存在两个 memtable(正在写的 memtable 和 immutable memtable)。

Sstable

table/table.cc
db 数据持久化的文件。文件的 size 有限制最大值(target_file_size)。文件前面为数据,后面是索引元信息

FileMetaData

db/version_edit.h
sstable 文件的元信息封装成 FileMetaData

struct FileMetaData {
  FileMetaData() : refs(0), allowed_seeks(1 << 30), file_size(0) {}

  int refs;  // 引用计数
  int allowed_seeks;  // compact之前允许的seek次数
  uint64_t number;    // File number
  uint64_t file_size;    // File size
  InternalKey smallest;  // sstable文件的最小key
  InternalKey largest;   // sstable文件的最大key
};

block

table/block.cc
sstable 的数据由一个个的 block 组成。当持久化数据时,多份 KV 聚合成 block 一次写入;当读取时,也是以 block 单位做 IO。sstable 的索引信息中会保存符合 key-range 的 block 在文件中的offset/size(BlockHandle)。

BlockHandle

table/format.h
block 的元信息(位于 sstable 的 offset/size)封装成 BlockHandle

FileNumber

db/dbformat.h
db 创建文件时会按照规则将 FileNumber 加上特定后缀作为文件名。所以,运行时只需要记录FileNumber(uint64_t)即可定位到具体的文件路径,省掉了字符串的麻烦。FileNumber 在 db 中全局递增

filename

db/filename.h

enum FileType {
  kLogFile,
  kDBLockFile,
  kTableFile,
  kDescriptorFile,
  kCurrentFile,
  kTempFile,
  kInfoLogFile  // Either the current one, or an old one
};
  1. kLogFile 日志文件:[0-9]+.logleveldb 的写流程是先记 binlog,然后写 sstable,该日志文件即是 binlog。前缀数字为FileNumber。
  2. kDBLockFile,lock 文件:LOCK一个 db 同时只能有一个 db 实例操作,通过对 LOCK 文件加文件锁(flock)实现主动保护。
  3. kTableFile,sstable 文件:[0-9]+.sst保存数据的 sstable 文件。前缀为 FileNumber。
  4. kDescriptorFile,db 元信息文件:MANIFEST-[0-9]+每当 db 中的状态改变(VersionSet),会将这次改变(VersionEdit)追加到 descriptor 文件中。后缀数字为 FileNumber。
  5. kCurrentFile,:CURRENTCURRENT 文件中保存当前使用的 descriptor 文件的文件名。
  6. kTempFile,临时文件:[0-9]+.dbtmp对 db 做修复(Repairer)时,会产生临时文件。前缀为 FileNumber。
  7. kInfoLogFile,db 运行时打印日志的文件:LOGdb 运行时,打印的 info 日志保存在 LOG 中。每次重新运行,如果已经存在 LOG 文件,会先将 LOG文件重名成 LOG.old

level-n

db/version_set.h
为了均衡读写的效率,sstable 文件分层次(level)管理,db 预定义了最大的 level 值。compact 进程负责 level 之间的均衡

compact

db/db_impl.cc db/version_set.cc
db 中有一个 compact 后台进程,负责将 memtable 持久化成 sstable,以及均衡整个 db 中各 level 的sstable。 Comapct 进程会优先将已经写满的 memtable dump 成 level-0 的 sstable(不会合并相同key 或者清理已经删除的 key)。然后,根据设计的策略选取 level-n 以及 level-n+1 中有 key-rangeoverlap 的几个 sstable 进行 merge(期间会合并相同的 key 以及清理删除的 key),最后生成若干个level-(n+1)的 ssatble。随着数据不断的写入和 compact 的进行,低 level 的 sstable 不断向高level 迁移。level-0 中的 sstable 因为是由 memtable 直接 dump 得到,所以 key-range 可能 overlap,而 level-1 以及更高 level 中的 sstable 都是做 merge 产生,保证了位于同 level 的 sstable 之间,key-range 不会 overlap,这个特性有利于读的处理。

Compaction

db/version_set.cc

// A Compaction encapsulates information about a compaction.
class Compaction {
  //...
  int level_;  // 要compact的level
  uint64_t max_output_file_size_;  // 生成sstable的最大size
  Version* input_version_;  // compact时当前的version
  VersionEdit edit_;  // 记录compact过程中的操作

  // Each compaction reads inputs from "level_" and "level_+1"
  std::vector<FileMetaData*> inputs_[2];  // The two sets of inputs   inputs_[0]是level-n的sstable文件信息   inputs_[1]是level-n+1的sstable信息

  // 位于 level-n+2,并且与 compact 的 key-range 有 overlap 的 sstable。
  // 保存 grandparents_是因为 compact 最终会生成一系列 level-n+1 的 sstable,
  // 而如果生成的 sstable 与 level-n+2 中有过多的 overlap 的话,当 compact
  // level-n+1 时,会产生过多的 merge,为了尽量避免这种情况,compact 过程中
  // 需要检查与 level-n+2 中产生 overlap 的 size 并与
  // 阈值 kMaxGrandParentOverlapBytes 做比较,
  // 以便提前中止 compact
  // State used to check for number of overlapping grandparent files
  // (parent == level_ + 1, grandparent == level_ + 2)
  std::vector<FileMetaData*> grandparents_;
  size_t grandparent_index_;  // Index in grandparent_starts_  记录 compact 时 grandparents_中已经 overlap 的 index
  // 记录是否已经有 key 检查 overlap
  // 如果是第一次检查,发现有 overlap,也不会增加 overlapped_bytes_.
  // (没有看到这样做的意义)
  bool seen_key_;             // Some output key has been seen
  int64_t overlapped_bytes_;  // Bytes of overlap between current output  记录已经 overlap 的累计 size
                              // and grandparent files

  // State for implementing IsBaseLevelForKey

  // compact 时,当 key 的 ValueType 是 kTypeDeletion 时,
  // 要检查其在 level-n+1 以上是否存在(IsBaseLevelForKey())
  // 来决定是否丢弃掉该 key。因为 compact 时,key 的遍历是顺序的,
  // 所以每次检查从上一次检查结束的地方开始即可,
  // level_ptrs_[i]中就记录了 input_version_->levels_[i]中,上一次比较结束的sstable 的容器下标。
  // level_ptrs_ holds indices into input_version_->levels_: our state
  // is that we are positioned at one of the file ranges for each
  // higher level than the ones involved in this compaction (i.e. for
  // all L >= level_ + 2).
  size_t level_ptrs_[config::kNumLevels];
};

Version

db/version_set.cc
将每次 compact 后的最新数据状态定义为 Version,也就是当前 db 元信息以及每个 level 上具有最新数据状态的 sstable 集合。compact 会在某个 level 上新加入或者删除一些 sstable,但可能这个时候,那些要删除的 sstable 正在被读,为了处理这样的读写竞争情况,基于 sstable 文件一旦生成就不会改动的特点,每个 Version 加入引用计数,读以及解除读操作会将引用计数相应加减一。这样, db 中可能有多个 Version 同时存在(提供服务),它们通过链表链接起来。当 Version 的引用计数为 0 并且不是当前最新的 Version 时,它会从链表中移除,对应的,该 Version 内的 sstable 就可以删除了(这些废弃的 sstable 会在下一次 compact 完成时被清理掉)

class Version {
  // ...
  VersionSet* vset_;  // VersionSet to which this Version belongs  属于的 VersionSet
  Version* next_;     // Next version in linked list  链表指针 next
  Version* prev_;     // Previous version in linked list
  int refs_;          // Number of live refs to this version  引用计数
 
  // 每个 level 的所有 sstable 元信息。
  // files_[i]中的 FileMetaData 按照 FileMetaData::smallest 排序,
  // 这是在每次更新都保证的。(参见 VersionSet::Builder::Save())
  // List of files per level
  std::vector<FileMetaData*> files_[config::kNumLevels];

  // Next file to compact based on seek stats.  需要 compact 的文件(allowed_seeks 用光)
  FileMetaData* file_to_compact_;
  int file_to_compact_level_;

  // 当前最大的 compact 权重以及对应的 level
  // Level that should be compacted next and its compaction score.
  // Score < 1 means compaction is not strictly needed.  These fields
  // are initialized by Finalize().
  double compaction_score_;
  int compaction_level_;
};

Version 中与 compact 相关的有 file_to_compact_/ file_to_compact_level_,compaction_score_/compaction_level_,这里详细说明他们的意义。

  1. compaction_score_: leveldb 中分 level 管理 sstable,对于写,可以认为与 sstable 无关。而基于 get 的流程(参见get 流程),各 level 中的 sstable 的 count,size 以及 range 分布,会直接影响读的效率。可以预想的最佳情形可能是 level-0 中最多有一个 sstable,level-1 以及之上的各 level 中 keyrange 分布均匀,期望更多的查找可以遍历最少的 level 即可定位到。将这种预想的最佳状态定义成: level 处于均衡的状态。当采用具体的参数量化,也就量化了各个level 的不均衡比重,即 compact 权重: score。score 越大,表示该 level 越不均衡,需要更优先进行 compact。
    每个 level 的具体均衡参数及比重计算策略如下:
    a. 因为 level-0 的 sstable range 可能 overlap,所以如果 level-0 上有过多的 sstable,在做查找时,会严重影响效率。同时,因为 level-0 中的 sstable 由 memtable 直接 dump 得到,并不受kTargetFileSize(生成 sstable 的 size)的控制,所以 sstable 的 count 更有意义。基于此,对于 level-0,均衡的状态需要满足:sstable 的 count < kL0_CompactionTrigger。score = sstable 的 count/ kL0_CompactionTrigger。为了控制这个数量, 另外还有 kL0_SlowdownWritesTrigger/kL0_StopWritesTrigger 两个阈值来主动控制写的速率(参见 put 流程)。
    b. 对于 level-1 及以上的 level,sstable 均由 compact 过程产生,生成的 sstable 大小被kTargetFileSize 控 制 , 所 以 可 以 限 定 sstable 总 的 size 。当前的策略是设置初始值kBaseLevelSize,然后以 10 的指数级按 level 增长。每个 level 可以容纳的 quota_size =kBaseLevelSize * 10^(level_number-1)。所以 level-1 可以容纳总共 kBaseLevelSize 的sstable,level-2 允许 kBaseLevelSize*10……基于此,对于 level-1 及以上的 level均衡的状态需要满足:sstable 的 size < quota_size。score = sstable 的 size / quota_size。每次 compact 完成,生效新的 Version 时(VersionSet::Finalize()),都会根据上述的策略,计算出每个 level 的 score,取最大值作为当前 Version 的 compaction_score_,同时记录对应的level(compaction_level_)。
  2. file_to_compact_: leveldb 对单个 sstable 文件的 IO 也做了细化的优化,设计了一个巧妙的策略。首先,一个 sstable 如果被 seek 到多次(一次 seek 意味找到这个 sstable 进行 IO),可以认为它处在不最优的情况(尤其处于高 level),而我们认为 compact 后会倾向于均衡的状态,所以在一个 sstable 的 seek 次数达到一定阈值后,主动对其进行 compact 是合理的。这个具体 seek 次数阈值(allowed_seeks)的确定,依赖于 sas 盘的 IO 性能:
    a. 一次磁盘寻道 seek 耗费 10ms。
    b. 读或者写 1M 数据耗费 10ms (按 100M/s IO 吞吐能力)。
    c. compact 1M 的数据需要 25M 的 IO:从 level-n 中读 1M 数据,从 level-n+1 中读 10~12M 数据,写入 level-n+1 中 10~12M 数据。所以,compact 1M 的数据的时间相当于做 25 次磁盘 seek,反过来说就是,1 次 seek 相当于compact 40k 数据。那么,可以得到 seek 阈值 allowed_seeks=sstable_size / 40k。保守设置,当前实际的 allowed_seeks = sstable_size / 10k。每次 compact 完成,构造新的 Version 时(Builder::Apply()),每个 sstable 的 allowed_seeks 会计算出来保存在 FileMetaData。在每次 get 操作的时候,如果有超过一个 sstable 文件进行了 IO,会将最后一个 IO 的 sstable 的allowed_seeks 减一,并检查其是否已经用光了 allowed_seeks,若是,则将该 sstable 记录成当前Version 的 file_to_compact_,并记录其所在的 level(file_to_compact_level_)。

VersionSet

db/version_set.h
整个 db 的当前状态被 VersionSet 管理着,其中有当前最新的 Version 以及其他正在服务的 Version链表;全局的 SequnceNumber,FileNumber;当前的 manifest_file_number; 封装 sstable 的TableCache。 每个 level 中下一次 compact 要选取的 start_key 等等。

class VersionSet {
  // ...
  Env* const env_;  // 实际的ENV
  const std::string dbname_;  // db的数据路径
  const Options* const options_;  // 传入的option
  TableCache* const table_cache_;  // 操作 sstable 的 TableCache
  const InternalKeyComparator icmp_; // comparator
  uint64_t next_file_number_; // 下一个可用的 FileNumber  
  uint64_t manifest_file_number_;// manifest 文件的 FileNumber
  uint64_t last_sequence_;// 最后用过的 SequnceNumber
  uint64_t log_number_; // log 文件的 FileNumber
  uint64_t prev_log_number_;  // 0 or backing store for memtable being compacted   辅助 log 文件的 FileNumber,在 compact memtable 时,置为 0

  // Opened lazily
  WritableFile* descriptor_file_;  // manifest 文件的封装
  log::Writer* descriptor_log_;  // manifest 文件的 writer
  Version dummy_versions_;  // Head of circular doubly-linked list of versions.   正在服务的 Version 链表
  Version* current_;        // == dummy_versions_.prev_   当前最新的的 Version

  // 为了尽量均匀 compact 每个 level,所以会将这一次 compact 的 end-key 作为
  // 下一次 compact 的 start-key。compactor_pointer_就保存着每个 level
  // 下一次 compact 的 start-key.
  // 除了 current_外的 Version,并不会做 compact,所以这个值并不保存在 Version 中。
  // Per-level key at which the next compaction at that level should start.
  // Either an empty string, or a valid InternalKey.
  std::string compact_pointer_[config::kNumLevels];
};

VersionEdit

db/version_edit.cc

compact 过程中会有一系列改变当前 Version 的操作(FileNumber 增加,删除 input 的 sstable,增加输出的 sstable……),为了缩小 Version 切换的时间点,将这些操作封装成 VersionEdit,compact完成时,将 VersionEdit 中的操作一次应用到当前 Version 即可得到最新状态的 Version。

每次 compact 之后都会将对应的 VersionEdit encode 入 manifest 文件。


 /
 /class VersionEdit {
  // ...

  typedef std::set<std::pair<int, uint64_t>> DeletedFileSet;

  std::string comparator_;// db 一旦创建,排序的逻辑就必须保持兼容,用 comparator 的名字做凭证
  uint64_t log_number_;// log 的 FileNumber
  uint64_t prev_log_number_;// 辅助 log 的 FileNumber
  uint64_t next_file_number_;// 下一个可用的 FileNumber
  SequenceNumber last_sequence_;// 用过的最后一个 SequnceNumber
  // 标识是否存在,验证使用
  bool has_comparator_;
  bool has_log_number_;
  bool has_prev_log_number_;
  bool has_next_file_number_;
  bool has_last_sequence_;

  std::vector<std::pair<int, InternalKey>> compact_pointers_;// 要更新的 level ==》 compact_pointer。
  DeletedFileSet deleted_files_;// 要删除的 sstable 文件(compact 的 input)
  std::vector<std::pair<int, FileMetaData>> new_files_;// 新的文件(compact 的 output)
};

VersionSet::Builder

db/version_set.cc

将 VersionEdit 应用到 VersonSet 上的过程封装成 VersionSet::Builder.主要是更新Version::files_[]

以 base_->files_[level]为基准,根据 levels_中 LevelStat 的 deleted_files/added_files 做 merge,输出到新 Version 的 files_[level] (VersionSet::Builder::SaveTo()).
1) 对于每个 level n, base_->files_[n]与 added_files 做 merge,输出到新 Version 的 files_[n]中。过程中根据 deleted_files 将要删除的丢弃掉(VersionSet::Builder:: MaybeAddFile()),。
2) 处理完成,新 Version 中的 files_[level]有了最新的 sstable 集合(FileMetaData)。

class VersionSet::Builder {
 private:
  // Helper to sort by v->files_[file_number].smallest  处理 Version::files_[i]中 FileMetaData 的排序
  struct BySmallestKey {
    const InternalKeyComparator* internal_comparator;

    bool operator()(FileMetaData* f1, FileMetaData* f2) const {
      int r = internal_comparator->Compare(f1->smallest, f2->smallest);
      if (r != 0) {
        return (r < 0);
      } else {
        // Break ties by file number
        return (f1->number < f2->number);
      }
    }
  };

  typedef std::set<FileMetaData*, BySmallestKey> FileSet;  //  排序的 sstable(FileMetaData)集合
  struct LevelState {// 要添加和删除的 sstable 文件集合
    std::set<uint64_t> deleted_files;
    FileSet* added_files;
  };

  VersionSet* vset_;// 要更新的 VersionSet
  Version* base_; // 基准的 Version,compact 后,将 current_传入作为 base
  // 各个 level 上要更新的文件集合(LevelStat)
  // compact 时,并不是每个 level 都有更新(level-n/level-n+1)。
  LevelState levels_[config::kNumLevels];
// ...
}

Manifest

db/version_set.cc

为了重启 db 后可以恢复退出前的状态,需要将 db 中的状态保存下来,这些状态信息就保存在manifeest 文件中。当 db 出现异常时,为了能够尽可能多的恢复,manifest 中不会只保存当前的状态,而是将历史的状态都保存下来。又考虑到每次状态的完全保存需要的空间和耗费的时间会较多,当前采用的方式是,只在 manifest 开始保存完整的状态信息(VersionSet::WriteSnapshot()),接下来只保存每次compact 产生的操作(VesrionEdit),重启 db 时,根据开头的起始状态,依次将后续的 VersionEdit replay,即可恢复到退出前的状态(Vesrion)。

TableBuilder/BlockBuilder

table/table_builder.cc table/block_builder.cc

生成 block 的过程封装成 BlockBuilder 处理。生出 sstable 的过程封装成 TableBuilder 处理。

Iterator

include/leveldb/iterator.h

leveldb 中对 key 的查找和遍历,上层统一使用 Iterator 的方式处理,屏蔽底层的处理,统一逻辑。提供 RegisterCleanup()可以在 Iterator 销毁时,做一些清理工作(比如释放 Iterator 持有句柄的引用)。

posted @ 2022-07-15 10:34  荒唐了年少  阅读(499)  评论(0编辑  收藏  举报