存储引擎常见batchwrite写优化

leveldb groupcommit思想

leveldb是一个KV引擎,支持上层多客户端线程并发写该引擎。所有客户端的写入会链接成一个链表,只需要head节点作为主writer负责写入,如图示例:
存储引擎常见batchwrite写优化

写由主writer负责,这个batch其他writer同步等待,等待被唤醒即可。主writer行为如下:

  • 合并所有writer的writeRequest,生成一个大batch
  • 写WAL
  • 写memtable
  • 都操作完,唤醒大batch中的其他writer回客户端ack,唤醒新进入的writer,作为主writer继续执行。

优点

  • 小的IO都会合并成大的IO落盘(合并batch),提升磁盘吞吐,写WAL,写memtable单线程执行,不需要锁。

缺点

  • 多线程变单线程工作,无法发挥多核优势。
    所以Rocksdb在这个方面做了很多优化工作,其中有一个特性就是pipelined write。

rocksdb pipelined write

存储引擎常见batchwrite写优化

思想很朴素,拆分写WAL,写Memtable等子任务项。客户端线程接力完成这些子任务项。每个子任务有个状态位,客户端线程通过状态位CAS操作抢任务项执行权力

拆分的task一般也就小几个,也就能多用几个线程,大量的客户端线程仍旧同步等待。现代cpu的多核架构仍不能有效发挥出来。

并行写+barrier思想,cassandra示例

客户端并行的写入,每个线程都写透,大家角色对等,这样也不存在rocksdb那样诡异的拆子任务逻辑,同时也能自适应硬件,充分发挥多核优势。但也存在如下问题:

  • 每个线程仅对自己的kv追加写WAL,单条kv都是小IO,浪费iops,这一点可以通过MMAP优化。
  • 多线程锁竞争激烈,其实仅在写wal及写memtable存在临界区,仅对这两块逻辑加锁即可。或者优化成lock-free编程。

客户端线程要源源不断写wal,然后写memtable。当memtable写满,flush线程要刷sst。要通知客户端切换写新memtable怎么实现,用互斥锁肯定可以实现。更朴素的思想是用barrier,核心思想是设置栅栏,栅栏前面的,栅栏后面的分别处理。参考下图
存储引擎常见batchwrite写优化

  • 有一个全局的writeorder,客户端线程写storage之前,调用writeorder.start方法,该方法增加当前group计数引用,将当前写attach到当前group上,上图就是group1. 然后继续写WAL,写memtable。可以看得出来整个链路耗时很长。该写线程完成后或异常时,当前group的引用计数-1。
  • 后台线程周期性尝试flush memtable,先创建新memtable,加入到链表头,标记为最新memtable。
  • 把old-memtable的barrier设置上,该barrier前一个group是group1.通过第一点我们知道客户端线程写完还是比较慢的,flush线程会等group1引用计数为0,也就是所有线程写完才会刷sst。
  • flush线程封箱barrier,意思是新的写会挂载在group2上,group1会减引用,如果为0然后unlink掉。
  • 新的写入attach到group2, 旧的写入写完慢慢减group1的引用,变为0之后,写线程都写完了,flush线程可以安全的刷sst,并且回收memtable。

总结

简单的记录了常见开源引擎里面一些令人印象深刻的优化,后续再慢慢追加wiretiger&innodb一些特别点。

源码参考

https://github.com/apache/cassandra/blob/6f213727b678be4ee6ec350bd3ed1869db394ae9/src/java/org/apache/cassandra/utils/concurrent/OpOrder.java

https://github.com/google/leveldb/blob/master/db/db_impl.cc

上一篇:常见的公共报错代码锦集


下一篇:IDEA里面添加lombok插件,简略Java代码的编写