RocksDB 调优指南

本指南的目的是为您提供足够的信息,以便您可以根据工作负载和系统配置RocksDB进行调优

RocksDB非常灵活,这既有好处也有坏处。您可以针对各种工作负载和存储技术对其进行调优。 在Facebook内部,我们对内存工作负载,flash闪存设置和旋转磁盘使用相同的代码库。 然而,灵活性并不总是用户友好的。我们引入了大量可能令人困惑的调优选项。 我们希望本指南将帮助您从系统中挤出最后一滴性能,并充分利用您的资源。

我们假设您已经基本了解了日志结构合并树(LSM)的工作原理。在LSM上已经有很多很好的资源,不需要再编写一个。

Amplification factors 放大因素

优化RocksDB常常需要在个放大因子之间进行权衡:写放大、读放大 和 空间放大。

写入放大:是写入存储的字节数与写入数据库的字节数之比。

例如,如果您正在向数据库写入10MB/s,并且观察到30MB/s的磁盘写入速率,则写入放大为3,如果写放大很大,工作负载可能会阻塞磁盘吞吐量。 例如,如果写放大为50,最大磁盘吞吐量为500MB/s,那么数据库可以保持10MB/s的写速率,在这种情况下,减少写入放大将直接增加最大写入速率。 高写入放大也会降低闪存寿命。有两种方法可以观察您的写入放大。 第一种方法是读取DB::GetProperty(“rocksdb.stats”, &stats)。 第二种方法是将磁盘写带宽(可以使用iostat)除以DB写速率。

读取放大:是每个查询读取磁盘的数量。如果您需要阅读5页的数据来返回一个查询,那么读取放大就是5。 逻辑读取是那些从缓存中获取数据的操作,无论是RocksDB块缓存还是OS文件系统缓存。物理读取由存储设备、闪存或磁盘处理。逻辑读取比物理读取成本低, 但仍然会增加CPU成本。您可能能够从iostat输出中评估物理读取速率,但这包括对查询和压缩执行的读取。

空间放大:是磁盘上数据库文件大小与数据大小的比值。如果在数据库中放入10MB,并且在磁盘上使用100MB,那么空间放大为10。通常需要设置空间放大的硬限制, 这样就不会耗尽磁盘空间或内存。

