Post

RocksDB学习笔记(三) -- RocksDB中的一些特性设计和高性能相关机制

通过官方博客了解RocksDB的一些特性设计,并梳理下高性能相关的机制。

RocksDB学习笔记(三) -- RocksDB中的一些特性设计和高性能相关机制

1. 引言

前两篇中梳理了RocksDB的总体结构和基本流程,本篇学习 RocksDB Blog 中的部分文章,了解下RocksDB的一些特性设计,并梳理RocksDB中的相关高性能机制。

此外,也梳理下其他C++和Linux相关的一些高性能机制,比如 coroutineio_uring

1、官网博客文章(按时间顺序,早期文章在前面):

2、RocksDB调优(当做文章索引,后续按需检索)

3、另外的一些性能相关博客和文章:

说明:本博客作为个人学习实践笔记,可供参考但非系统教程,可能存在错误或遗漏,欢迎指正。若需系统学习,建议参考原链接。

2. RocksDB相关博客

2.1. 索引SST文件以提升查找性能

1、可以先了解下 LevelDB中的SST文件结构(可见:LevelDB学习笔记(五) – sstable实现):

leveldb-sstable-overview
出处,在此基础上添加说明

2、RocksDB中的SST结构:

除了meta block 2对应的index block的位置不同外,RocksDB中还新增了另外几种元数据:3-compression dictionary block、4-range deletion block、5-stats block,以及预留的元数据block扩展。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<beginning_of_file>
[data block 1]
[data block 2]
...
[data block N]
[meta block 1: filter block]                  (see section: "filter" Meta Block)
[meta block 2: index block]
[meta block 3: compression dictionary block]  (see section: "compression dictionary" Meta Block)
[meta block 4: range deletion block]          (see section: "range deletion" Meta Block)
[meta block 5: stats block]                   (see section: "properties" Meta Block)
...
[meta block K: future extended block]  (we may add more meta blocks in the future)
[metaindex block]
[Footer]                               (fixed size; starts at file_size - sizeof(Footer))
<end_of_file>

3、Indexing SST Files for Better Lookup Performance 该篇文章中的设计:

  • 由于各SST文件的数据范围是有序的,本层文件和其下一层文件的相对位置不会变化
  • 那么可以在SST文件进行compaction时,对每个文件预先构造2个指向下一级SST文件的指针,分别指向下一层中左侧和右侧SST文件(并作为SST文件的一部分),用来加速二分查找
    • 这样就避免了本层没找到符合条件的key时,下一层再对所有SST文件再做二分查找
  • 这个设计方式类似分数级联,可了解 Fractional cascading

比如下面要查找key为80的记录,level1中没有符合的SST,找level2中处于level1里file1左侧的3个文件即可:

1
2
3
4
5
6
7
8
                                         file 1                                          file 2
                                      +----------+                                    +----------+
level 1:                              | 100, 200 |                                    | 300, 400 |
                                      +----------+                                    +----------+
           file 1     file 2      file 3      file 4       file 5       file 6       file 7       file 8
         +--------+ +--------+ +---------+ +----------+ +----------+ +----------+ +----------+ +----------+
level 2: | 40, 50 | | 60, 70 | | 95, 110 | | 150, 160 | | 210, 230 | | 290, 300 | | 310, 320 | | 410, 450 |
         +--------+ +--------+ +---------+ +----------+ +----------+ +----------+ +----------+ +----------+

2.2. 减少锁竞争

Reducing Lock Contention in RocksDB

