目录
既然谈到什么周期,那我们得明确,MemTable至少有下面几个变化点:
1 刚启动的时候或者说启动恢复的时候,从无到有。
2 Memtable变成Immemtable的时候。
3 Immemtable下刷到磁盘变成sst的时候。
memtable变成immemtable
这个逻辑主要实现在SwitchMemtable里面,它的核心逻辑就是
创建一个新的Memtable,替换之前那个,然后把之前那个加到Immemtable里面,同时也新建一个WAL。
有一个问题,我只是把可读的memtable变成了不可读的immemtable,为什么要来一个新的wal文件呢?
这个问题得这么看:
第一, 不新建一个新的wal文件,行不行?
我觉得是可以的。这两个没有必然的数据安全性问题。
第二:那为什么还要换wal文件?
我认为的一个可能原因是:为了数据隔离性。
如果不切换新WAL文件,那就是一个wal文件同时对应一个mem,一个immem一共两个内存结构。切换了之后,那就是一个wal对应一个mem文件(暂时先忽略CF的问题)。
一对一肯定比一对二逻辑清晰么。或者再另一个角度来说,新建一个新的wal文件也没有什么大的弊端么。
在RocksDB中会有四个地方会调用SwitchMemtable,分别是:
DbImpl::HandleWriteBufferFull
DBImpl::SwitchWAL
DBImpl::FlushMemTable
DBImpl::ScheduleFlushes
DbImpl::HandleWriteBufferFull
情况是:所有ColumnFamily的memtable总内存超过限制(db_write_buffer_size)
代码流程:
BImpl::WriteImpl->
DBImpl::PreprocessWrite->
DBImpl::HandleWriteBufferManagerFlush->
DBImpl::SwitchMemtable
也就是说每次写入操作的时候,都会检查一下。但是一般默认情况下,db_write_buffer_size为0,也就是不检查。
DBImpl::SwitchWAL
情况是:磁盘上的wal文件的体积超过了限制(max_total_wal_size)
这个值如果没有设置,就会把多个columnFamily的write_buffer_size* max_write_buffer_number的积相加,然后让使用的logsize与它相比。
调用过程:
BImpl::WriteImpl->
DBImpl::PreprocessWrite->
DBImpl::SwitchWAL->
DBImpl::SwitchMemtable
DBImpl::FlushMemTable
FlushMemTable是用户主动调用的
调用流程:
FlushMemTable->
SwitchMemtable
DBImpl::ScheduleFlushes
这里有两个链路的代码
首先更新状态:
MemTable::Add->
MemTable::UpdateFlushState->
MemTable::ShouldFlushNow
看名字就能知道,上面的ShouldFlushNow就和容量检查有关,如果单个ColumnFamily下的一个可读的MemTable的size超过了设定值(write_buffer_size)shouldFlushNow就会返回true,然后UpdateFlushState就会把memtable的flush_state_改成FLUSH_REQUESTED
其次根据状态进行调度
MemTableInserter::PutCF->
MemTableInserter::PutCFImpl->
MemTableInserter::CheckMemtableFull
每次写操作的时候,会调用CheckMemtableFull,如果memtable的flush_state_是FLUSH_REQUESTED,就会把它改成FLUSH_SCHEDULED,同时也会初始化flush_scheduler_,之后检查的时候flush_scheduler_.Empty()就会是false
另一方面写的时候也会检查:
BImpl::WriteImpl->
DBImpl::PreprocessWrite->
flush_scheduler_.Empty()->
DBImpl::ScheduleFlushes->
DBImpl::SwitchMemtable
最终会调用SwitchMemtable
Immetable变成磁盘上的sst
在下面这几种条件下RocksDB会flush memtable到磁盘.
- 当某一个memtable的大小超过write_buffer_size.
- 当总的memtable的大小超过db_write_buffer_size.会把体积最大的immetable下刷(db_write_buffer_size默认是0,这个case忽略)
- 当WAL文件的大小超过max_total_wal_size之后
原因是,当WAL文件大小太大之后,我们需要清理WAL,因此此时我们需要将此WAL对应的数据都
超过write_buffer_size
这个其实和上面那个mem->immem是同样的调用顺序。
BImpl::WriteImpl->
DBImpl::PreprocessWrite->
flush_scheduler_.Empty()->
DBImpl::ScheduleFlushes->
SchedulePendingFlush(flush_req);
在SchedulePendingFlush里面会把对应cf的信息记录到一个名为flush_queue_的队列里。
而刷新MemTable到磁盘是一个后台线程来做的,这个后台线程叫做BGWorkFlush,最终这个函数会调用BackgroundFlush函数,而BackgroundFlush主要功能是在flush_queue_中找到一个ColumnFamily然后刷新它的memtable到磁盘。
超过max_total_wal_size
在之前的Switchwal里面就会调用SchedulePendingFlush。之后的流程就和上面的一样了。
超过db_write_buffer_size
BImpl::WriteImpl->
DBImpl::PreprocessWrite->
DBImpl::HandleWriteBufferManagerFlush->
DBImpl::SchedulePendingFlush
把数据从immemtable刷新到sst文件
上文已经说了刷新MemTable到磁盘是一个后台线程来做的,这个后台线程叫做BGWorkFlush,最终这个函数会调用BackgroundFlush函数,而BackgroundFlush主要功能是在flush_queue_中找到一个ColumnFamily然后刷新它的memtable到磁盘。
void DBImpl::MaybeScheduleFlushOrCompaction() {
while (!is_flush_pool_empty && unscheduled_flushes_ > 0 &&
bg_flush_scheduled_ < bg_job_limits.max_flushes) {
bg_flush_scheduled_++;
FlushThreadArg* fta = new FlushThreadArg;
fta->db_ = this;
fta->thread_pri_ = Env::Priority::HIGH;
// 启动后台线程BGWorkFlush
env_->Schedule(&DBImpl::BGWorkFlush, fta, Env::Priority::HIGH, this,
&DBImpl::UnscheduleFlushCallback);
--unscheduled_flushes_;
}
}
void DBImpl::BGWorkFlush(void* arg) {
FlushThreadArg fta = *(reinterpret_cast<FlushThreadArg*>(arg));
static_cast_with_check<DBImpl>(fta.db_)->BackgroundCallFlush(fta.thread_pri_);
}
在上面的BackgroundCallFlush里面又会调用BackgroundFlush
Status DBImpl::BackgroundFlush(bool* made_progress, JobContext* job_context,
LogBuffer* log_buffer) {
...
// 拿到一个满足条件的CF
while (!flush_queue_.empty()) {
auto first_cfd = PopFirstFromFlushQueue();
// 判断IsFlushPending是否真正需要flush
...
cfd = first_cfd;
break;
}
if (cfd != nullptr) {
...
// 将memtable写到磁盘
status = FlushMemTableToOutputFile(cfd, mutable_cf_options, made_progress,
job_context, log_buffer);
...
}
return status;
}
Status DBImpl::FlushMemTableToOutputFile(
ColumnFamilyData* cfd, const MutableCFOptions& mutable_cf_options,
bool* made_progress, JobContext* job_context,
SuperVersionContext* superversion_context,
...) {
// 创建flush job
FlushJob flush_job(
dbname_, cfd, immutable_db_options_, mutable_cf_options, max_memtable_id, ...);
// 从CF中拿到需要flush的memtable
flush_job.PickMemTable();
// memtable flush
s = flush_job.Run(&logs_with_prep_tracker_, &file_meta,
&switched_to_mempurge);
// 新的superversion上线
InstallSuperVersionAndScheduleWork(cfd, superversion_context,
mutable_cf_options);
}
Status FlushJob::Run(LogsWithPrepTracker* prep_tracker, FileMetaData* file_meta, bool* switched_to_mempurge) {
// 将memtable转换为L0 sstable
s = WriteLevel0Table();
}
Status FlushJob::WriteLevel0Table() {
// 遍历所有memtable创建iterator
for (MemTable* m : mems_) {
// memtable iterator
memtables.push_back(m->NewIterator(ro, &arena));
// range del iterator
auto* range_del_iter =
m->NewRangeTombstoneIterator(ro, kMaxSequenceNumber);
if (range_del_iter != nullptr) {
range_del_iters.emplace_back(range_del_iter);
}
}
// 基于前面创建每个memtable的iterator,创建merge iterator
ScopedArenaIterator iter(
NewMergingIterator(&cfd_->internal_comparator(), memtables.data(),
static_cast<int>(memtables.size()), &arena));
// 使用merge iterator和range del iterator,创建L0 sstable
s = BuildTable(dbname_, versions_, db_options_, tboptions, file_options_,
cfd_->table_cache(), iter.get(), std::move(range_del_iters), ...);
// table创建完成后更新versionedit
edit_->AddFile(0 /* level */, meta_.fd.GetNumber(), meta_.fd.GetPathId(),
meta_.fd.GetFileSize(), meta_.smallest, meta_.largest,
...);
}
Status BuildTable(
const std::string& dbname, VersionSet* versions,
const ImmutableDBOptions& db_options, const TableBuilderOptions& tboptions,
const FileOptions& file_options, TableCache* table_cache,
InternalIterator* iter,
std::vector<std::unique_ptr<FragmentedRangeTombstoneIterator>>
range_del_iters,...) {
// iter指向first
iter->SeekToFirst();
if (iter->Valid() || !range_del_agg->IsEmpty()) {
TableBuilder* builder;
// 创建一个file
IOStatus io_s = NewWritableFile(fs, fname, &file, file_options);
// 基于当前的iter,创建一个compaction的遍历迭代器
CompactionIterator c_iter(
iter, tboptions.internal_comparator.user_comparator(), &merge,
kMaxSequenceNumber, &snapshots, ...);
c_iter.SeekToFirst();
// 将key写入到file中
for (; c_iter.Valid(); c_iter.Next()) {
const Slice& key = c_iter.key();
const Slice& value = c_iter.value();
const ParsedInternalKey& ikey = c_iter.ikey();
std::cout<<" in fluest: value:"<<value.ToString() <<
" type:"<< static_cast<int>(ikey.type)<<
" user_key:"<<ikey.user_key.ToString()<<std::endl;
builder->Add(key, value);
}
}
}
咱们具体说说上面的CompactionIterator ,它内部持有的是直接指向各个immemtable的迭代器,CompactionIterator 这个迭代器最终返回的就是需要写给下层sst的kv对。具体来说,假如有下面的代码
std::string myValue="xxx";
db->Put(WriteOptions(), "a", myValue + std::to_string(1));
db->Put(WriteOptions(), "a", myValue + std::to_string(2));
db->Put(WriteOptions(), "a", myValue + std::to_string(3));
db->Delete(WriteOptions(), "a");
db->Put(WriteOptions(), "b", myValue + std::to_string(5));
db->Put(WriteOptions(), "b", myValue + std::to_string(6));
db->Put(WriteOptions(), "b", myValue + std::to_string(7));
db->Flush(rocksdb::FlushOptions());
在Flush中,CompactionIterator最终打印的日志是
in fluest: value: type:0 user_key:a
in fluest: value:xxx7 type:1 user_key:
而关于type
enum ValueType : unsigned char {
kTypeDeletion = 0x0,
kTypeValue = 0x1,
}
那最底层的imemtable的迭代器返回的数据是什么样子的呢?大家可以在CompactionIterator::NextFromInput里面加日志看。
具体来说,就是先打印字符顺序小的,然后如何字符顺序一样,就先打印seq大的。其实这也是memtalbe里面的skiplist里面数据的组织顺序。
上面部分变量
WritableFileWriter 的构造函数里面
env 是PosixEnv
FileSystem 是PosixFileSystem
file 是 PosixWritableFile
关于后半部分的代码,大量参考了https://blog.csdn.net/easonwx/article/details/126253897
add之后就是append
BlockBasedTableBuilder::Add(key, value);
BlockBasedTableBuilder::Flush
BlockBasedTableBuilder::WriteBlock
BlockBasedTableBuilder::WriteRawBlock
WritableFileWriter::Append
WritableFileWriter::Flush
WritableFileWriter::WriteBuffered
PosixWritableFile::Append
PosixWrite
write
PosixWritableFile::Flush
WritableFileWriter::Sync
这里需要强调一点,即使代码到PosixWritableFile::Flush里,也没有立即刷新到磁盘。
默认情况,当一个datablock的大小超过4k,就会进入到BlockBasedTableBuilder::Flush
在FSWritableFile::PrepareWrite里面preallocation_block_size_是0,相当于什么都没有做
然后判断一下当前的buf_里面的内存长度是否够新增加的sst datablock,如果够就拷贝到buf_里面,如果不够就扩容,最大长度是1MB(writable_file_max_buffer_size)
如果buf_里面已经放不下了,就会调用WritableFileWriter::Flush,把内存里的数据刷新到操作系统的缓存里。
关于磁盘的分配
从immemtable到sst没有预先分配空间,数据都先写到内存里的datablock里,如果超过4k,就进行刷新到WritableFileWriter的buf_里面,当buf_的数据超过1MB,就下刷到操作系统的缓存里,什么时候下刷到磁盘由操作系统决定。 最后一次写block会调用WritableFileWriter::Sync下刷到磁盘
compaction的时候,sst有预先分配空间,默认就是64MB(Compaction::OutputFilePreallocationSize),数据都先写到内存里的datablock里,如果超过4k,就进行刷新到WritableFileWriter的buf_里面,当buf_的数据超过1MB,就下刷到操作系统的缓存里,什么时候下刷到磁盘由操作系统决定。最后一次写block会调用WritableFileWriter::Sync下刷到磁盘
一些日志
根据 flush table 可以查到一次imm到L0的下刷
cat LOG |grep 'flush table'
参考资料
https://wanghenshui.github.io/rocksdb-doc-cn/doc/MemTable.html
https://bravoboy.github.io/2018/12/07/rocksdb-Memtable/
https://zhuanlan.zhihu.com/p/444460663
https://blog.csdn.net/ZNBase/article/details/127886018
https://zhuanlan.zhihu.com/p/414145200
https://blog.csdn.net/easonwx/article/details/126253897