【翻译】rocksdb调试指引

时间:2022-12-02 14:04:42

翻译自官方wiki:https://github.com/facebook/rocksdb/wiki/RocksDB-Tuning-Guide
转载请注明出处:https://www.cnblogs.com/morningli/p/16788424.html


基本调试建议

rocksdb是可以灵活地高度地进行配置的。另一方面,rocksdb多年来一直在提高它的自适应性。如果你的应用在SSD上正常运行,我们完全不建议你对rocksdb进行调优。我们建议用户 设置选项和基本调整 ,除非你看到一个明显的问题,否则不需要调试它。一方面在这个页面建议的典型配置甚至开箱即用的配置通常会对正常的负荷是合理的。另一方面当负荷或者硬件改变时,调优rocksdb常常容易出现更大的性能回退。用户通常需要持续调试rocksdb来保持同样等级的性能。

当你需要调优rocksdb,在调优之前我们建议你先去了解基本的rocksdb设计(有很多 会谈 和 一些 出版物 ),特别是rocksdb的LSM树是怎么实现的和 压实

rocksdb统计数据

要了解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

有时,由于系统指标已饱和,性能会受到限制,并且有时会出现意想不到的症状。微调 RocksDB 的用户应该能够从操作系统中查询系统指标,并确定特定系统指标的使用率是否很高。

  • 磁盘写带宽 通常rocksdb compaction会尝试写超过SSD驱动处理能力的数据。症状可能是rocksdb的write stalling(见compaction stats的Stalls或者statistics的STALL_MICROS)。或者会导致读请求很慢,因为compaction积压和LSM数结构倾斜。读请求的Perf context有时候可以显示一次读请求打开了太多的SST文件。如果发生了这样的情况应该调试compaction。

  • 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的术语放大因素(amplification factor):写放大,读放大和空间放大。这些放大因素将用户逻辑请求的大小与rocksdb对底层硬件的请求联系起来。有时候当我们需要调试rocksdb的时候哪个因素需要调整是明显的,但是有时候是不清楚的。不管是哪种情况,compaction都是改变三者之间权衡的关键。

写放大 是写到存储的字节数与写到数据库的字节数的比例。

举个例子,如果你正在写 10MB/s的数据到数据库但是你观察到磁盘读写速率是30MB/s,你的写放大是3。如果写放大很高,这个工作负载可能会受限于磁盘吞吐。举个例子,如果写放大是50,最大的写吞吐是500MB/s,你的数据库可以支持10MB/s的写速率。在这种情况,减少写放大会直接增加写速率。

高写放大也会减少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

系统指标没有饱和时的慢

通常系统指标没有饱和,但是rocksdb还是达不到预期的速度。有可能有不同的原因。有一些常见的场景:

  • compaction不够快 有时候SSD远没有饱和但是compacrtion还是赶不上。有可能是rocksdb的compaction受限于compaction并行度的配置,没有最大化资源使用。默认的配置通常比较低,有很多空间可以改善。见 Parallelism options

  • 写不够快 虽然写入通常受到写入 I/O 的瓶颈,但在某些情况下,I/O 不够快,rocksdb 无法以足够快的速度写入 WAL 和 memtable。用户可以尝试无序写入、手动 WAL 刷新和/或将相同的数据分片到多个 DB 并并行写入它们。

  • 想要更低的读延迟 又是什么问题也没有用户只是想要更低的读延迟。可以从通过 Perf Context and IO Stats Context 检查每个语句的状态来找出导致延迟高的CPU或者IO问题并尝试调整响应的选项。

调整flush和compaction

flush和comaction是针对多个瓶颈的重要调优,而且很复杂。可以从 compaction 了解rocksdb的compaction是如何工作的。

并行度选项

当compaction之后而磁盘远没有饱时,可以试着增加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线程平均吞吐的最小值。

**max_background_flushes ** 是flush最大并发度,通常设置成1已经足够好了。

compaction 优先级(只适用于leveled compaction)

