关于memtable的生命周期与flush流程

既然谈到什么周期,那我们得明确,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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值