【Rocksdb实现分析及优化】enable_pipelined_write
rocksdb v5.5.1的配置项中多了一个enable_pipelined_write,RELEASE中描述如下:
New option enable_pipelined_write which may improve write throughput in case writing from multiple threads and WAL enabled.
就是靠pipeline来进一步提高写性能。
1. 哪里可以pipeline
rocksdb的多线程写入流程之前的博客详细说过,大致如下:
- 多线程的Write操作会首先入队列WriteThread::newest_writer_,入队之后如果发现自己是leader(队头),则继续如下操作,如果不是leader是follower,则阻在WriteThread::AwaitState上
- leader将当前队列中所有的writer打入一个batch,然后写WAL和Memtable
- leader更新batch中的所有writer的state,然后选择当前队列中除batch外第一个writer当新的leader,唤醒新leader和batch中的followers
- 阻塞在WriteThread::AwaitState上的followers都被唤醒,发现自己的state已经被更新,直接返回
上面的步骤中,哪里可以优化呢?
如果将写WAL和Memetable分开,leader只负责写WAL,写完之后就唤醒新的leader,然后把当前batch插入到一个新的队列中,这个队列专门负责写Memtable,写完以后唤醒batch中的follower和该队列中的下一个leader。
这样就形成了一个pipeline,进一步提高性能。
2. 实现
实现也不难,除之前WriteThread::newest_writer_队列外,新增了一个WriteThread::newest_memtable_writer_队列,负责上面说的排队写Memtable。
下面简单记录下代码,方便回顾:
Status DBImpl::WriteImpl(const WriteOptions& write_options,
WriteBatch* my_batch, WriteCallback* callback,
uint64_t* log_used, uint64_t log_ref,
bool disable_memtable, uint64_t* seq_used) {
......
if (immutable_db_options_.enable_pipelined_write) {
return PipelinedWriteImpl(write_options, my_batch, callback, log_used,
log_ref, disable_memtable, seq_used);
}
}
如果打开了enable_pipelined_write,则调用PipelinedWriteImpl
Status DBImpl::PipelinedWriteImpl(const WriteOptions& write_options,
WriteBatch* my_batch, WriteCallback* callback,
uint64_t* log_used, uint64_t log_ref,
bool disable_memtable, uint64_t* seq_used) {
......
// 1. 入队列newest_writer(不是leader则阻塞等待被唤醒)
write_thread_.JoinBatchGroup(&w);
if (w.state == WriteThread::STATE_GROUP_LEADER) {
......
// 2. 如果自己是newest_writer_的leader,打包队列中的
// Writer到wal_write_group
last_batch_group_size_ =
write_thread_.EnterAsBatchGroupLeader(&w, &wal_write_group);
// 3. 写WAL
......
// 4. 将wal_write_group连到newest_memtable_writer_队列中,
// 然后选择newest_writer_队列中新的leader并唤醒它
// (不是newest_memtable_writer_的leader则阻塞等待被唤醒)
write_thread_.ExitAsBatchGroupLeader(wal_write_group, w.status);
}
if (w.state == WriteThread::STATE_MEMTABLE_WRITER_LEADER) {
......
// 5. 如果自己是newest_memtable_writer_的leader,打包队列中的
// Writer到memtable_write_group
write_thread_.EnterAsMemTableWriter(&w, &memtable_write_group);
// 6. 写Memtable
......
// 7. 选择newest_memtable_writer_队列中的新leader并唤醒它,
// 然后更新memtable_write_group中所有writer的state并
// 唤醒所有followers
write_thread_.ExitAsMemTableWriter(&w, memtable_write_group);
}
}
这就是pipeline的关键逻辑实现。两个队列的followers分别会阻塞在步骤1和4上,分别等待newest_writer_和newest_memtable_writer_的leader来唤醒。
还有比较关键的两个地方就是ExitAsBatchGroupLeader和ExitAsMemtableLeader,涉及新leader的选择,两个队列的拼接,followers的唤醒等等,具体实现就不展开了,看代码很快。
总结
提高性能的一个重要手段就是pipeline,以后自己的项目中对于关键逻辑部分也要细化拆分做到尽可能的优。
BUT: 对于rocksdb的这个pipeline的优化,本人持怀疑态度,毕竟写WAL要比Memtable慢很多,可能每次从newest_writer_向newest_memtable_writer_传batch的时候,后者基本都是空的。这样反而额外耗了CPU,得不偿失呀。这个pipeline的优化我还没有实测,这两天找时间测测,看看具体效果如何。