compaction_pri=kMinOverlappingRatio 是rocksdb的默认值,大多数情况已经是最优的了。我们在UDB和msgdb中都是用他,与以前的默认值相比,写放大下降了一半以上(https://fb.workplace.com/groups/MyRocks.Internal/permalink/1364925913556020/)。

删除时触发compaction

当删除很多行时,一些sst文件会被墓碑填满,影响范围扫描的性能。我们扩展了rocksdb的compaction去追踪长的接近的墓碑。如果发现一个高密度墓碑的key范围,它会立即触发另一次compaction来将它们压缩掉。这帮助了减少因为扫描过多墓碑导致的范围扫描性能倾斜。在rocksdb,对应的类是CompactOnDeletionCollectorFactory。

定时和TTL compaction

虽然compaction类型有时候通常优先删除包含更多删除的文件,但并不能保证这一点。另外一些选项可以帮助到。options.ttl 指定一个过期的数据会从sst文件删除的时间界限。options.periodic_compaction_seconds 保证一个文件每隔一段时间通过compaction过滤器,这样compaction 过滤器会删除相应的数据。

flush 选项

rocksdb的所有写请求会受限插入到一个内存数据结构成为memtable。一旦活跃的memtable(active memtable)被填满,我们创建一个新的并标记这个就得为只读。我们称只读memtable为不可变的(immutable)。任何时间都只存在一个活跃的memtable和0个或者多个不可变的memtable。不可变的memtable在等待被flush到存储上。有三个选项控制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

在level style compaction,数据库文件被组织成不同的层次。memtable被flush到0层的文件,包含最新的数据。更高层包含更老的数据。0层的文件可能会重叠,但是1层及以上的不会重叠。结果是,Get() 通常余姚检查0层的每一个文件,但是在每个连续的层,不会有超过一个的文件同时包含一个key。每层比上一层大10倍(倍数可以配置)。

一个compacrtion可能会拿一些N层的文件和N+1层中重叠的文件一起压缩。两次不同层次或者不同key范围的compaction操作是独立的,可以并发执行。compaction速度直接跟最大写速率成正比。如果compaction不能艮山写速度,数据库使用的磁盘空间会持续增加。将rocksdb配置成compaction高并发执行并且充分利用存储是很重要的。

0层和1层的compaction是个难题。在0层的文件通常跨越整个key空间。当压缩L0->L1(0层到1层),compaction包含1次层所有的文件。当1层所有的文件正在与0层进行压缩是,L1->L2的压缩不能执行;必须等待L0->L1结束。如果L0->L1压缩很慢,大多数时候只有一个运行中的compaction,因为其他compaction必须等待他结束。

L0->L1 压缩是单线程的。使用单线程compaction很难实现好的吞吐。要检查这是否会导致问题,检查磁盘利用率。如果磁盘没有充分使用,有可能在compaction配置除了问题。我们通常建议通过让0层的大小接近1层大小来让L0->L1尽可能的快。

一旦你决定1层的合适的大小,你必须决定层间的倍数(level multiplier)。让我们假设你的1层大小是512MB,level multiplier是10,数据库大小是500GB。2层的大小会是5GB,3层是51GB,4层是512GB。因为你的数据库大小是500GB,5层以上会是空的。

大小放大很容易计算。(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是一样的。

因此总的写放大接近于 1 + 2 + 10 + 10 + 10 = 33。点查询必须查询所有0层的文件和其他层最多一个文件。然而,布隆过滤器大大减少了读放大。然而,临时的范围查询成本比较大。布隆过滤器无法用于范围扫描,所以读放大是number_of_level0_files + number_of_non_empty_levels。

让我们开始了解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_basemax_bytes_for_level_multiplier -- max_bytes_for_level_base是1层总大小。我们建议其大小约为0级的大小。每个连续层是前一个的max_bytes_for_level_multiplier倍大。默认是10,我们不建议去修改它。

target_file_size_basetarget_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

level style compaction 的写放大有可能会在一些用例很高。在写请求占比大的负载,你可能会受限于磁盘吞吐。对这样的工作负载进行优化,rocksdb引入一个新的compaction形势称为 universal compaction,想要减少写放大。然而,它可能会增加读放大并且一直增加空间放大。 universal compaction有一个大小限制。当你的数据库(或者列族)大小大于100GB请小心。可以通过universal compaction 了解更多。

使用universal compaction,一个压缩进程有可能临时让大小放大翻倍。换而言之,如果你存10GB到数据库,compaction进程除了空间放大有可能消费额外的10GB。

然而,有一些技术帮助减少临时的空间防备。如果你使用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,我们不会在系统缓存缓存文件。

**max_open_files ** -- rocksdb在table cache保存所有的文件描述符。如果文件描述符超过了max_open_files,一些文件会从table cache中淘汰并关闭它们的文件描述符。这表示每次读请求必须通过table cache找到需要的文件描述符。设置max_open_files为-1会一直打开所有的文件,避免高成本的table cache调用。

table_cache_numshardbits -- 这是用来控制table cache分配的选项。如果table cache的mutex有影响可以增加这个选项。

**block_size ** -- rocksdb打包用户数据到block中。当从一个tabel文件读一个key-value对是,会加载整个block到内存中 。block size默认是4K。每个table文件包含一个所有block的索引。增加block_size 表示索引会包含更少的条目(因为每个文件的block更少了),所以会更小。增加block_size会减少内存使用和空间放大,但是增加读放大。

共享缓存和线程池

又是你可能会希望在相同的进程里运行多个rocksdb的实例。rocksdb提供这些实例共享block cache和线程池的方法。要共享block cache,将一个cache对象赋值给所有的实例:

first_instance_options.block_cache = second_instance_options.block_cache = rocksdb::NewLRUCache(1GB)

这样两个实例都共享了一个总大小为1GB的block cache。

线程池是跟Env实例关联的。当你构建Options的时候,options.env默认会设置为Env::Default(),大多数情况是最好的。因为所有的选项使用相同的静态对象Env::Default(),线程池默认就是共享的。通过这种方式,你可以设置compaction和flush的运行数量上限,及时是运行多个rocksdb实例的时候。

Write stalls

详情见 Write stalls

前缀数据库

rocksdb保存所有数据并支持有序的迭代(iteration)。但是,某些应用程序不需要对key进行完全排序。他们只想要使用公共前缀排序key。

这些应用会从设置数据库的prefix_extractor获利。

prefix_extractor -- 定义key前缀的SliceTransform 对象。key前缀可以用来实现一些有趣的优化:

  1. 定义前缀布隆过滤器,可以减少前缀范围查询的读放大(例如,拉取前缀为xxx的全部key)。请一定要设置Options::filter_policy
  2. 使用hash-map-based memtable来避免在memtable中二分查找的消耗。
  3. 添加hash索引到table文件来避免在table文件中的二分查找。

想了解更多关于1和2的细节,可以看 Custom memtable and table factories 。请注意1在减少IO上通常已经足够了。2和3可以在某些场景下减少CPU消耗,但是常常会消耗一定的内存。你应该只有在CPU是你的瓶颈,并且你已经使用了所有更容易的方式去减少CPU消耗的时候才尝试去调整它,这并不常见。确保检查在include/rocksdb/options.h中关于prefix_extractor的注释。

布隆过滤器

布隆过滤器(Bloom filters)是用来检查一个元素是否属于一个集合的一部分的概率数据结构。在rocksdb的布隆过滤器通过filter_policy选项来控制。当一个用户小于Get(key)的时候,会有一系列文件可能包含这个key。这通常是所有在0层的文件和其他层每层一个文件。然而在我们读每个文件之前,我们先咨询布隆过滤器。布隆过滤器会过滤掉大多数不包含key的文件的读取。在大多数情况下,Get()只需要读一个文件。布隆过滤器始终保存在内存中以供打开的文件使用,除非BlockBasedTableOptions::cache_index_and_filter_blocks设置成了true。大家文件的数量由max_open_files 选项来控制。

有两种类型的布隆过滤器:block-based和全过滤。

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和table的格式。

**memtable_factory ** -- 定义memtable。这里有我们支持的memtable的列表:

  1. SkipList -- 默认memtable
  2. HashSkipList -- 只有 prefix_extractor 才有意义。它会根据key的前缀保存key到不同的桶(bucket)中。每个桶是一个跳表。
  3. HashLinkedList 只有 prefix_extractor 才有意义。它会根据key的前缀保存key到不同的桶(bucket)中。每个桶是一个链表。

**table_factory ** -- 定义table格式。这里有我们支持的table的列表:

  1. Block based -- 这是默认的table。这适用于吧数据存储到磁盘和flash存储。它使用block大小的块来寻址和加载(见block_size 选项)。所以叫Block based。
  2. Plain Table -- 只有 prefix_extractor 才有意义。适合存数据到内存(tmpfs文件系统)。它是字节可寻址。

内存使用

学习更多可以看https://github.com/facebook/rocksdb/wiki/Memory-usage-in-RocksDB

机械硬盘的区别

Tuning RocksDB on Spinning Disks

配置示例

在这个章节我们会展示一些我们实际在生产环境使用的rocksdb的配置。

在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存储

这个数据库同时执行Get()和全排序迭代。分片?

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。

机械硬盘的数据库

功能齐全的内存数据库

在这个例子中,数据库是安装在tmpfs文件系统。

使用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。

更积极地设置压缩并为刷新和compaction分配更多的线程:

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。

内存前缀数据库

在这个离职中,数据库是安装在tmpfs文件系统。我们使用自定义格式来加速,有一些功能是不支持的。我们只支持Get()和前缀范围扫描。预写日志(Write-ahead logs,WAL)存储在硬盘避免在查询以外的地方消耗内存。Pre()不支持。

因为数据库是在内存的,我们不关心写放大。我们更关心读放大和空间放大。这是个有趣的离职,因为我们将compaction调到一个极端,系统中通常只会有一个sst table。我们隐藏减少读和空间放大,写放大非常大。

因为使用了通用compaction( universal compaction),我们将在压缩期间有效地增加一倍我们的空间使用量。这在内存数据库是非常危险的。我们隐藏将数据分片成400个rocksdb实例。我们允许只有两个并发的compaction,这样在一个时间只有两个分片可能会使空间翻倍。

在这个例子,前缀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不是简单的事情。及时我们作为rocksdb的开发者也不完全知道每个配置的修改会带来的影响。如果你想要完全为你的工作负载进行优化,我们建议实验和benchmark,同时关注三个放大因素。同时,请不要犹豫来 RocksDB Developer's Discussion Group 找我们寻求帮助。