RocksDB在内存态使用时,更容易成为瓶颈。RocksDB中的一些优化锁使用的措施:

  • 1、对于操作,引入 super versionSuperVersion类) 来整合memtableimmutable memtablesversion的引用计数,避免需频繁各自加锁进行引用计数的增减
  • 2、使用 std::atomic 替换了部分 引用计数(通常需要锁保护),减少 mutex 的使用
  • 3、在查询中,获取super version和引用计数是 lock-free 操作
  • 4、避免在mutex中进行磁盘IO,包含事务日志、日志信息打印。事务日志记录调整到锁外;日志信息打印则先记录到log buffer,内容中带时间戳,再在 锁外延迟写
  • 5、减少在mutex中进行对象创建。在RocksDB的部分场景中,对象创建会涉及malloc操作,需要lock一些共享数据结构
    • std::vector内部会使用malloc,RocksDB中引入了 autovector,其中会预分配一些数据,很适合操作元数据信息
    • 创建iterator通常需要加锁且涉及malloc、合并iterator还可能涉及排序,开销比较昂贵。RocksDB中调整为仅增加引用计数,并在iterator创建前就释放锁
  • 6、LRU缓存(用于block cachetable cache)中的锁处理:
    • 读查询时,引入 旁路(bypass)table cache 模式,通过options.max_open_files=-1开启该模式,并由用户负责SST文件的缓存和读取,而不通过LRU缓存
    • 对于内存文件系统(ramfs/tmpfs),则引入 PlainTable Format 格式来优化SST,不通过block的方式组织数据,当然也没有block cache

经过上述优化后,锁不再是瓶颈。内存负载下的性能数据,可见:RocksDB In Memory Workload Performance Benchmarks

2.3. RocksDB中的异步IO

Asynchronous IO in RocksDB

IteratorMultiGet 中利用异步IO优化性能。ReadOptions中新增了 async_io选项,使用FSRandomAccessFile::ReadAsync接口进行异步读

注意,6.15.5 分支还不包含这部分,下面基于v7.9.2分支查看代码。
从 HISTORY.md 中可看到是 7.0.0 (02/20/2022) 中开始新增的 ReadAsync

这里看下MultiGet,其中会调用到TableReader::MultiGet(读取SST文件),基于 协程(coroutine) 实现(Facebook的folly库)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// rocksdb_v7.9.2/table/table_reader.h
class TableReader {
  ...
  // 纯虚函数,需要实现类实现
  virtual Status Get(const ReadOptions& readOptions, const Slice& key,
                     GetContext* get_context,
                     const SliceTransform* prefix_extractor,
                     bool skip_filters = false) = 0;

  virtual void MultiGet(const ReadOptions& readOptions,
                        const MultiGetContext::Range* mget_range,
                        const SliceTransform* prefix_extractor,
                        bool skip_filters = false) {
    for (auto iter = mget_range->begin(); iter != mget_range->end(); ++iter) {
      *iter->s = Get(readOptions, iter->ikey, iter->get_context,
                     prefix_extractor, skip_filters);
    }
  }

#if USE_COROUTINES
  virtual folly::coro::Task<void> MultiGetCoroutine(
      const ReadOptions& readOptions, const MultiGetContext::Range* mget_range,
      const SliceTransform* prefix_extractor, bool skip_filters = false) {
    MultiGet(readOptions, mget_range, prefix_extractor, skip_filters);
    co_return;
  }
#endif  // USE_COROUTINES
  ...
};

上面的TableReader定义了一个抽象类,可看下block结构组织场景下的实现类:BlockBasedTable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// rocksdb_v7.9.2/table/block_based/block_based_table_reader.h
class BlockBasedTable : public TableReader {
  ...
  // Get 实现
  Status Get(const ReadOptions& readOptions, const Slice& key,
             GetContext* get_context, const SliceTransform* prefix_extractor,
             bool skip_filters = false) override;
  // MultiGet 重载
  DECLARE_SYNC_AND_ASYNC_OVERRIDE(void, MultiGet,
                                  const ReadOptions& readOptions,
                                  const MultiGetContext::Range* mget_range,
                                  const SliceTransform* prefix_extractor,
                                  bool skip_filters = false);
  ...
};

block_based_table_reader.cc中默认MultiGet实现是同步读SST,即async_read参数是false。RocksDB中通过 block_based_table_reader_sync_and_async.h 里面的宏开关,控制对MultiGet的重载,以支持async_read为true的版本。

