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 略有冗余。
- SequnceNumber: WriteBatch 中开始使用的 SequnceNumber。
- count: 批量处理的 record 数量
- 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
};
- kLogFile 日志文件:[0-9]+.logleveldb 的写流程是先记 binlog,然后写 sstable,该日志文件即是 binlog。前缀数字为FileNumber。
- kDBLockFile,lock 文件:LOCK一个 db 同时只能有一个 db 实例操作,通过对 LOCK 文件加文件锁(flock)实现主动保护。
- kTableFile,sstable 文件:[0-9]+.sst保存数据的 sstable 文件。前缀为 FileNumber。
- kDescriptorFile,db 元信息文件:MANIFEST-[0-9]+每当 db 中的状态改变(VersionSet),会将这次改变(VersionEdit)追加到 descriptor 文件中。后缀数字为 FileNumber。
- kCurrentFile,:CURRENTCURRENT 文件中保存当前使用的 descriptor 文件的文件名。
- kTempFile,临时文件:[0-9]+.dbtmp对 db 做修复(Repairer)时,会产生临时文件。前缀为 FileNumber。
- 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_,这里详细说明他们的意义。
- 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_)。 - 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 持有句柄的引用)。