rocksdb调试指引
https://github.com/facebook/rocksdb/wiki/RocksDB-Tuning-Guide
转载请注明出处:https://www.cnblogs.com/morningli/p/16788424.html
基本调试建议
设置选项和基本调整 ,除非你看到一个明显的问题,否则不需要调试它。一方面在这个页面建议的典型配置甚至开箱即用的配置通常会对正常的负荷是合理的。另一方面当负荷或者硬件改变时,调优rocksdb常常容易出现更大的性能回退。用户通常需要持续调试rocksdb来保持同样等级的性能。
会谈 和 一些 出版物 ),特别是rocksdb的LSM树是怎么实现的和 压实 。
rocksdb统计数据
statistics -- 设置成rocksdb::CreateDBStatistics(,任何时候通过调用options.statistics.ToString(可以得到一个可读的rocksdb统计数据。详情见 statistics
Compaction and DB stats rocksdb一直保存一些compaction的统计数据和基本的db运行状态。这是了解LSM树形状的最简单的方法,可以通过这来评估读写性能。详情见 Compaction and DB stats
Perf Context and IO Stats Context Perf Context and IO Stats Context 可以帮助了解某一指定语句的相关计数。
可能的性能瓶颈
System Metrics
Disk Read IOPs 请注意,可以保证合理读取性能的持续读取 IOPS 通常远低于硬件规格。 我们鼓励用户通过系统工具测量他们想要使用的读IOPS(比如fio),并根据此测量检查系统指标。如果IOPS已经饱和,应该开始检查compaction。也可以提高block cache命中率。有时候引起问题的是读index,filter或者大数据block,有不同的方式来处理他们。
CPU CPU通常是读路径引起的,但是也有可能是compaction引起的。很多选项可能会受到影响,比如compaction,compression,bloom过滤器,block大小等待。
磁盘空间 技术上来说这不是一个瓶颈。但是当系统指标不饱和,性能是足够好的,我们已经差不多填满了SSD的空间,我们说它是一个空间瓶颈。如果用户想通过这个机器服务更多的数据,compaction和compression是需要调优的主要领域。 Space tune 会指导你如何减少磁盘空间的使用。
磁盘写带宽 通常rocksdb compaction会尝试写超过SSD驱动处理能力的数据。症状可能是rocksdb的write stalling(见compaction stats的Stalls
或者statistics的STALL_MICROS
)。或者会导致读请求很慢,因为compaction积压和LSM数结构倾斜。读请求的Perf context有时候可以显示一次读请求打开了太多的SST文件。如果发生了这样的情况应该调试compaction。
放大因素
写放大 是写到存储的字节数与写到数据库的字节数的比例。
高写放大也会减少flash的寿命。有两种方式可以观察到你的写放大。第一种方式是通过DB::GetProperty("rocksdb.stats", &stats
的输出来读取。第二种是用你的磁盘写带宽去除以数据库写速率。
读放大 是每个请求的读磁盘数。如果你需要读取5个页去响应一个请求,读放大是5。逻辑读指从缓存获取数据,包括rocksdb block cache和操作系统 cache。物理读由flash或者磁盘这些存储设备来处理。逻辑读比物理读成本低很多,但是还是会影响CPU消耗。你可以从iostat
的输出来评估物理读的速率,但是这会包含请求和compaction的读请求。
空间放大 是数据库文件在磁盘的大小跟数据大小的比例。如果你放10MB数据到数据库中,它在磁盘占用了100MB,那么空间放大是10。你通常想要设置一个硬限制在空间放大,这样你不会用完你的磁盘空间或者内存。Space tune 会指导你如何减少磁盘空间的使用。
Mark Callaghan's talk at Highload。
系统指标没有饱和时的慢
写不够快 虽然写入通常受到写入 I/O 的瓶颈,但在某些情况下,I/O 不够快,rocksdb 无法以足够快的速度写入 WAL 和 memtable。用户可以尝试无序写入、手动 WAL 刷新和/或将相同的数据分片到多个 DB 并并行写入它们。
想要更低的读延迟 又是什么问题也没有用户只是想要更低的读延迟。可以从通过 Perf Context and IO Stats Context 检查每个语句的状态来找出导致延迟高的CPU或者IO问题并尝试调整响应的选项。
compaction不够快 有时候SSD远没有饱和但是compacrtion还是赶不上。有可能是rocksdb的compaction受限于compaction并行度的配置,没有最大化资源使用。默认的配置通常比较低,有很多空间可以改善。见 Parallelism options。
调整flush和compaction
compaction 了解rocksdb的compaction是如何工作的。
并行度选项
在LSM架构,有两个后台程序:flush和compaction。两者可以通过线程来并发执行,来利用存储技术的并发性。flush线程在高优先级的池子,compaction线程在低优先级的池子。增加每个池子的线程数量可以通过调用:
options.env->SetBackgroundThreads(num_threads, Env::Priority::HIGH;
options.env->SetBackgroundThreads(num_threads, Env::Priority::LOW;
要想从更多线程中获益你需要设置这些选项来修改并发compaction和flush的最大数量。
max_background_compactions 是后台compaction最大并发度。默认是1,但是为了充分利用你的CPU和存储你可能会希望把它增加到系统CPU数和磁盘吞吐除以一个compaction线程平均吞吐的最小值。
compaction 优先级(只适用于leveled compaction)
compaction_pri=kMinOverlappingRatio 是rocksdb的默认值,大多数情况已经是最优的了。我们在UDB和msgdb中都是用他,与以前的默认值相比,写放大下降了一半以上(https://fb.workplace.com/groups/MyRocks.Internal/permalink/1364925913556020/)。
删除时触发compaction
定时和TTL compaction
虽然compaction类型有时候通常优先删除包含更多删除的文件,但并不能保证这一点。另外一些选项可以帮助到。options.ttl
指定一个过期的数据会从sst文件删除的时间界限。options.periodic_compaction_seconds
保证一个文件每隔一段时间通过compaction过滤器,这样compaction 过滤器会删除相应的数据。
flush 选项
write_buffer_size 设置一个memtable的大小。一旦memtyable超出这个大小。它会被标记位不可变的并创建一个新的。
max_write_buffer_number 设置memtable最大的数量,包含活跃的和不可变的。如果活跃的memtable填满了而且memtable的总数大于max_write_buffer_number我们会停止(stall)继续写入。如果flush成语比写的速率慢会发生这样的情况。
min_write_buffer_number_to_merge 是在flush到存储之前要合并的最小memtble数。举个离职,如果这个选项设置为2,不可变memtable只会在有两个时才会flush - 一个不可变memtable绝不会被flush。如果多个memtable被合并到一起,有可能会有更少的数据写到存储,因为两个更新被合并到一个key。然而,每次Get(一定会线性遍历所有的不可变memtable来检查key是否存在。这个选项设置得台高容易影响读性能。
write_buffer_size = 512MB;
max_write_buffer_number = 5;
min_write_buffer_number_to_merge = 2;
写速率是16MB/s。在这个用例,一个新的memtable会每32秒创建一次,两个memtable会被合并到一起然后每64秒flush一次。根据工作集大小,flush大小将在 512MB 和 1GB 之间。为了防止flush跟不上写速率,memtable使用的内存上限是5*512MB = 2.5GB。当达到内存上限,后续的写操作会被阻塞住知道flush结束并释放了memtable使用的内存。
Level Style Compaction
leveled compation
一个compacrtion可能会拿一些N层的文件和N+1层中重叠的文件一起压缩。两次不同层次或者不同key范围的compaction操作是独立的,可以并发执行。compaction速度直接跟最大写速率成正比。如果compaction不能艮山写速度,数据库使用的磁盘空间会持续增加。将rocksdb配置成compaction高并发执行并且充分利用存储是很重要的。
L0->L1 压缩是单线程的。使用单线程compaction很难实现好的吞吐。要检查这是否会导致问题,检查磁盘利用率。如果磁盘没有充分使用,有可能在compaction配置除了问题。我们通常建议通过让0层的大小接近1层大小来让L0->L1尽可能的快。
大小放大很容易计算。(512 MB + 512 MB + 5GB + 51GB + 512GB / (500GB = 1.14。可以这样计算写放大:每个字节首先写到0层。然后压缩到1层。因为1层大小跟0层相同,L0->L1的写放大是2。然而,当一个自己从1层压缩到2层,它会跟2层的10个字节(因为2层是1层的10倍)。L2-L3和L3->L4是一样的。
让我们开始了解level compaction的选项。我们会先介绍重要的选项,然后介绍次重要的。
level0_file_num_compaction_trigger -- 0层的文件达到这个数字会立刻触发L0->L1的压缩。因此我们可以将稳定状态下的0层的大小估计为 write_buffer_size * min_write_buffer_number_to_merge * level0_file_num_compaction_trigger。
max_bytes_for_level_base和max_bytes_for_level_multiplier -- max_bytes_for_level_base是1层总大小。我们建议其大小约为0级的大小。每个连续层是前一个的max_bytes_for_level_multiplier倍大。默认是10,我们不建议去修改它。
target_file_size_base和target_file_size_multiplier -- 1层的文件有target_file_size_base个字节。每层的文件大小比前一层的大target_file_size_multiplier倍。然而,默认的target_file_size_multiplier 是1,所以L1...Lmax的文件大小是一样的。增加target_file_size_base 会减少数据库的文件数,一般是一件好事。我们建议设置target_file_size_base为max_bytes_for_level_base / 10,这样在1层会有10个文件。
compression_per_level -- 使用这个选项来设置每层的压缩算法。通常不会压缩0层和1层的护甲,只会在更高的层次压缩数据。你设置可以在最高层设置更慢的压缩算法,在低层用更快的压缩算法(最高层表示Lmax)。
num_levels -- num_levels大于预期的数据库层次数量是安全的。一些更高层可能是空的,但是不会影响性能。只有你语气你的层次数量会大于7层需要修改这个选项(默认是7)。
universal compaction
universal compaction
universal compaction 了解更多。
然而,有一些技术帮助减少临时的空间防备。如果你使用universal compaction,我们强烈建议给你的数据分片并保存到不同的rocksdb实例中。让我们假设你有S个分片。配置Env线程池,使压缩线程只有N个。S个分片中只有N个分片会有额外的空间放大,这将额外的空间放大从1降为了N/s。举个例子,如果你的数据库是10GB你配置成100个分片,么个分片包含100MB数据。如果你配置你的线程池为20个并发的压缩线程,你会只消耗额外的2GB而不是10GB,compaction会并行执行,充分利用你的存储并发度。
max_size_amplification_percent -- 大小放大,定义为存储一个字节到数据库需要的额外空间(百分比)。默认是200,意味着100字节的数据库会需要300字节的存储。300字节中的200字节是临时的,只会在compaction的时候使用。增加这个限制会减少写放大,但是(显然)会增加空间放大。
compression_size_percent -- 数据库压缩的数据的百分比。更老的数据会被压缩,更新的数据不会被压缩。如果设置为-1(默认值),所有的数据会被压缩。减少compression_size_percent会减少CPU使用率并增加空间放大。
调优其他选项
通用选项
filter_policy -- 如果你正在在一个未压实的数据库进行点查询,你肯定会想要打开布隆过滤器。我们使用布隆过滤器来避免不必要的磁盘读。你应该设置filter_policy为rocksdb::NewBloomFilterPolicy(bits_per_key。默认的bits_per_key是10,这会产生约1%的误报率。更大的bits_per_key会减少误报率,但是会增加内存使用和空间放大。
block_cache -- 我们通常建议将这个配置设置成rocksdb::NewLRUCache(cache_capacity, shard_bits的结果。block cache 缓存解压后的block。另一方面操作系统缓存压缩后的block(因为这是存储在文件的方式)。所以有理由同时使用block_cache和操作系统缓存。我们需要在进入block cache的时候加锁,所以有时候我们会看到rocksdb的瓶颈在于block cache的mutex,特别是数据库大小比内存小。这种情况下,有理由通过设置shard_bits成一个更大的数字来对block cache分片。如果shard_bits是4,分片的总数是16。
allow_os_buffer -- [已弃用] 如果设置为false,我们不会在系统缓存缓存文件。
table_cache_numshardbits -- 这是用来控制table cache分配的选项。如果table cache的mutex有影响可以增加这个选项。
共享缓存和线程池
又是你可能会希望在相同的进程里运行多个rocksdb的实例。rocksdb提供这些实例共享block cache和线程池的方法。要共享block cache,将一个cache对象赋值给所有的实例:
first_instance_options.block_cache = second_instance_options.block_cache = rocksdb::NewLRUCache(1GB
这样两个实例都共享了一个总大小为1GB的block cache。
options.env默认会设置为Env::Default(
,大多数情况是最好的。因为所有的选项使用相同的静态对象Env::Default(
,线程池默认就是共享的。通过这种方式,你可以设置compaction和flush的运行数量上限,及时是运行多个rocksdb实例的时候。
Write stalls
Write stalls
前缀数据库
这些应用会从设置数据库的prefix_extractor
获利。
prefix_extractor -- 定义key前缀的SliceTransform 对象。key前缀可以用来实现一些有趣的优化:
定义前缀布隆过滤器,可以减少前缀范围查询的读放大(例如,拉取前缀为xxx的全部key)。请一定要设置
Options::filter_policy使用hash-map-based memtable来避免在memtable中二分查找的消耗。
添加hash索引到table文件来避免在table文件中的二分查找。
Custom memtable and table factories 。请注意1在减少IO上通常已经足够了。2和3可以在某些场景下减少CPU消耗,但是常常会消耗一定的内存。你应该只有在CPU是你的瓶颈,并且你已经使用了所有更容易的方式去减少CPU消耗的时候才尝试去调整它,这并不常见。确保检查在include/rocksdb/options.h中关于prefix_extractor的注释。
布隆过滤器
filter_policy选项来控制。当一个用户小于Get(key的时候,会有一系列文件可能包含这个key。这通常是所有在0层的文件和其他层每层一个文件。然而在我们读每个文件之前,我们先咨询布隆过滤器。布隆过滤器会过滤掉大多数不包含key的文件的读取。在大多数情况下,Get(只需要读一个文件。布隆过滤器始终保存在内存中以供打开的文件使用,除非BlockBasedTableOptions::cache_index_and_filter_blocks
设置成了true。大家文件的数量由max_open_files
选项来控制。
block-based 过滤器(已弃用)
可以通过调用options.filter_policy.reset(rocksdb::NewBloomFilterPolicy(10, true
设置基于block的过滤器。block-based布隆过滤器为每个block单独构建。一个读请求我们首先咨询索引,索引会返回我们查找的block。现在我们拿到了block,我们再去咨询这个block的布隆过滤器。
全过滤
options.filter_policy.reset(rocksdb::NewBloomFilterPolicy(10, false设置全过滤。全过滤是每个文件构建一个。每个文件只有一个布隆过滤器。这表示我们可以绕开索引先去咨询布隆过滤器。与block-based布隆过滤器相比,在key不在布隆过滤器的情况,我们节省了一次查询索引的时间。
Partitioned Filters 。
自定义memtable和table格式
**memtable_factory ** -- 定义memtable。这里有我们支持的memtable的列表:
SkipList -- 默认memtable
HashSkipList -- 只有 prefix_extractor 才有意义。它会根据key的前缀保存key到不同的桶(bucket)中。每个桶是一个跳表。
HashLinkedList 只有 prefix_extractor 才有意义。它会根据key的前缀保存key到不同的桶(bucket)中。每个桶是一个链表。
Block based -- 这是默认的table。这适用于吧数据存储到磁盘和flash存储。它使用block大小的块来寻址和加载(见block_size 选项)。所以叫Block based。
Plain Table -- 只有 prefix_extractor 才有意义。适合存数据到内存(tmpfs文件系统)。它是字节可寻址。
内存使用
学习更多可以看https://github.com/facebook/rocksdb/wiki/Memory-usage-in-RocksDB
机械硬盘的区别
Tuning RocksDB on Spinning Disks
配置示例
在flash存储的前缀数据库
这个服务使用rocksdb来实现前缀范围查询和点查询,它是运行在flash存储的。
options.prefix_extractor.reset(new CustomPrefixExtractor(;
因为这个服务不需要完整的优先迭代(见 Prefix databases ),我们定义前缀提取器(extractor)。
rocksdb::BlockBasedTableOptions table_options;
table_options.index_type = rocksdb::BlockBasedTableOptions::kHashSearch;
table_options.block_size = 4 * 1024;
options.table_factory.reset(NewBlockBasedTableFactory(table_options;
我们使用table文件中的hash索引来加速前缀查找,但是它会增加存储空间和内存的使用。
options.compression = rocksdb::kLZ4Compression;
LZ4压缩减少CPU使用,但是会增加存储空间。
options.max_open_files = -1;
这个设置禁用在table cache中查找文件,这会加速所有的请求。如果你的server的打开文件数上限比较大,这样设置是好事。
options.options.compaction_style = kCompactionStyleLevel;
options.level0_file_num_compaction_trigger = 10;
options.level0_slowdown_writes_trigger = 20;
options.level0_stop_writes_trigger = 40;
options.write_buffer_size = 64 * 1024 * 1024;
options.target_file_size_base = 64 * 1024 * 1024;
options.max_bytes_for_level_base = 512 * 1024 * 1024;
我们使用level compaction。memtable大小是64MB,会定时flush导0层。compaction L0->L1在有10个0层的文件时触发(一共640MB)。当L0是640MB,会触发压缩到最大大小是512MB的L1。总数据库大小???
options.max_background_compactions = 1
options.max_background_flushes = 1
任何时候只有一个并发的compactrion和一个flush在执行。然而,在系统中有多个分片,所以多个compaction会出现在不同的分配。否则,只有两个线程写到存储不会让存储得到充分使用。
options.memtable_prefix_bloom_bits = 1024 * 1024 * 8;
使用memtable布隆过滤器可以避免一些对memtable的访问。
options.block_cache = rocksdb::NewLRUCache(512 * 1024 * 1024, 8;
block cache 配置成了512MB。(是所有分配共享的吗?)
全排序数据库,flash存储
options.env->SetBackgroundThreads(4;
我们先设置线程池的线程数为4。
options.options.compaction_style = kCompactionStyleLevel;
options.write_buffer_size = 67108864; // 64MB
options.max_write_buffer_number = 3;
options.target_file_size_base = 67108864; // 64MB
options.max_background_compactions = 4;
options.level0_file_num_compaction_trigger = 8;
options.level0_slowdown_writes_trigger = 17;
options.level0_stop_writes_trigger = 24;
options.num_levels = 4;
options.max_bytes_for_level_base = 536870912; // 512MB
options.max_bytes_for_level_multiplier = 8;
我们使用高并发度的level compaction。memtable大小是64MB,0层文件数量是8。这便是compaction在L0的大小增长到512MB的时候被触发。L1的大小是512MB,每层比上一层大8倍。L2是4GB,L3是32GB。
机械硬盘的数据库
功能齐全的内存数据库
使用mmap读:
options.allow_mmap_reads = true;
禁用block cache,启用布隆过滤器并减少增量编码重启间隔:
BlockBasedTableOptions table_options;
table_options.filter_policy.reset(NewBloomFilterPolicy(10, true;
table_options.no_block_cache = true;
table_options.block_restart_interval = 4;
options.table_factory.reset(NewBlockBasedTableFactory(table_options;
如果你想要速度优先,你可以禁用压缩:
options.compression = rocksdb::CompressionType::kNoCompression;
或者,启用一个 轻量级的压缩,LZ4或者snappy。
options.level0_file_num_compaction_trigger = 1;
options.max_background_flushes = 8;
options.max_background_compactions = 8;
options.max_subcompactions = 4;
保持所有的文件打开:
options.max_open_files = -1;
当读数据的时候,考虑设置ReadOptions.verify_checksums = false。
内存前缀数据库
因为数据库是在内存的,我们不关心写放大。我们更关心读放大和空间放大。这是个有趣的离职,因为我们将compaction调到一个极端,系统中通常只会有一个sst table。我们隐藏减少读和空间放大,写放大非常大。
在这个例子,前缀hash可以用来运行系统使用hash索引代替二进制索引,已经在可能的情况下使用布隆过滤器来迭代:
options.prefix_extractor.reset(new CustomPrefixExtractor(;
使用为低延迟访问构建的内存寻址table格式,这需要打开mmap读取模式:
options.table_factory = std::shared_ptr<rocksdb::TableFactory>(rocksdb::NewPlainTableFactory(0, 8, 0.85;
options.allow_mmap_reads = true;
options.allow_mmap_writes = false;
使用hash链表memtable来将内存table查找从二分查找改为hash查找:
options.memtable_factory.reset(rocksdb::NewHashLinkListRepFactory(200000;
为hash table启用布隆过滤器来减少key不存在在内存table时的内存访问(通常意味着 CPU 缓存未命中):
options.memtable_prefix_bloom_bits = 10000000;
options.memtable_prefix_bloom_probes = 6;
设置compaction,只要有两个文件就开始全量压缩(full compaction):
options.compaction_style = kUniversalCompaction;
options.compaction_options_universal.size_ratio = 10;
options.compaction_options_universal.min_merge_width = 2;
options.compaction_options_universal.max_size_amplification_percent = 1;
options.level0_file_num_compaction_trigger = 1;
options.level0_slowdown_writes_trigger = 8;
options.level0_stop_writes_trigger = 16;
设置布隆过滤器最小化内存访问:
options.bloom_locality = 1;
所有表的redser对象一直在缓存中,避免在读的时候访问table cache:
options.max_open_files = -1;
一次使用一个内存table。它的大小取决于我们能接受的全量压缩的间隔。我们调整compaction成每次flush都会触发一次全量compaction,这会消耗cpu。memtable大小越大,compaction的间隔越长,同时我们看到内存效率越低,查询性能越差,重启DB时间恢复时间越长。
options.write_buffer_size = 32 << 20;
options.max_write_buffer_number = 2;
options.min_write_buffer_number_to_merge = 1;
多个数据库分片共享限制为2的compaction线程池:
options.max_background_compactions = 1;
options.max_background_flushes = 1;
options.env->SetBackgroundThreads(1, rocksdb::Env::Priority::HIGH;
options.env->SetBackgroundThreads(2, rocksdb::Env::Priority::LOW;
设置WAL日志:
options.bytes_per_sync = 2 << 20;
memory block table的建议
hash_index:在新的版本,hash索引是为block base table启用的。相比于二分查找索引,它会使用5%的额外空间但是会提升随机读50%的速度。
table_options.index_type = rocksdb::BlockBasedTableOptions::kHashSearch;
block_size:默认的,这个值设置为4K。如果压缩是开启的,更小的blcok size会导致更高的随机读速度因为解压的开销减少了。但是block size不能设置得太小导致压缩失效了。建议设置成1k。
verify_checksum:我们存储数据到tmpfs并且更多关系读性能,校验和可以禁用。
最后
RocksDB Developer's Discussion Group 找我们寻求帮助。