1
2
3
4
5
6
7
8
9
10
11
// Generate the regular and coroutine versions of some methods by
// including block_based_table_reader_sync_and_async.h twice
// Macros in the header will expand differently based on whether
// WITH_COROUTINES or WITHOUT_COROUTINES is defined
// clang-format off
#define WITHOUT_COROUTINES
#include "table/block_based/block_based_table_reader_sync_and_async.h"
#undef WITHOUT_COROUTINES
#define WITH_COROUTINES
#include "table/block_based/block_based_table_reader_sync_and_async.h"
#undef WITH_COROUTINES
1
2
3
4
5
6
// rocksdb_v7.9.2/table/block_based/block_based_table_reader_sync_and_async.h
DEFINE_SYNC_AND_ASYNC(void, BlockBasedTable::MultiGet)
(const ReadOptions& read_options, const MultiGetRange* mget_range,
 const SliceTransform* prefix_extractor, bool skip_filters) {
    ...
}

3. RocksDB调优指南

RocksDB Tuning Guide

RocksDB有高度的灵活性和可配置性,另外随着这些年的发展,自身也有很高的自适应性

  • 指南中建议:如果是运行在SSD上的普通应用程序,不建议再去调优。
  • 可调整设置的参数:Setup Options and Basic Tuning
    • 除非碰到明显的性能问题,否则也不建议做调整,大部分保持默认参数即可
    • 参数示例,size_t write_buffer_size = 64 << 20;ColumnFamilyOptions中):列族Writer Buffer,默认64MB,侧重单个memtable;size_t db_write_buffer_size = 0;,构建在memtable中写入硬盘前跨所有列族的数据,0表示不限制
  • 理解RocksDB中的基本设计

3.1. RocksDB统计

RocksDB统计信息非常全面,单独一个小节特别说明下。(相比而言,自己前段时间刚进行一次项目的性能测试,出现瓶颈后发现缺乏各种指标!部分节点安装使用eBPF都费劲,还需要重新编译内核和配套的多个驱动)

  • statistics介绍和使用,见:Statistics
  • compactiondb状态,见:Compaction Stats and DB Status
    • RocksDB会定期(stats_dump_period_sec配置项)dump统计信息到日志文件里
    • 也可以手动获取:db->GetProperty("rocksdb.stats")
  • Perf 上下文和 IO 统计数据上下文,见:Perf Context and IO Stats Context
    • 包含iostats_context.hperf_context.h,头文件中各自介绍了有哪些指标字段
    • 其使用示例,也可见 这篇文章

统计使用示例(完整代码可见 这里):

1
2
3
4
5
6
7
8
9
10
11
    rocksdb::Options options;
    // 创建统计
    options.statistics = rocksdb::CreateDBStatistics();
    // 默认等级是kExceptDetailedTimers,不统计锁和压缩的耗时
    // options.statistics->set_stats_level(rocksdb::StatsLevel::kAll);
    ...
    // 打印统计信息
    // 直方图,支持很多种类型,由传入参数指定,此处为获取耗时
    std::cout << "histgram:\n" << options.statistics->getHistogramString(rocksdb::Histograms::DB_GET) << std::endl;
    // 统计字段
    std::cout << "statistics:\n" << options.statistics->ToString() << std::endl;

执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[CentOS-root@xdlinux ➜ rocksdb git:(main)]$ ./test_rocksdb_ops      
====== begin... =====
key:xdkey1, value:test12345
put key:xdkey2 value:test12345
delete key:xdkey1
get key:xdkey2, value:test12345
histgram:
Count: 2 Average: 59.0000  StdDev: 58.00
Min: 1  Median: 1.0000  Max: 117
Percentiles: P50: 1.00 P75: 117.00 P99: 117.00 P99.9: 117.00 P99.99: 117.00
------------------------------------------------------
[       0,       1 ]        1  50.000%  50.000% ##########
(     110,     170 ]        1  50.000% 100.000% ##########

statistics:
rocksdb.block.cache.miss COUNT : 0
rocksdb.block.cache.hit COUNT : 0
rocksdb.block.cache.add COUNT : 0
rocksdb.block.cache.add.failures COUNT : 0
rocksdb.block.cache.index.miss COUNT : 0
...

