无论index block还是data block两者的内部结构都相同(都是block的结构),
均使用block的Seek方法进行二分查找,根据重启点进行二分查找
查找key先从index block查找,再从data block查找
index block的key是两个data block的分割点,大于等于当前block的所有key,小于下一个block的最小key
value是对应data block地址的偏移量,根据这个value可定位data block两者的内部结构都相同
data block存放的是真实数据的key,value
先通过index_iter_找到第一个 >= target 的 key, 对应的 value 是某个 data block 的 size&offset,接下来继续在该 data block 内查找。
为什么可以直接在这个 data block 内查找?我们看下原因
首先,(index_iter_ - 1).key() < target,而index_iter_ - 1对应的 data block 内所有的 key 都满足 <= (index_iter_ - 1)->key(),因此该 data block1 内所有的 key 都满足< target
其次,index_iter_.key() >= target,而index_iter_对应的 data block2 内所有的 key 都满足 <= index_iter_->key()
即data block2 内所有的 key 都满足 <= target
由于index block的key是两个data block的分割点,大于等于当前block的所有key,小于下一个block的最小key
index_iter + 1对应的 data block3 的最小key必大于index_iter_.key(),而index_iter_.key() >= target,
即data block3 的最小key大于target,由于data block1,2,3是连续,因此,如果 target 存在于该 sstable,那么一定存在于index_iter_当前指向的 data block
Status TableBuilder::Finish()
写入filter_block_handle, metaindex_block_handle, index_block_handle
footer的编码和解码
void Footer::EncodeTo(std::string* dst) const
Status Footer::DecodeFrom(Slice* input)
查找分隔点
void TableBuilder::Add(const Slice& key, const Slice& value)
r->options.comparator->FindShortestSeparator
刚写入了一个data block后设置r->pending_index_entry为true
将数据写入sstable
Status BuildTable(const std::string& dbname, Env* env, const Options& options,
TableCache* table_cache, Iterator* iter, FileMetaData* meta)
对 Version 的修改主要发生于 compaction 之后。compaction 分为 minor compaction 和 major compaction,其中 minor compaction 是落 memtable 到 L0,只会新增文件,而 major compaction 会跨 level 做合并,既新增文件也删除文件。每当这时,便会生成一个新的 VersionEdit 产生新的 Version,插入 VersionSet 链表的头部
什么时候删除Version
Version含有引用计数,当引用计数为0时,会delete掉执行析构函数
析构函数里面会先从VersionSet里面删除对应的Version
然后检查Version里面包含的文件里面的引用计数,如果引用计数为0,则删除
uint64_t new_log_number = versions_->NewFileNumber()
logfile_number_ = new_log_number;
void DBImpl::CompactMemTable() // 549
VersionSet和VersionEdit含有log_number_
MakeRoomForWrite会执行MaybeScheduleCompaction
MakeRoomForWrite => MaybeScheduleCompaction => BGWork => BackgroundCall => BackgroundCompaction
=> CompactMemTable => RemoveObsoleteFiles
void DBImpl::CompactMemTable() edit.SetLogNumber(logfile_number_); // Earlier logs no longer needed
当成功将immetable写入0层sstable时,将versionEdit的logNumber设置为最新(即Db的log_number_),用于后面删除不需要保留的logFile
RecoverLogFile根据logFile恢复丢失数据
跳表比较器 Status DBImpl::MakeRoomForWrite(bool force) mem_ = new MemTable(internal_comparator_);
comparator.cc
Status DBImpl::Write(const WriteOptions& options, WriteBatch* updates)
WriteBatch::Put => Status DBImpl::Write
压缩时sstable的选择
PickLevelForMemTableOutput memtable dump 出来 sstable 的选择
如何确定 Compaction 的输入源
PickCompaction()
第0层新增文件时比较version_set.cc BySmallestKey
Status TableBuilder::Finish() 写入metaindexblock和indexblock
Status DBImpl::WriteLevel0Table => Status BuildTable => Status TableBuilder::Finish
一个sstable有一个index block和多个data block
当写入数据时,data block大于4M时,会持久化磁盘,并更新index block
void BlockBuilder::Add 把每个key,value写入buffer_,并更新restart_
restarts_数组存放每个key-value的大小
data+restart_ 第一个重启点的偏移量, 加上restarts_数组每个元素的大小即可得出每个重启点的偏移量
index block和data block里每16个元素设置一个重启点,每个重启点会写入当前buffer_大小(即当前data block已写入key_value的大小)
当data block大于4M时,会持久化磁盘,清空buffer_
ReuseManifest
Status Table::Open 从sstable读取footer和index block
写入sstable时,先将key-value写入data block(先写入data block),然后写meta index block,再写index block,最后写footer
DBImpl::Recover => DBImpl:RecoverLogFile => DBImpl:WriteLevel0Table => VersionSet::NewFileNumber
启动时next_file_number_递增的顺序:
VersionSet::Recover(next_file_number_ = next_file + 1) => DBImpl::WriteLevel0Table(NewFileNumber) =>
DBImpl::Recover(MarkFileNumberUsed)
f->allowed_seeks = static_cast<int>((f->file_size / 16384U)); // file_size/16KB
16kb的计算:
1MB/25=40KB
40KB/25=16
LevelDB:Compaction(为什么stats->seek_file在查找key时只记录一次,即如果查找的文件多于一个,则记录下第一个查找的文件)
https://www.jianshu.com/p/527c3a00f79e?hmsr=toutiao.io&utm_medium=toutiao.io&utm_source=toutiao.io
SeekToRestartPoint:找到重启点指向的key
NextEntryOffset:获取下一个key的位置,如果刚调用过SeekToRestartPoint,此时返回第一条entry,SeekToRestartPoint传入的size_为0
基于leveldb开源的项目 ssdb rocksdb
leveldb current的作用:
current文件用于指向当前使用的MANIFEST,因为每次启动时会生成新的MANIFEST,这时候会有多个MANIFEST,需要确定使用哪一个MANIFEST,
断电是可能发生的,当断电时可能新生成的MANIFEST还没有完全写入,这时候current仍指向旧的MANIFEST,当MANIFEST完全写入的时候,会将
current指向新MANIFEST,也可能MANIFEST完全写入了,但current还没来得及更新记录就断电,这时候仍指向旧的MANIFEST
在选中 level n+1 层的 sstable 后,可能还可以加入一些 level n 的 sstable,但为什么不扩大level n+1 层的范围呢
因为compaction主要删除重复的数据,而选中 level n+1 层的 sstable后已包含所有和leven n层重叠的数据,扩大level n+1 层的范围并
不会增加重复的数据,只增加compaction的文件从而更耗时
MaybeScheduleCompaction的调用时机:
Status DBImpl::Get
Status DBImpl::MakeRoomForWrite
void DBImpl::BackgroundCall() (Compaction会增加level n+1层所占的总字节数,有可能需要再次Compaction)
300 100101100
172 10101100
2 00000010
10101100 00000010
Version析构函数可以直接delete f(FileMetaData)的原因:
f持有引用计数,它会被多个Version引用,当引用计数为0时,即代表没有Version引用它(或引用它的Version已析构)
故可以delte释放内存
将manifes(多个VersionEdit)应用到Builder: Status VersionSet::Recover
Status VersionSet::LogAndApply:
新建Version,持有VersionSet的指针,新建Builder,持有VersionSet和当前current_的指针
将当前的更改edit传入Builder
将当前Builder应用到Version(当前Version(即current_)+edit=新建Version)
新建manifest,将当前Version(即current_)和更改(即edit)写入manifest
旧current_引用减1,将新建Version设置为当前Version
VersionSet log_number,next_file,last_sequence,prev_log_number设置
数据库启动时会调用Status VersionSet::Recover
从manifest文件读取所有edit,并更新log_number,next_file,last_sequence,prev_log_number
如果一开始没有manifest文件,则这些值都为0
versions_->NewFileNumber调用的地方(用于log_file和tablefile):
Status DBImpl::WriteLevel0Table (用于tablefile,写入0层的sstable)
Status DBImpl::MakeRoomForWrite(用于log_file)
Status DB::Open (用于log_file)
OpenCompactionOutputFile (Compaction时生成新的输出文件tablefile)
如果压缩的时候已生成新的sstable,但还没有写入manifest就断电,
将会在下次压缩的时候删除这个sstable(因为这个文件没有元数据FileMetaData,从而RemoveObsoleteFiles会删除)
RemoveObsoleteFiles
各组件默认大小: options.h
memtable: 4MB size_t write_buffer_size = 4 * 1024 * 1024; Status DBImpl::MakeRoomForWrite
sstable:2MB size_t max_file_size = 2 * 1024 * 1024; Status DBImpl::DoCompactionWork
log 文件,由一系列的 32kb 物理块组成 static const int kBlockSize = 32768; Status Writer::AddRecord
SStable 的 datablock, 变长,但最小为 4kb size_t block_size = 4 * 1024; void TableBuilder::Add
各 level 的大小
level0,不按大小算,而是按照文件个数算,level0 默认为 4 个文件触发一次 compaction。其他还有两个配置,1 个 8 个文件,在 MakeRoomForWrite 函数中使用,一个 12,达到 12 个文件,系统将暂停写入
// Level-0 compaction is started when we hit this many files.
static const int kL0_CompactionTrigger = 4;
// Soft limit on number of level-0 files. We slow down writes at this point.
static const int kL0_SlowdownWritesTrigger = 8;
// Maximum number of level-0 files. We stop writes at this point.
static const int kL0_StopWritesTrigger = 12;
其他 level, level1 10M, level 2 100M, 逐层递乘 10. MaxBytesForLevel