一、前言
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:
- 首先调用WriteThread::LinkOne函数,将请求挂到链表上,这个链表可能会包含多个请求,后续会通过一次io请求一起写入到wal中。挂到链表上的同时,会判断是否是该链表上的第一个请求,如果是第一个请求,那么这个请求就会作为leader,leader负责写wal,其他请求等待wal写完之后再继续。
- 如果不是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.youkuaiyun.com/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的流程是类似的。