Nutanix 是一家做超融合的云计算厂商,实话,我之前对这家公司是一无所知,但在 2018 年 RocksDB meetup 上面,他们做了一个如何在 RocksDB 支持 coroutine read 以及 async write 的 talk 之后,我突然对这家厂商有了兴趣。佩服他们对 RocksDB 有非常深的研究,顺带在 Scholar 上面查了查,然后又发现了 TRIAD: Creating synergies between memory, disk and log in log structured key-value stores 这篇 Paper,觉得有必要整理下他们公司对 RocksDB 的研究了。
需要注意,下面的东西只是根据 Nutanix 公开的 talk 和 paper 做的一些调研以及猜想,具体他们怎么做的,我其实是不清楚的。
Filter + Async I/O
对于 RocksDB 来说,它的读写 I/O 都是同步的,大家都知道,一般同步的东西,代码写起来是挺简单,但性能其实并不是特别的高效。所以 RocksDB 的 team 一直想引入 Async I/O,也有了一些讨论,也有了一些 PR,但无奈改动太大了。
Nutanix 采用了另一种方案来支持 Async I/O,也就是使用 coroutine,而且对 RocksDB core 几乎代码没有改动。
原理也比较简单,因为 RocksDB 提供了比较好的抽象,对于文件的操作,都是使用一个 Env 对外提供的,所以只需要实习一个自己的 Env,就能控制 RocksDB 的文件读写了。
Nutanix 实现了一个自己的应用线程池,类似于 Folly 的 Fibers 库,然后实现了一个 Async I/O 的 thread pool,用来提交和处理 RocksDB 的 I/O 请求,然后这个 AIO pool 再去跟底层真正的 AIO 交互。
因为他们没有透漏更多,我猜想 Nutanix 的流程应该是:
- 操作跑在一个单线程上面,基于 Fibers
- RocksDB 需要读取某个文件的数据
- RocksDB 将请求发给 AIO thread pool
- 挂起当前的 coroutine
- AIO pool 发给底层的 AIO
- 等 I/O 处理结束在重新 resume 挂起的 coroutine 继续处理
其实这个跟通常的 coroutine 方式差不多,Nutanix 在 talk 里面说到对于单个线程,吞吐能提升 8 倍,还是很猛的一个数字了。
Async Write
上面提到的主要是 Nutanix 对于 Async I/O 的优化,在写入上面,他们也做了优化。
对于 LSM 这种数据结构来说,一次 Write,我们会先将数据 append 到 WAL 上面,然后在写入 memtable。RocksDB 支持多线程写,虽然它提供了 lock-free 的 memtable,但在 append WAL 仍然是不可能做到多线程并发的。所以 RocksDB 做了一些优化。一个是会选出一个 leader 线程,收集其他所有线程的写入,做个 batch,批量写入 WAL。另外就是引入了 pipeline 机制,一个线程先写 WAL,然后写 memtable,这时候另外的线程可以写 WAL 了。
虽然有这些优化,但对于 write 来说,仍然可以认为是同步的,Nutanix 这里引入了 async write,其实原理很简单,就是在 write 的时候带上一个 callback,内部启动了一个新的 leader 线程用来收集数据,batch 写入,然后等写入成功之后调用 callback。这里,Nutanix 额外提到使用了 direct I/O 来操作 WAL,这个还是比较有意思的,因为我以前一直以为对于 append 这种 I/O 操作,direct I/O 其实没啥太大的作用,所以也不知道他们是如何实现的。
基于这个优化,Nutanix 说写入提升了 3 到 4 倍,latency 减少了 2 倍,这个已经很猛了。
TRIAD
最后再来聊聊 TRIAD 这篇论文,这里来个小插曲,Facebook 的技术大佬 Mark 也提到了这篇 Paper,他说到之前竟然没看到这篇文章(毕竟是 2017 年发布的),我猜想他其实之前也没怎么关注 Nutanix,然后也是因为 RocksDB meetup 知道了,然后在 Google 出来的。。。
TRIAD 的原理还是非常简单的,对于一些热点频繁更新的数据,在 Memtable flush 到 Level 0 的时候,并不会 flush 到 Level 0,而是重新写回到 memtable,当然为了保证数据安全,会额外将这些数据写入到一个 log 里面。
在 Memtable 里面,每个 key 会有额外的 4 字节空间来统计 key 的频率,然后在 flush 的时候统计出最 hot 的 k 个 key。现在的算法比较简单,只要大于平均频率的 key 就是 hot key,这个算法其实在多数场景下面都是有效的。
对于 Level 0 和 Level 1 compaction,TRIAD 采用了 Hyperloglog 来计算两层之间的重叠情况,如果如果有足够的重叠了,就触发 compaction,否则则是延迟触发。计算重叠的公式为 UniqueKeys(file-1, file-2, ... file-n) / sum( Keys( file-i ) )
,其中 Keys( file-i )
表明是第 I 个 SST 的总的 key 的个数,而 UniqueKeys
则是估算的所有 SST 的唯一 key 的个数。
对于 LSM 来说,一个被刷到 Level 0 的 memtable,通常数据其实也存在 WAL 里面,所以 TRIAD 做了一些改进,在 flush 到 Level 0 的时候,只是将一个 index(CL-SSTable) 刷到了 Level 0,这样通过 index 就能在 WAL 找到对应的数据了。然后在 Level 0 compacted 到 Level 1 的时候,WAL 才会被删除。
关于 TRIAD,大家可以直接去看源码。
总结
上面只是一些我自己的理解,直观的感受就是 Nutanix 这家公司在 RocksDB 上面也做了很多东西,但网上能 Google 出来的东西挺少的。对于我们来说,这些优化如果 RocksDB 能引入那当然最好,如果不能,短期对我们意义不大,毕竟我们现在没太多的人力去开发相关的东西,如果你对这块感兴趣,欢迎联系我 tl@pingcap.com。