3.2. 可能的性能瓶颈

调优指南里的 Possibilities of Performance Bottlenecks 小节,可以作为定位类似性能问题的思路。

3.2.1. 系统指标达到饱和

一些场景下性能受到限制是由于 系统指标达到饱和了,但又是非预期的情况,调优时需要判断这些指标是否使用率偏高。

  • 硬盘写入带宽:RocksDB的compaction过程会写SST到硬盘,写入时可能超出硬盘驱动器的负载能力,表现为写入停滞/延迟(Write Stall),还可能导致读取变慢
    • Perf Context的read相关指标,会显示当前是否有过多SST文件在读取,出现时考虑对compaction进行调优
  • 硬盘读取IOPS:注意,硬件能一直持续的稳定IOPS规格常常会比硬件厂商提供的spec规格更低。建议使用工具(如fio)或系统指标进行基准规格测试。
    • 如果IOPS已达到硬件基准规格的饱和值,则去检查compaction
    • 并尝试提高block cache的缓存命中率
    • 问题的可能原因:读取的索引、过滤器、大block,不同原因处理的方式也不一样
  • CPU:通常受compaction的读取路径影响
    • 有很多会影响CPU的参数选项,比如:compaction, compression, bloom filters, block size
  • Space(空间):在技术上一般不是瓶颈,但是当系统指标未达到饱和、RocksDB性能足够好且已经几乎填满SSD空间时,通常会说性能受到空间的瓶颈影响。

3.2.2. 放大因子

三种放大:write amplification, read amplification and space amplification

应该优化哪种放大因素,有时是比较明显的,但有时又不明显,无论哪种情况,compaction都是三者间做trade-off的关键因素。

1、写放大:写入数据库的数据大小 vs 写入磁盘的数据大小,比如写10MB/s到数据库,但写入硬盘30MB/s,则写放大是3倍。

观察写放大的2种方式:

  • 1)DB::GetProperty("rocksdb.stats", &stats) 获取
  • 2)自行计算:硬盘带宽(iostat统计) 除以 数据库写入速率

2、读放大:每次查询时硬盘的读取数据量,比如单次查询需要5个page,则读放大是5倍。

  • 逻辑读:从缓存读取,RocksDB的 block cache 或者 操作系统的 page cache
  • 物理读:从硬盘读

3、空间放大:数据库文件在硬盘上的大小 和 数据大小 的比值,比如插入10MB数据到数据库,但硬盘上用了100MB,则空间放大为10倍。

  • 通常需要设置一个硬性的空间使用限制,避免写爆空间(HDD或者SSD或者内存态),可见 Space Tuning 中减小空间放大的调优指导

3.2.3. 系统未达饱和但RocksDB慢

有时系统指标未达饱和,但RocksDB速率不及用户预期。有一些可能的场景:

  • compaction不够快
    • SSD远未饱和,但受到compaction允许的资源最大使用配置的限制,或者并发限制
    • 可参考:Parallelism options
  • 无法快速写入(Cannot Write Fast Enough)
    • 写入的问题通常是由于写IO的瓶颈,用户可以尝试无序写、手动刷WAL、多数据库共享、并行写入
  • 有时只是想要更低的读延迟(Demand Lower Read Latency )
    • 有时没什么问题,但用户只希望读取延迟更低
    • 可通过 Perf Context and IO Stats Context 检查每次的查询状态(query status),看是CPU 还是 I/O比较耗费时间,再调整相应选项

还有一些其他因素,暂不展开,后续可见参考链接。

4. 小结

梳理了RocksDB官方博客的一些文章,包括:索引SST文件以提升查找性能、减少锁竞争、异步IO优化读性能,并简单实验查看了RocksDB统计信息。

并梳理学习RocksDB调优指南,分析了一些性能问题场景的可能原因和调优思路。

文章中涉及的一些机制当前未深入梳理代码实现,包括其中的协程和io_uring使用,TODO。

5. 参考

This post is licensed under CC BY 4.0 by the author.