RocksDB 读优化

RocksDB MultiGet 优化

去年的工作内容中涉及到RocksDB的读写优化,场景分为:大批量写;批量只读;读写TTL

本文回顾批量只读场景下的优化思路,以及记录未来的计划

RocksDB 简单介绍

RocksDB 是一个基于LevelDB优化的持久化KV数据库,其存储结构是LSM-Tree,主要设计用来应对写入密集型工作负载

第一个问题,技术选型

技术选型阶段,我们遇到一个问题,天生为写优化的RocksDB在只读场景是否有必要采用呢?

表面看起来,似乎没有了写入的需求,我们已经不需要LSM-Tree了?我们只需要一个索引查找到磁盘指针,读取数据就行了?

是的,没错,情况的确是如此

但是如果我们限制LSM-Tree的最大高度为1呢?LSM-Tree在这里被打平了,和我们的目标是一致的!

第二个问题,可观测性

很明显,RocksDB的读请求处理,将会是整个请求中的高频热点

除了操作系统级别的系统资源监控和perf,还有什么样的措施可以让我们对RocksDB的内部状态有足够透明的观测?

基于观测到的数据,我们将来可以调整配置进一步优化性能,或者调整系统资源避免瓶颈

根据RocksDB Wiki的介绍,他们提供了三个观测措施:stats(数据统计指标),perf context(单请求逻辑指标),IO context(单请求IO指标)

我们简单的用了stats,其他观测措施将来有需要再使用

还有一个工具,rocksdb advisor据说可以根据log推荐配置,这个以后在读写场景下介绍

准备工作,理解 MultiGet

虽然我们可以观测stats,但很明显,在理解了其内部原理以后,会对stats有更准确的理解

优化思路,也只有在了解原理的基础上才能找到方向

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

- DBImpl::PrepareMultiGetKeys -> sort

- DBImpl::MultiGet -> batch(32)

- (mutable)Memtable::MultiGet -> Rep_(SkipListRep)::Get (只读场景忽略)

- (immutable)MemtableList::MultiGet -> Memtable::MultiGet -> Rep_(SkipListRep)::Get (只读场景忽略)

- Version::Mutliget

-- prepare get_ctx (key, cmp, status) (以及其他在SST文件搜索过程中的状态,比如index之类)

-- FilePickerMultiGet::prepareNextLevel (根据key与当前文件最小/最大key的对比关系,确定下一层的搜索SST文件的边界,加快查找SST)

-- FilePickerMultiGet::GetNextFileInLevel(在上一层确定的这一层搜索边界内确定可能包含key的SST)

-- Version::MultiGetFromSST(从单个SST读取key)-> TableCache::MultiGet

-- TableCache::GetFromRowCache

-- TableCache::FindTable -> cache::Lookup or TableCache::GetTableReader

-- BlockBasedTable::MultiGet

-- BlockBasedTable::FullFilterKeysMayMatch(两种bloomfilter:Full or Block)

-- BlockBasedTable::NewIndexIterator(根据rep->footer.index_handle加载index block,也可能cache)

-- for keys

-> indexiter::Seek

-> block_cache::StartAsyncLookupFull

-- block_cache::WaitAll

-- BlockBasedTable::RetrieveMultipleBlocks if cache miss

-> BlockBasedTable::RetrieveBlock

-- for keys

-> BlockBasedTable::NewDataBlockIterator

-> DataBlockIter::SeekForGet

-> GetContext::SaveValue

场景分析

我们的需求是向多个实例发送3万个Key并返回Value

单节点观测到的数据:

  • 从系统的角度看,IOPS比较高,应该是因为大量SST文件被读取;由于3万个Key范围太广,每个SST几乎都被波及

  • IO latency数据忘了,但是由于SSD的特性,空间占用太高会影响读写

  • 从stats观察到的现象:

  • keys.read很高,但是keys.found很低,大量key在其他实例上属于无效查询

  • blockcache 命中率一般,这也和IOPS高相关

结合观测数据和实现原理,我们发现优化空间还是非常大的:

机制

  • 我们注意到,MultiGet内部其实是把32个key作为一批处理,顺序处理多批数据;为了充分利用SSD的IO并发与带宽,我们可以多线程读取

  • 在row cache生效前,仍然需要加载SST meta信息以确定是否要进一步查询SST,这些信息可以缓存,所以还好

  • 在SST内部搜索时,row.cache 类似于Mysql中行缓存,对于解决热点key的情况非常有效

  • 在SST内部搜索时,可以选择bloomfilter在SST级别生效还是Block级别生效,当然越早生效越好

  • 对于只读场景,其实可以考虑数据库级别过滤器,或者把key路由到相关节点,避免无效查找

  • 加载索引容易被IO抖动影响,因此建议放在block cache里面

  • 选择哈希索引还是二分索引?哈希索引效率高但是内存局部性不够好,可能存在CPU Stall;这个保持默认

  • 是否选择二级索引?索引二级索引对随机读是比较好的,对scan场景就反而导致多次IO

  • RocksDB支持Pin机制,避免多余的数据拷贝,只要记得释放Slice就行

  • 数据压缩,节省了一半的空间

优化效果

  • 数据压缩,对读取效果影响不大

  • 数据路由大幅过滤了无效IO,SST级别的bloomfilter过滤效果不大(因为在路由时已经过滤了),大幅解放了SSD的IOPS,为接下来并行IO打下基础

  • row.cache在1/3左右,性价比比较低,聊胜于无吧

  • filter/index缓存这个没有实施,因为性能已经满足需要

  • 二级索引同上

  • Pin机制必须的

  • 以32为一批并行调用MultiGet,避免了无效等待,最终DB总耗时从数百毫秒降低到毫秒级别

未来优化方向

  • 异步IO,更进一步提升IO利用率

  • 并行IO线程池优化,减少排队阻塞

  • 事实上读取Block的时候涉及到PrefetchBuffer,可以调整预读buffer大小,避免不必要的IO

  • 尝试哈希索引,以及启用大内存页避免地址转换器缓存失效

  • 衡量SST筛选的开销,考虑是否魔改加快

结语

为了追求极致的性能,RocksDB里面隐藏了非常多的性能细节,也提供了非常灵活的配置,使用者可以根据负载调整配置即可

目前还没有到需要魔改RocksDB的程度,未来碰到了再探索进一步的优化方向