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 |
|
场景分析
我们的需求是向多个实例发送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的程度,未来碰到了再探索进一步的优化方向