rocksdb介绍之数据写入流程

一、前言

rocksdb是一个kv数据库,其介绍可以参考http://vpha.rd.tp-link.net/phame/post/view/3566/rocksdb_%E5%AD%98%E5%82%A8%E5%8E%9F%E7%90%86%E4%BB%8B%E7%BB%8D/

本文主要介绍rocksdb写入数据的流程与相关代码,因为是边学习边写的,可能有一些疏漏请读者见谅。

二、写入入口

rocksdb的写入操作大致可以分为4中:put, merge, deletion, range deletion。

put指的是插入操作,插入一个kv对。
merge是更新操作,修改一个值,例如可以+1。
deletion是删除操作,删除一个kv对。
range deletion是删除操作,删除k在一定范围内的所有kv对。

本文主要看比较基础的put操作。

一个比较简单的写入例程如下,插入了一个kv对"key1" “value”:

std::string kDBPath = "/tmp/rocksdb_simple_example";

int main() {
  DB* db;
  Options options;

  Status s = DB::Open(options, kDBPath, &db);
  s = db->Put(WriteOptions(), "key1", "value");
  delete db;
  return 0;
}

实际写入时,数据库内部会使用WriteBatch将"key1" "value"这个kv对进行封装,再调用实际的写入接口。
WriteBatch也可以存放多个写入请求,可以由外部进行组合:

std::string kDBPath = "/tmp/rocksdb_simple_example";

int main() {
  DB* db;
  Options options;

  Status s = DB::Open(options, kDBPath, &db);
  {
    WriteBatch batch;
    batch.Delete("key1");
    batch.Put("key2", value);
    s = db->Write(WriteOptions(), &batch);
  }
  delete db;
  return 0;
}

以上步骤完成后,数据就写入到硬盘了,但首先是写入到wal中,并在memtable中进行缓存,并没有写入到sst文件中,需要等待后续进行flush操作时才可以写入到sst文件。

二、整体写入流程

在这里插入图片描述

图片来源于网络。

可能有多个线程同时调用rocksdb的write,此时会选择一个线程作为leader,选择一些batch组成一个group,一起写入到wal,而其他线程则作为follower,等待leader完成wal的写入,之后各个线程并发写入memtable。
此过程中,可能有新的write请求,或先挂在队列上,需要等待上一个leader完成wal的写入后才能进行后续处理。

三、WriteBatch

以下是实际运行过程中,使用gdb打印处理的WriteBatch内容

(gdb) p *my_batch
$2 = {
  <rocksdb::WriteBatchBase> = {_vptr.WriteBatchBase = 0x555555ce36e8 <vtable for rocksdb::WriteBatch+16>}, 
  save_points_ = std::unique_ptr<rocksdb::SavePoints> = {get() = 0x0}, 
  wal_term_point_ = {
    size = 0, 
    count = 0,
    content_flags = 0
  }, 
  content_flags_ = {
    <std::__atomic_base<unsigned int>> = {static _S_alignment = 4, _M_i = 2},
    <No data fields>
  }, 
  max_bytes_ = 0, 
  is_latest_persistent_state_ = false,
  prot_info_ = std::unique_ptr<rocksdb::WriteBatch::ProtectionInfo> = {get() = 0x0}, 
  rep_ = "\000\000\000\000\000\000\000\000\001\000\000\000\001\003key\006value", 
  timestamp_size_ = 0
}

数据主要存放在rep_变量中,是一个字符串,官方的说明注释如下:

// WriteBatch::rep_ :=
//    sequence: fixed64
//    count: fixed32
//    data: record[count]
// record :=
//    kTypeValue varstring varstring
//    kTypeDeletion varstring
//    kTypeSingleDeletion varstring
//    kTypeRangeDeletion varstring varstring
//    kTypeMerge varstring varstring
//    kTypeColumnFamilyValue varint32 varstring varstring
//    kTypeColumnFamilyDeletion varint32 varstring
//    kTypeColumnFamilySingleDeletion varint32 varstring
//    kTypeColumnFamilyRangeDeletion varint32 varstring varstring
//    kTypeColumnFamilyMerge varint32 varstring varstring
//    kTypeBeginPrepareXID varstring
//    kTypeEndPrepareXID
//    kTypeCommitXID varstring
//    kTypeRollbackXID varstring
//    kTypeBeginPersistedPrepareXID varstring
//    kTypeBeginUnprepareXID varstring
//    kTypeNoop
// varstring :=
//    len: varint32
//    data: uint8[len]

再看rep_的值"\000\000\000\000\000\000\000\000\001\000\000\000\001\003key\006value"
可以拆分成:
sequence: “\000\000\000\000\000\000\000\000“,转化成数字就是0,是一个序号
count: ”\001\000\000\000”,转化成数字就是1,只有一个kv对
data: “\001\003key\006value”,只有一个kv对,可以进一步拆分成具体内容
type: “\001”,就是kTypeValue,对应 kTypeValue varstring varstring,也就是跟者两个varstring,分别是key和value
key: “\003key”,3表示key的长度
value: “\006value”,6表示value的长度,这里包括了\0的长度,这是因为写入的时候指定的长度是6,而写入key的时候指定的长度是3所以不包含\0

