文章目录
本文在rocksdb 整个读写链路基础上给出一些简单的调优策略,主要是通过调整一些 参数来满足我们大多数workload的性能需求。
更详细的源代码分析暂不涉及,一部分调优机制的源代码分析之前的部分文章中已经有过描述,需要的话会在文中提及。6.4及以上版本的实现中有超过200个参数,对于大多数使用rocksdb的同学来说实在是负担太重,想要通过分析源代码来明确每一个参数的深层含义以及在不同workload下的其表现最优的值显然是不可能的,所以只需要明确核心链路的可优化点即可。
通过本文,能够清楚 读写性能调优,compaction的核心调优,通用的优化配置。
写性能优化
我们知道rocksdb是基于LSM架构的单机存储引擎,其拥有高效的写吞吐。为了保证一致性,整个写入链路会先写WAL,再写memtale即可返回。其中WAL如果开了sync,则需要走一次I/O ,memtable是一个内存数据结构,通常是跳表实现的,基本属于CPU计算。后续数据在memtable的罗盘则通过异步的flush来进行。
wal的写入无法避免,写入参数上的优化就主要集中在写memtable上了。
rocksdb提供了write buffer size的配置,即单个memtable能够接受的最大写入量,并且提供了可以设置每一个DB/ColumnFamily 的写缓存配置。
CF write buffer size
cf_options.write_buffer_size = 64 << 20;
默认是64M。
可以用来设置以Column Family为单位的缓存大小。设置之前最好预估一下最坏情况下的内存使用量,如果发现内存不够用,那么需要降低这个数值。
DB write buffer size
db_options.write_buffer_size = 64 << 30;
默认值是0。
这个配置用来设置整个db 所有column family 共享的write buffer size, 表示整个db的write buffer size达到阈值,会对当前db的所有cf进行flush。
如果需要改变这个值,可以通过如上参数的方式将该值设置为64G。
读性能优化
block cache
block cache 主要是用来缓存解压后的数据。blockcache的大小,社区给的建议是 整个内存有效负载大小的1/3。即如果内存有效负载是240G(240G的used的情况下操作系统仍能正常工作,不需要回收低优先级的进程内存),那么blockcache的大小就设置为80G。
block cache的配置可以通过table_options来进行设置,需要注意的是为了保证所有的column family共用一个blockcache,需要让table_options对象所有的cf_options的table_factory公用。
auto cache = NewLRUCache(128 << 20); // LRUcache
BlockBasedTableOptions table_options;
table_options.block_cache = cache;
auto table_factory = new BlockBasedTableFactory(table_options);
cf_options.table_factory.reset(table_factory);
bloom filter
如果你的系统中有查找相关的操作,建议打开bloom filter这个配置。bloom filter主要是用来过滤sst文件中不存在的key的查找请求,可以在O(1)时间内完成这个操作,而不需要额外的CPU计算和昂贵的I/O操作。
需要注意的是bloom filter能够有效提升点查性能,却无法提升range scan的性能。
通过如下配置来打开bloom filter,并将bloom filter位数设置为10位。
rocksdb::BlockBasedTableOptions table_options;
table_options.filter_policy.reset(rocksdb::NewBloomFilterPolicy(10, false));
auto table_factory = new rocksdb::BlockBasedTableFactory(table_options);
cf_options.table_factory.reset(table_factory);
Compression 压缩
压缩的主要目的是为了节省空间但却有读性能 以及 系统CPU和 磁盘I/O的额外消耗,因为需要将读到的datablock 进行解压,这个过程会有CPU的计算和I/O代价,所以这个配置选项是一个权衡。
rocksdb提供了不同压缩算法的选择:
cf_options.compression
这个配置控制的是前n-1层的压缩算法,建议使用lz4(kLZ4Compression
)算法,如果不可用的话再选择snappy(kSnappyCompression
)cf_options.bottommost_compression
控制最后一层的压缩算法,建议使用ZStandard(kZSTD
),如果不可用的话可以选择Zlib(kZlibCompression
)
为什么要有这样的针对不同层的不同压缩算法的配置?
因为n-1层 的sst文件还是有比较高的概率被读到,所以空间的压缩比不需要那么高,为了降低压缩对系统资源和性能的消耗,编解码的效率则会优先考虑。但文件落到了第n层, 这个文件被读到的概率就比较低,可以设置相对较高的压缩比。
Compaction优化
关于Compaction 的原理及其所带来的问题,之前的多篇文章也有详细描述。
Rocksdb Compaction 源码详解(一):SST文件详细格式源码解析
Rocksdb Compaction源码详解(二):Compaction 完整实现过程 概览
Rocksdb 的 rate_limiter实现 – compaction限速
LSM 优化系列(三)SILK- Preventing Latency Spikes in Log-Structured Merge Key-Value Stores ATC‘19
这里也推荐大家使用rocksdb的Rate limiter进行限速,限速能够达到从I/O层面限制compaction的过程对系统资源CPU和IO的竞争的目的,从而保证客户端的请求无论是qps还是延时(当然qps和请求延时是有相关性的)能够较为平稳。
大家在使用rocksdb的过程中如果发现客户端的请求受到compaction的I/O 竞争,可以选择RateLimiter的配置接入。
通过如下配置进行设置:
db_options.rate_limiter.reset(
rocksdb::NewGenericRateLimiter(
rate_bytes_per_sec /* int64_t */,
refill_period_us /* int64_t */,
fairness /* int32_t */));
核心参数如下三个:
rate_bytes_per_sec
这个是最常用也是大家使用起来最有效的一个参数,用来控制compaction或者flush过程中每秒写入的量。比如,设置了200M, 表示当compaction 累积的总写入token达到 200M /s 时才会触发系统调用的write.refill_period_us
用来控制 token 更新的频率;比如设置的rate_bytes_per_sec
是10M/s, 且refill_period_us
设置的是100ms,那么表示 每100ms即可重新调用一次compaction的写入。针对1M的大value 可以立即写入,而小于1M的数据则需要消耗CPU, 累积到1M 触发一次写入。fairness
表示低优先级请求获得处理的概率。
RateLimiter 支持接受高优先级线程 和 低优先级线程的请求,一半flush操作是最高的优先级,其次是 L0 --> L1 compaction优先级较高,最后则是Higher Level compactions 优先级最低。那么这个参数fairness
表示 即使现在有较多的高优先级任务在调度,低优先级的任务也有 1 / fairness 的机会能够被调度,从而防止被饿死。
更加详细的原理实现可以参考:
Rocksdb 的 rate_limiter实现 – compaction限速
通过测试接入RateLimiter(限速128M),在100B 的9:1读写比场景,无压缩,单db 500G, blockcache 10G的配置下
L5的P9999 读延时由原来的120ms量级
降到了2-3ms的量级
具体的RateLimiter配置需要在实际场景中进行测试,如果写入量比较大,带来的compaction的量也会很大,相应的限速带宽应该更高。
通用workload的配置
下面的这一些配置也是社区提供的他们认为比较通用的配置,大家在检查的过程中会发现这一些配置和我们的默认配置还是有较大差异。 社区为了防止新老版本的db 配置差异导致的一些问题,所以基本没有怎么修改过默认rocksdb的配置项,一直沿用最初的默认配置。
如果用户需要重新加载一个 新的db,那么建议使用这里的配置,能够有较好的性能提升,并不建议直接在旧的db上应用。
基本的配置选项如下:
// 动态调整L0--Ln的大小,保证L0-->L1能够及时刷新,不会因为容量不足而阻塞上层的flush。能够比较好得维护
// LSM tree的树状结构
cf_options.level_compaction_dynamic_level_bytes = true;
// 最大compaction的线程数
options.max_background_compactions = 4;
options.max_background_flushes = 2;
// flush/compaction 每次sync的数据量,即data block累计达到1M 触发一次sync
options.bytes_per_sync = 1048576;
// compaction 过程中选择文件的优先级,能够降低写放大
// 配置了这个参数,会优先选择和下一层sst文件覆盖度较低的文件进行compaction,减少compaction过程中被反复读出写入
options.compaction_pri = kMinOverlappingRatio;
// 设置block_size,这里的block是指sst文中的一个个数据block(data,index,filter,meta,range del)
table_options.block_size = 16 * 1024;
table_options.cache_index_and_filter_blocks = true;
table_options.pin_l0_filter_and_index_blocks_in_cache = true;
以上通用的配置就社区的推荐 以及 实际测试过程中发现 并不会有成倍的性能提升, 在不同的通用workload下拥有较好的性能。
所以如果拥有较强的研发实力 以及 对特定的业务场景拥有强需求 ,那么对rocksdb进行定制化改造是一个更加合适的选择。就像阿里的x-engine,针对电商业务的洪峰,泄洪,洋流三个问题 进行了LSM 重写,这样的优化在特定的业务场景所产生的收益肯定比通用的rocksdb的收益来的更加彻底。