为了在不同数据库算法的背景下了解更多这三个放大因子,我们强烈推荐Mark Callaghan在Highload上的演讲。(http://vimeo.com/album/2920922/video/98428203)

RocksDB statistics RocksDB统计

在调试性能时,有一些工具可以帮助您:

statistics — 将其设置为rocksdb::CreateDBStatistics()。您可以通过调用options.statistics.ToString() 随时获得人性化可读的RocksDB统计信息。 有关详细信息,请参见统计数据(https://github.com/facebook/rocksdb/wiki/Statistics)。

stats_dump_period_sec — 我们每秒钟将统计信息转储到日志文件中。默认值是600,这意味着每10分钟将转储一次统计信息。 通过调用db->GetProperty(“rocksdb.stats”),可以在应用程序中获得相同的数据;

每一个stats_dump__sec,你会在你的日志文件中发现类似这样的东西:

  1. ** Compaction Stats **
  2. Level Files Size(MB) Score Read(GB) Rn(GB) Rnp1(GB) Write(GB) Wnew(GB) Moved(GB) W-Amp Rd(MB/s) Wr(MB/s) Comp(sec) Comp(cnt) Avg(sec) Stall(sec) Stall(cnt) Avg(ms) KeyIn KeyDrop
  3. -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  4. L0 2/0 15 0.5 0.0 0.0 0.0 32.8 32.8 0.0 0.0 0.0 23.0 1457 4346 0.335 0.00 0 0.00 0 0
  5. L1 22/0 125 1.0 163.7 32.8 130.9 165.5 34.6 0.0 5.1 25.6 25.9 6549 1086 6.031 0.00 0 0.00 1287667342 0
  6. L2 227/0 1276 1.0 262.7 34.4 228.4 262.7 34.3 0.1 7.6 26.0 26.0 10344 4137 2.500 0.00 0 0.00 1023585700 0
  7. L3 1634/0 12794 1.0 259.7 31.7 228.1 254.1 26.1 1.5 8.0 20.8 20.4 12787 3758 3.403 0.00 0 0.00 1128138363 0
  8. L4 1819/0 15132 0.1 3.9 2.0 2.0 3.6 1.6 13.1 1.8 20.1 18.4 201 206 0.974 0.00 0 0.00 91486994 0
  9. Sum 3704/0 29342 0.0 690.1 100.8 589.3 718.7 129.4 14.8 21.9 22.5 23.5 31338 13533 2.316 0.00 0 0.00 3530878399 0
  10. Int 0/0 0 0.0 2.1 0.3 1.8 2.2 0.4 0.0 24.3 24.0 24.9 91 42 2.164 0.00 0 0.00 11718977 0
  11. Flush(GB): accumulative 32.786, interval 0.091
  12. Stalls(secs): 0.000 level0_slowdown, 0.000 level0_numfiles, 0.000 memtable_compaction, 0.000 leveln_slowdown_soft, 0.000 leveln_slowdown_hard
  13. Stalls(count): 0 level0_slowdown, 0 level0_numfiles, 0 memtable_compaction, 0 leveln_slowdown_soft, 0 leveln_slowdown_hard
  14. ** DB Stats **
  15. Uptime(secs): 128748.3 total, 300.1 interval
  16. Cumulative writes: 1288457363 writes, 14173030838 keys, 357293118 batches, 3.6 writes per batch, 3055.92 GB user ingest, stall micros: 7067721262
  17. Cumulative WAL: 1251702527 writes, 357293117 syncs, 3.50 writes per sync, 3055.92 GB written
  18. Interval writes: 3621943 writes, 39841373 keys, 1013611 batches, 3.6 writes per batch, 8797.4 MB user ingest, stall micros: 112418835
  19. Interval WAL: 3511027 writes, 1013611 syncs, 3.46 writes per sync, 8.59 MB written

Compaction stats 压缩统计数据

层级N和N+1之间执行的压缩的压缩统计数据在层级N+1上报告(压缩输出)。这里是快速参考:

  • Level - 对于LSM层次的leveled层次压缩。对于通用压缩,所有文件都在L0中。Sum将所有级别的值聚合在一起。Int类似Sum,但仅限于上一个报告间隔的数据。
  • Files - 它有两个值(a/b)。第一个是该级别的文件数量。第二个是当前为该级别执行压缩的文件数量。
  • Score: 对于除L0之外的级别,得分为(当前级别大小)/(最大级别大小)。0或1的值是可以的,但是任何大于1的值都意味着需要压缩该级别。
    1. 对于L0,分数是根据当前文件数量h和触发压缩的文件数量计算的。
  • Read(GB): 在级别N和N+1之前的压缩期间读取的总字节数。这包括从级别N和N+1读取的字节。
  • Rn(GB): 在级别N和N+1之间的压缩期间,从级别N读取字节。
  • Rnp1(GB): 在级别N和N+1之间的压缩期间,从级别N+1读取字节。
  • Write(GB): 在级别N和N+1之间的压缩期间写入的总字节数。
  • Wnew(GB): 将新字节写到级别N+1,计算为(将总字节写到N+1) - (在使用级别N进行压缩时从N+1读取字节)
  • Moved(GB): 在压缩期间将字节移动到级别N+1。在本例中,除了更新清单以指示以前位于X级别的文件现在位于Y级别外,没有其他IO
  • W-Amp: (写入到N+1级的总字节数) / (从N级读取的总字节数) 这是级别N和N+1之间的压缩的写放大。
  • Rd(MB/s): 在级别N和N+1之间的压缩期间读取数据的速度。这是 (Read(GB) * 1024) / duration,其中duration是从级别N到级别N+1进行压缩的时间。
  • Wr(MB/s): 压缩期间写入数据库的速率。查阅:Rd(MB/s)
  • Rn(cnt): 在级别N和N+1之间的压缩期间,从级别N读取的文件总数。
  • Rnp1(cnt): 在级别N和级别N+1之间的压缩期间,从级别N+1读取的总文件。
  • Wnp1(cnt): 在级别N和级别N+1之前的压缩过程中写入到级别N+1的总文件。
  • Wnew(cnt): (Wnp1(cnt) - Rnp1(cnt)) — 由于级别N和N+1之前的压缩,文件数量增加。
  • Comp(sec): 在级别N和N+1之间执行压缩的总时间。
  • Comp(cnt): 级别N和N+1之间的压缩总数。
  • Avg(sec): 在N级和N+1级之间每次压缩的平均时间。
  • Stall(sec): 由于级别N+1未压缩(压缩分数很高),总写时间被停止
  • Stall(cnt): 由于级别N+1未压缩而导致写失速的总数
  • Avg(ms): 由于级别N+1未压缩,以毫秒为单位的平均写操作时间被停止
  • KeyIn: 压缩期间比较的记录数
  • KeyDrop: 在压缩过程中删除(而不是写出来)的记录数量

General stats 常规统计数据

在每个级别的压缩统计信息之后,我们还输出一些常规统计信息。一般统计数据包括累积和间隔。 累计统计报告从RocksDB实例启动的总值。Interval stats报告自上次stats输出以来的值。

  • Uptime(secs): total — 此实例运行的秒数,interval — 自上次stats转储以来的秒数。
  • Cumulative/Interval writes: total — Put调用的数量; keys — 来自Put调用的WriteBatches中的条目数;
    1. batches -- 每个组提交持久执行一个或多个Put调用的组提交数量(并发性中可以有多个Put调用在某个时间点持久执行);
    2. per batch -- 单个batch中平均字节数;ingest -- 写入DB的总字节数(不包括压缩);
    3. stall micros -- 压缩落后时,写入的微秒数已停止
  • Cumulative/Interval WAL: writes — 记录在WAL中的写数量; syncs — 使用fsync或fdatasync的次数;

    1. writes per sync -- 写入与同步的比率; GB written -- 写入到WALGB数量。
  • Stalls: 从时间开始的每个档位类型的总计数和秒数:level0_slowdown — level0_slowdown_writes_trigger导致的Stall停滞; level0_numfiles — level0_stop_writes_trigger导致的Stall停滞.

    1. memtable_compaction -- 由于所有memtables都已满,刷新进程无法跟上; leveln_slowdown -- 由于soft_rate_limithard_rate_limit而导致的停滞.

Perf Context and IO Stats Context Perf上下文和IO Stats上下文

Perf上下文和IO Stats上下文可以帮助确定一个特定查询中的计数器。(https://github.com/facebook/rocksdb/wiki/Perf-Context-and-IO-Stats-Context)

Parallelism options 并行性选项

在LSM体系结构中,有两个后台进程:刷新和压缩。两者都可以通过线程并发执行,以利用存储技术的并发性。 刷新线程在高优先级池中,而压缩线程在低优先级池中。要增加每个池调用中的线程数:

  1. options.env->SetBackgroundThreads(num_threads, Env::Priority::HIGH);
  2. options.env->SetBackgroundThreads(num_threads, Env::Priority::LOW);

要从更多线程中获益,您可能需要设置这些选项来更改并发压缩和刷新的最大数量:

max_background_compactions 是并发背景压缩的最大数目。默认值是1,但是要充分利用CPU和存储,您可能需要将其增加到系统中大约的核心数量。 max_background_flushes 是并发刷新操作的最大数目。通常,设置为1就足够了。

General options 通用选项

filter_policy — 如果您进行point点搜索,您肯定想打开Bloom过滤器。我们使用Bloom过滤器来避免不必要的磁盘读取。您应该将filter_policy设置为rocksdb::NewBloomFilterPolicy(bits_per_key). 默认的每键位为10,产生约1%的假阳性率。较大的bits_per_key值将减少假阳性率,但会增加内存使用量和空间放大。

block_cache — 我们通常建议将其设置为调用rocksdb::NewLRUCache(cache_capacity, shard_bits)的结果,块缓存缓存未压缩的块。另一方面,OS Cache缓存压缩块(因为这是它们存储在文件中的方式)。因此,同时使用block_cache和OS cache是有意义的。 我们需要锁定对块缓存的访问,有时我们会看到RocksDB阻塞了快缓存的互斥,特别是当DB大小小于RAM时。在这种情况下,通过将shard_bits设置为更大的数字来分割块缓存是有意义的。如果shard_bits是4,那么碎片的总数将是16. allow_os_buffer — 如果为false,我们将不会在OS缓存中缓存文件。参见上面的评论。 max_open_files — RocksDB将所有文件描述符保存在一个表缓存中。如果文件描述符的数量超过max_open_files,则从表缓存中删除一些文件并关闭它们的文件描述符。这意味着每次读取都必须通过表缓存来查找所需所需的文件。将max_open_files设置为-1,以始终保持所有文件打开,这样可以避免昂贵的表缓存调用。 table_cache_numshardbits — 此选项控制表缓存分片。如果争用表缓存互斥量,则增加它。 block_size — RocksDB以块的形式打包用户数据。当从表文件中读取键值对时,将整个块加载到内存中。块大小默认为4KB。每个表文件包含一个索引,列出所有块的偏移量。 增加block_size意味着索引包含更少的条目(因为每个文件包含更少的块),因此更小。增加block_size会减少内存使用量和空间放大,但是会增加读取放大。

Sharing cache and thread pool 共享缓存和线程池

有时,您可能希望从同一个进程运行多个RocksDB实例。RocksDB为这些实例提供了共享块缓存和线程池的方法。若要共享块缓存,请为所有实例分配一个缓存对象。

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

这将使两个实例共享一个总大小为1GB的块缓存。

线程池与Env对象相关联。当你构建选项时,options.env被设置为Env::Default(),因此线程池在默认情况下是共享的。参见Parallelism options 并行选项,了解如何在线程池中设置线程数。 这样,即使运行多个RocksDB实例,也可以设置并发运行的压缩和刷新的最大数量。

Flushing options 刷新选项

所有对RocksDB的写操作都首先插入到名为memtable的内存数据结构中。一旦active活跃memtable被填满,我们将创建一个新的memtable并将旧的memtable标记为只读。 我们称只读memtable为不可变的。在任何时候,都只有一个active活跃的memtable和零个或多个不可变的memtable。不可变的memtables正在等待被刷新到存储中。有三个选项控制刷新行为:

write_buffer_size 设置单个memtable的大小。一旦memtable超过这个大小,它就被标记为不可变的,并创建一个新的memtable。

max_write_buffer_number memtable设置memtable的最大数量,包括活动的和不可变的。如果活动的memtable被填满,并且memtable的总数大于max_write_buffer_number,那么我们将停止进一步的写操作。 如果刷新进程比写速率慢,可能会发生这种情况。

min_write_buffer_number_to_merge 是将内存表刷新到存储之前要合并的最小内存表数。例如,如果将此选项设置为2,则仅当不可变memtable有两个时才刷新它们 - 一个不可变memtable永远不会被刷新。 如果将多个memtable合并在一起,由于将两个更新合并到一个键中,写入存储器的数据可能会更少。但是,每个Get()必须线性遍历所有不可变的memtable,以检查键是否存在,将此选项设置过高可能会损害读取性能。

例子:选项:

  1. write_buffer_size = 512MB;
  2. max_write_buffer_number = 5;
  3. min_write_buffer_number_to_merge = 2;

写速率为16MB/s。在本例中,每32秒创建一个新的memtable,两个memtable将合并在一起,并每64秒刷新一次。根据工作集的大小,刷新大小将在512MB到1GB之间。 为了防止刷新速度跟不上写入速度,memtables使用的内存上限为 5 * 512MB = 2.5GB。当达到这个值时,任何进一步的写操作都会被阻塞,直到刷新完成并释放memtables使用的内存为止。

Level Style Compaction 层级样式压缩

在Level样式压缩中,数据库文件被组织成层次。Memtables被刷新到L0的文件中,其中包含最新的数据。 较高的级别包含较旧的数据。L0中的文件可能重叠,但L1及更高的文件不重叠。因此,Get()通常需要从第0级开始检查每个文件,但是对于每个连续的级别,最多只能有一个文件包含key。 每个级别都比前一个级别大10倍(这个乘数是可配置的)。

压缩可以从N级获取一些文件,然后用N+1级的重叠文件压缩它们。两个操作在不同级别或不同键范围的压缩是独立的,可以并发执行。压缩速度与最大写入速率成正比。 如果压缩不能跟上写速率,数据库使用的空间将继续增长。重要的是配置RocksDB,使压缩可以以高并发性执行,并充分利用存储。

L0和L1的压缩比较复杂。L0的文件通常跨越整个键空间。当压缩L0->L1(从0级压缩到1级)时,压缩包含来自1级的所有文件。当L1中的所有文件都被L0压缩后,L1->L2不能继续压缩;它必须等待L0->L1压缩完成。 如果L0->L1压缩比较慢,那么它将是系统中大多数时间中唯一运行的压缩,因为其他压缩必须等待它完成。

L0->L1 也是单线程的。单线程压缩很难实现良好的吞吐量。要查看这是否导致问题,请检查磁盘利用率。 如果没有充分利用磁盘,则可能存在压缩配置问题。我们通常建议通过使L0的大小与L1的大小相似来使L0->L1尽可能快。

一旦确定了L1的适当大小,就必须确定级别乘数。假设您的L1的大小是512MB,级别乘法是10,数据库大小是500GB。L2的大小将是5GB、L3的大小将是51GB 和 L4的512GB。 由于数据库大小为500GB, L5或更高的数据库将为空。

尺寸放大容量计算。它是(512 MB + 512 MB + 5GB + 51GB + 512GB) / (500GB) = 1.14。 下面是我们计算写入放大的方法: 首先将每个字节写到L0。然后将其压缩到级别1。由于L1的大小与L0相同,所以L0->L1压缩的写入放大值为2。 然而,当第1级的字节被压缩到第二级时,它被压缩到第2级的10个字节(因为第2级要大10倍)。L2->L3和L3->L4压缩也是如此。

因此,总的写入放大约为 1 + 2 + 10 + 10 + 10 = 33。 点查找必须参考L0中的所有文件,并且彼此级别之间最多只能有一个文件。然而,Bloom过滤器有助于大大降低读取放大。 然而,短时间的范围扫描要贵一些。Bloom过滤器对范围扫描没有用处,因此读取的放大是number_of_level0_files + number_of_non_empty_levels。

让我们深入研究控制级别压缩的选项。我们将从更重要的开始,然后是不那么重要的。

level0_file_num_compaction_trigger - 一旦L0达到这个文件数量,就会触发L0->L1压缩。 因此,我们可以将处于稳定状态的L0大小估计为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是L1的总大小。如前所述,我们建议这个级别的大小在L0左右,每个后缀级别为 max_bytes_for_level_multiplier 都大于前一个级别。默认值是10,我们不建议更改它。 target_file_size_basetarget_file_size_multiplier - L1中的文件将具有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,这样在L1中就有10个文件。 compression_per_level - 使用此选项为不同级别设置不同的压缩。通常,避免压缩L0和1,只压缩更高级别的数据是有意义的。 您甚至可以在最高级别设置较慢的压缩,在较低级别设置较快的压缩(我们所说的最高是指Lmax)。 num_levels - num_levels大于数据库中预期的级别数量是安全的。一些更高的级别可能是空的,但这不会以任何方式影响性能。只有当您期望级别数大于7(默认值)时才更改此选项。

Universal Compaction 通用压缩

在某些情况下,级别样式压缩的写放大值可能很高。对于写任务繁重的工作负载,可能会出现磁盘吞吐量瓶颈。 为了优化这些工作负载,RocksDB引入了一种新的压缩方式,我们称之为通用压缩,旨在减少写放大。 但是,它可能会增加读取放大,并且总是增加空间放大。通用压缩具有大小限制,当您的DB(或列族)大小超过100GB时,请小心。 有关详细信息。请参阅Universal Compaction(https://github.com/facebook/rocksdb/wiki/Universal-Compaction)

但是,有一些技术可以帮助减少临时空间翻倍。如果使用通用压缩,我们强烈建议对数据进行切分,并将其保存在多个RocksDB实例中。假设您有S分片,然后,仅使用N个压缩线程配置Env线程池。在所有S分片中,只有N个分片会有额外的空间放大,从而将其降低到N/S而不是1。 例如,如果您的数据库是10GB,并且配置了100个分片,那么每个分片将包含100MB的数据。如果使用20个并发压缩配置线程池,则只会消耗额外的2GB数据,而不是10GB。此外,压缩将并行执行,这将充分利用您的存储并发性。

max_size_amplification_percent - 大小放大,定义为在数据库中存储一个字节数据所需的额外存储量(以百分比为单位)。默认值是200,这意味着一个100字节的数据库可能需要多达300字节的存储空间。 这300个字节中的200个字节是临时的,仅在压缩期间使用。增加这个限制会降低写放大,但是(明显地)会增加空间放大。 compression_size_percent - 数据库中被压缩的数据的百分比。旧数据被压缩,新数据不被压缩。如果设置为-1(默认值),则压缩所有数据。减少compression_size_percent将减少CPU使用并增加空间放大。

对于通用压缩,压缩过程可以暂时将尺寸放大两倍。换句话说,如果您在数据库中存储10GB,除了空间放大之外,压缩过程还可能消耗额外的10GB。

有关通用压缩的更多信息,请参见Universal Compaction通用压缩页面。(https://github.com/facebook/rocksdb/wiki/Universal-Compaction)

Write stalls 写停滞

有关详细信息,参见 Write stall 页面(https://github.com/facebook/rocksdb/wiki/Write-Stalls)。

Prefix databases 前缀数据库

RocksDB保持所有数据的排序,并支持有序的迭代。然而,有些应用程序不需要对键进行完全排序。他们只对具有公共前缀的键排序感兴趣。

这些应用程序可以从为数据库配置prefix_extractor中获益。

prefix_extractor - 一个定义关键前缀的SliceTransform对象。然后使用关键前缀执行一些有趣的优化:

1.定义前缀bloom过滤器,它可以减少前缀范围查询的读取放大(例如,给我所有以XXX开头的键)。 2.使用基于散列映射的memtables来避免memtables中的二进制搜索开销。 3.将哈希索引添加到表文件中,以避免表文件中的二进制搜索成本。有关(2)和(3)的更多信息,请参见自定义memtable和表工厂。请注意(1)通常足以减少I/Os。 (2)和(3)可以在某些用例中降低CPU成本,通常还会降低一些内存成本。您应该只在CPU是您的瓶颈并且您没有其他更容易的调优来保存CPU时才尝试它,这是不常见的。 确保在include/rocksdb/options.h中检查关于prefix_extractor的注释。

Bloom filters 布隆过滤器

Bloom过滤器是一种概率数据结构,用于测试一个元素是否是集合的一部分。RocksDB中的Bloom过滤器由一个选项filter_policy控制。 当用户调用Get(key)时,会有一个包含该键的文件列表。这通常是所有级别0上的文件,每个级别都有一个大于0的文件。 然而,在阅读每个文件之前,我们首先要参考bloom过滤器。Bloom过滤器将过滤掉不包含密钥的大多数文件的读取。 在大多数情况下,Get()只读取一个文件。Bloom过滤器总是为打开的文件保存在内存中, 除非BlockBasedTableOptions::cache_index_and_filter_blocks被设置为true。打开文件的数量由max_open_files选项控制。

有两种类型的bloom过滤器:基于块的过滤器和完全过滤器。

Block-based filter

通过调用options.filter_policy.reset(rocksdb::NewBloomFilterPolicy(10, true)) 设置基于块的过滤器。 基于块的bloom过滤器是为每个块单独构建的。在读取时,我们首先查询索引,它返回我们要查找的键的块。现在我们有了一个块,我们向bloom filter查询该块

Full filter

通过调用options.filter_policy.reset(rocksdb::NewBloomFilterPolicy(10,false))设置完整的过滤器, 每个文件都构建完整的过滤器。 一个文件只有一个bloom过滤器。这意味着我们可以在不访问索引的情况下首先查询bloom过滤器。在键不在bloom过滤器中的情况下, 与基于块的过滤器相比,我们保存了一个索引查找。

完整的过滤器可以进一步分区:分区过滤器(https://github.com/facebook/rocksdb/wiki/Partitioned-Index-Filters)

Custom memtable and table format 自定义memtable和表格式

高级用户可以配置自定义memtable和表格式。

memtable_factory — 定义了memtable,以下是我们支持的memtables列表:

1.SkipList — 这是默认的memtable 2.HashSkipList — 只是使用prefix_extractor才有意义。它根据键的前缀将键保存在桶中。每个bucket都是一个跳转列表。 3.HashLinkedList — 它适用于prefix_extractor。它根据键的前缀将键保存在桶中。每个bucket都是一个链表。

table_factory — 定义表格式。以下是我们支持的列表:

1.Block based 基于块的 — 这是默认表。它适用于在磁盘和闪存上存储数据。它是在块大小的块中寻址和加载的(参见block_size选项)。因此,基于块的名称。

2.Plain Table 普通表 — 只有使用prefix_extractor才有意义。它适合在内存(tmpfs文件系统)上存储数据。这是 byte-addressible 字节-可寻址的.

Memory usage 内存使用

要了解关于RocksDB如何使用内存的更多信息,请查看这个wiki页面:https://github.com/facebook/rocksdb/wiki/Memory-usage-in-RocksDB

Difference of spinning disk 差异性旋转磁盘

对于旋转磁盘上的数据库,内存/持久存储比通常要低得多。如果数据与RAM的比例过大,则可以减少将性能关键数据保存在RAM中所需的内存。建议:

  • 使用相对较大的块大小来减少索引块大小。您应该使用至少64KB的块大小。您可以考虑使用256KB甚至512KB。使用大块的缺点是在块缓存中浪费RAM。
  • 打开 BlockBasedTableOptions.cache_index_and_filter_blocks=true,因为很可能无法在内存中容纳所有索引和bloom过滤器。即使可以,最好还是把它放在安全的地方。
  • 启用 options.optimize_filters_for_hits 减少一些bloom过滤器块的大小。
  • 注意是否有足够的内存来保存所有的bloom过滤器。如果不能,那么bloom过滤器可能会影响性能。
  • 尽量压缩keys的编码。较短的键可以减少索引块的大小。

旋转磁盘通常比闪存提供更低的随机读取吞吐量。

  • 设置 options.skip_stats_update_on_db_open=true 以加快DB的打开时间。
  • 这里有一个争议的建议:使用基于Level级别的压缩,因为减少对磁盘的读取更友好。
  • 如果使用基于Level级别的压缩,请使用options.level_compaction_dynamic_level_bytes=true.
  • 如果服务器有多个磁盘,设置 options.max_file_opening_threads 的值将大于1。

在旋转磁盘中,随机读取和顺序读取之间的吞吐量差距要大得多。建议:

  • 启用RocksDB级别预读取压缩输入: options.compaction_readahead_sizeoptions.new_table_reader_for_compaction_inputs=true
  • 使用相对较大的文件大小。我们建议至少256MB。
  • 使用相对较大的块大小。

旋转磁盘比flash闪存大得多

  • 为了避免太多的文件描述符,可以使用更大的的文件。我们建议文件大小至少为256MB.
  • 如果使用通用压缩样式,不要将单个DB大小设置得太大,因为完整的压缩将花费很长时间并影响性能。您可以使用更多的DBs,但是单个DB的大小小于500GB。

Example configurations 示例配置

在本节中,我们将介绍一些实际在生产中运行的RocksDB配置。

Prefix database on flash storage 闪存上的前缀数据库

该服务使用RocksDB执行前缀范围扫描和点查找。它运行在闪存上.

  1. options.prefix_extractor.reset(new CustomPrefixExtractor());

由于服务不需要总顺序迭代(参见前缀数据库),所以我们定义了前缀提取器。

  1. rocksdb::BlockBasedTableOptions table_options;
  2. table_options.index_type = rocksdb::BlockBasedTableOptions::kHashSearch;
  3. table_options.block_size = 4 * 1024;
  4. options.table_factory.reset(NewBlockBasedTableFactory(table_options));

我们在表中使用哈希索引来加快前缀查找,但是它增加了存储空间和内存使用量。

  1. options.compression = rocksdb::kLZ4Compression;

LZ4压缩减少了CPU的使用,但增加了存储空间。

  1. options.max_open_files = -1;

此设置禁用在表缓存中查找文件,从而加快所有查询的速度。如果您的服务器对打开的文件有很大的限制,那么这总是一个好设置。

  1. options.options.compaction_style = kCompactionStyleLevel;
  2. options.level0_file_num_compaction_trigger = 10;
  3. options.level0_slowdown_writes_trigger = 20;
  4. options.level0_stop_writes_trigger = 40;
  5. options.write_buffer_size = 64 * 1024 * 1024;
  6. options.target_file_size_base = 64 * 1024 * 1024;
  7. options.max_bytes_for_level_base = 512 * 1024 * 1024;

我们使用级别样式压缩。Memtable大小为64MB,定期刷新到0级。压缩缩L0->L1在有10个0级文件时触发(总共640MB)。当L0为640MB时,压缩被触发到L1,L1的最大大小为512MB。总数据库大小???

  1. options.max_background_compactions = 1
  2. options.max_background_flushes = 1

在任何给定时间只能执行一个并发压缩和一个刷新。但是,系统中有多个切分,因此在不同的切分上发生多个压缩。否则,只有2个线程写入存储器,存储器就不会饱和。

  1. options.memtable_prefix_bloom_bits = 1024 * 1024 * 8;

使用memtable bloom过滤器,可以避免对memtable的一些访问。

  1. options.block_cache = rocksdb::NewLRUCache(512 * 1024 * 1024, 8);

块缓存被设置为512MB。(它是跨碎片共享的吗?)

Total ordered database, flash storage 总有序数据库,闪存

这个数据库同时执行Get()和total order迭代。分片????

  1. options.env->SetBackgroundThreads(4);

我们首先在线程池中总共设置了4个线程。

  1. options.options.compaction_style = kCompactionStyleLevel;
  2. options.write_buffer_size = 67108864; // 64MB
  3. options.max_write_buffer_number = 3;
  4. options.target_file_size_base = 67108864; // 64MB
  5. options.max_background_compactions = 4;
  6. options.level0_file_num_compaction_trigger = 8;
  7. options.level0_slowdown_writes_trigger = 17;
  8. options.level0_stop_writes_trigger = 24;
  9. options.num_levels = 4;
  10. options.max_bytes_for_level_base = 536870912; // 512MB
  11. options.max_bytes_for_level_multiplier = 8;

我们使用具有高并发性的级别样式压缩。Memtable大小为64MB, 0级文件的总数为8个。这意味着当L0大小增长到512MB时将触发压缩。 L1的大小为512MB,每层的大小都是上一层的8倍。L2是4GB, L3是32GB。

Database on Spinning Disks 数据库跑在旋转磁盘

很快…..

In-memory database with full functionalities 具有完整功能的内存数据库

在本例中,数据库安装在tmpfs文件系统中。

用mmap读: options.allow_mmap_reads = true;

禁用块缓存,启用bloom过滤器并减少增量编码重启间隔:

  1. BlockBasedTableOptions table_options;
  2. table_options.filter_policy.reset(NewBloomFilterPolicy(10, true));
  3. table_options.no_block_cache = true;
  4. table_options.block_restart_interval = 4;
  5. options.table_factory.reset(NewBlockBasedTableFactory(table_options));

如果你想优先考虑速度。你可以禁用压缩:

  1. options.compression = rocksdb::CompressionType::kNoCompression;

否则,启用轻量级压缩,LZ4或Snappy。

更积极地设置压缩,并分配更多线程用于刷新和压缩:

  1. options.level0_file_num_compaction_trigger = 1;
  2. options.max_background_flushes = 8;
  3. options.max_background_compactions = 8;
  4. options.max_subcompactions = 4;

保持所有文件打开:

  1. options.max_open_files = -1;

在读取数据时,考虑转换ReadOptions.verify_checksums = false.

In-memory prefix database 内存预置数据库

在本例中,数据库安装在tmpfs文件系统中。我们使用定制格式来加快速度,但不支持某些功能。我们只支持Get()和前缀范围扫描。写前日志存储在硬盘上,以避免消耗不用于查找的内存。不支持Prev()。

由于这个数据库在内存中,所以我们不关心写入放大。但是,我们非常关心读取放大和空间放大。这是一个有趣的例子,因为我们将压缩调到极致,所以系统中通常只存在一个SST表。因此,我们减少了读取和空间放大,而写入放大非常高。

由于使用了通用压缩,我们将有效地加倍压缩期间的空间使用。这对于内存数据库来说是非常危险的。因此,我们将数据分割为400个RocksDB实例。我们只允许两个并发压缩,因此在任何时间只有两个碎片可以双倍使用空间。

在这种情况下,可以使用前缀哈希来允许系统使用哈希索引而不是二进制索引,并在可能的情况下使用bloom过滤器进行迭代:

  1. options.prefix_extractor.reset(new CustomPrefixExtractor());

使用为低延迟访问构建的内存寻址表格式,这需要打开mmap读取模式:

  1. options.table_factory = std::shared_ptr<rocksdb::TableFactory>(rocksdb::NewPlainTableFactory(0, 8, 0.85));
  2. options.allow_mmap_reads = true;
  3. options.allow_mmap_writes = false;

使用哈希链表memtable在mem表中将二分查找改为哈希查找:

  1. options.memtable_factory.reset(rocksdb::NewHashLinkListRepFactory(200000));

为哈希表启用bloom filter,以减少从mem表读取到1时的内存访问(通常意味着CPU缓存丢失),对于在memtable中没有找到键的情况:

  1. options.memtable_prefix_bloom_bits = 10000000;
  2. options.memtable_prefix_bloom_probes = 6;

优化压缩,这样,当我们有两个文件时,就会启动一个完整的压缩。我们修改了通用压缩的参数:

  1. options.compaction_style = kUniversalCompaction;
  2. options.compaction_options_universal.size_ratio = 10;
  3. options.compaction_options_universal.min_merge_width = 2;
  4. options.compaction_options_universal.max_size_amplification_percent = 1;
  5. options.level0_file_num_compaction_trigger = 1;
  6. options.level0_slowdown_writes_trigger = 8;
  7. options.level0_stop_writes_trigger = 16;

调整bloom过滤器,以减少内存访问:

  1. options.bloom_locality = 1;

所有表的读取器对象总是缓存,避免在读取时访问表缓存:

  1. options.max_open_files = -1;

一次使用一个mem表。它的大小是由我们要支付的全部压缩间隔决定的。我们对压缩进行调优,以便在每次刷新之后都触发完整的压缩,这将消耗CPU。 mem表越大,压缩间隔就越长,同时,在重新启动数据库时,我们会发现内存效率更低,查询性能更差,恢复时间更长。

  1. options.write_buffer_size = 32 << 20;
  2. options.max_write_buffer_number = 2;
  3. options.min_write_buffer_number_to_merge = 1;

多个DBs共享相同的2个压缩池:

  1. options.max_background_compactions = 1;
  2. options.max_background_flushes = 1;
  3. options.env->SetBackgroundThreads(1, rocksdb::Env::Priority::HIGH);
  4. options.env->SetBackgroundThreads(2, rocksdb::Env::Priority::LOW);

WAL日志设置:

  1. options.bytes_per_sync = 2 << 20;

Suggestion for in memory block table 建议在内存块表

hash_index: 在新版本中,哈希索引为基于块的表启用。它将使用5%以上的存储空间,但与普通的二进制搜索索引相比,随机读取速度提高了50%。

  1. table_options.index_type = rocksdb::BlockBasedTableOptions::kHashSearch;

block_size: 默认情况下,该值设置为4k。如果启用压缩,较小的块大小将导致更高的随机读取速度,因为减压开销减少了。 但是块大小不能太小。不能使压缩无效。建议将其设置为1k。

verify_checksum: 由于我们将数据存储在tmpfs中,并且非常注意读取性能,所以可能会禁用checksum。

Final thoughts 最终见解

不幸的是,优化配置RocksDB并不简单。即使我们作为RocksDB开发人员也不能完全理解每个配置更改的影响。如果您想为您的工作负载充分优化RocksDB,我们建议您进行实验和基准测试,同时关注三个放大因子。 另外,请不要犹豫在 RocksDB Developer’s Discussion Group(RocksDB开发人员的讨论组 https://www.facebook.com/groups/rocksdb.dev/)中向我们寻求帮助。