四、JoinBatchGroup

写数据基本在函数DBImpl::WriteImpl中进行,进行的第一个主要流程是JoinBatchGroup。

JointBatchGroup:

  1. 首先调用WriteThread::LinkOne函数,将请求挂到链表上,这个链表可能会包含多个请求,后续会通过一次io请求一起写入到wal中。挂到链表上的同时,会判断是否是该链表上的第一个请求,如果是第一个请求,那么这个请求就会作为leader,leader负责写wal,其他请求等待wal写完之后再继续。
  2. 如果不是leader,就会调用WriteThread::AwaitState函数,等待leader的唤醒。AwaitState的实现也比较复杂,主要是为了优化性能,leveldb中是直接使用条件变量的,rocksdb做了优化。参考https://www.cnblogs.com/cobbliu/articles/8511269.html

这里有个技巧是使用了CAS,多线程可以无锁并发执行。

五、leader

JoinBatchGroup后,确定了leader,下面看leader的处理流程。

PreprocessWrite

1.检查wal大小,如果超过了配置的最大大小,需要调用SwitchWAL;
2.检查memtable大小,如果超过了配置的最大大小,需要调用HandleWriteBufferFull
3.调用ScheduleFlushes,
4.检查是否需要执行DelayWrite,也就是发生了write stall,通常是因为flush或者compaction不及时
5.检查是否需要等待log sync完成,需要则等待直到logs_.front().getting_synced变为true。?为什么写之前等待

SwitchWal:

触发的原因是wal的大小超过了配置的上限,该配置可以通过修改option中的max_total_wal_size进行修改。一般来说,应该是先触发memtable满进行flush的,这个条件触发的应该比较少。

首先选择哪些column family需要进行处理,也就是判断wal中包含哪些column family的内容,判断条件是cfd->OldestLogToKeep() <= oldest_alive_log,OldestLogToKeep记录的是包含该column family的wal的最小序号。

然后对挑选出来的column falimy执行SwitchMemtable,使得新数据写入到新的Memtable中,以便旧的Memtable可以进行flush。
SwitchMemtable:
1.创建一个新的wal
2.将当前的memtable放入到immutable memtable链表中,再创建一个新的memtable
3.调用InstallSuperVersionAndScheduleWork,更新SuperVersion。并且调用MaybeScheduleFlushOrCompaction检查是否要进行flush或者compaction,这里应该是需要进行flush的,但flush的执行不是再这里,而是由专门的flush线程进行。也可不是这里,后面会再生成一个flush请求。

最后,创建了一个flush请求,等待调度执行。

HandleWriteBufferFull

首先选择哪些column family需要进行处理,判断条件是!cfd->mem()->IsEmpty(),也就是memtable中有数据

然后对挑选出来的column falimy执行SwitchMemtable

最后,创建了一个flush请求,等待调度执行。

与SwitchWal的执行流程比较相似。

DelayWrite和log sync先不介绍,不是主干逻辑。

EnterAsBatchGroupLeader

在这里插入图片描述

图片来源:https://blog.csdn.net/qq_43479736/article/details/109056437

每个write_batch会用一个writer类进行封装,writer类会组成一个链表,链表的最新元素记录在指针WriteThread::newest_writer_。
这个链表是可以随时插入新节点的,在写入过程中也可以插入,leader会选择尽量多的Writer组成一个Write Group,批量写入到Wal。

EnterAsBatchGroupLeader的工作,就是选择可以组合到一起的batch,构建group供后续处理。

WriteToWAL

下一步的主要流程是调用WriteToWAL将group写入到wal中。

首先调用MergeBatch将group中的所有batch合为一个,合并的方法其实就是把所有batch的rep_合并成一个字符串。

然后调用log_writer->AddRecord,将合并的batch中的内容加入到log_writer的dest_成员中,此时并没有写入到硬盘,还是在内存中。然后AddRecord函数中,会判断manual_flush_,如果没有设置manual_flush_,那么就需要写入硬盘,调用dest_->Flush()写入硬盘。

memtable

写完wal之后,再把数据写入到内存的memtable中。

我们主要看可以并发写入的memtable的写入流程,使用的数据结构是skiplist。

首先,调用LaunchParallelMemTableWriters,将write_group中的所有writer设为状态STATE_PARALLEL_MEMTABLE_WRITER,这样follower可以并发写入。

然后是写入leader自己的memtable,memtable内容较多,具体内容先不介绍了,后续可以单独讲一篇。

六、follower

follower在JoinBatchGroup,没有被分配到leader角色,需要等待leader完成一些任务后(一般情况就是写完wal)再进行后续的处理,等待是调用AwaitState函数进行的:

    AwaitState(w, STATE_GROUP_LEADER | STATE_MEMTABLE_WRITER_LEADER |
                      STATE_PARALLEL_MEMTABLE_WRITER | STATE_COMPLETED,
               &jbg_ctx);

一般来说,使用的memtable是可以并行写入的,在leader的介绍中,说明了调用LaunchParallelMemTableWriters时,会给follower设置STATE_PARALLEL_MEMTABLE_WRITER状态,此时follower就会被唤醒了,然后进入写memtable的流程,这个流程与leader的流程是类似的。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值