RocksDB学习笔记(三) -- RocksDB中的一些特性设计和高性能相关机制
通过官方博客了解RocksDB的一些特性设计,并梳理下高性能相关的机制。
1. 引言
前两篇中梳理了RocksDB的总体结构和基本流程,本篇学习 RocksDB Blog 中的部分文章,了解下RocksDB的一些特性设计,并梳理RocksDB中的相关高性能机制。
此外,也梳理下其他C++和Linux相关的一些高性能机制,比如 coroutine 和 io_uring。
1、官网博客文章(按时间顺序,早期文章在前面):
- Indexing SST Files for Better Lookup Performance
- Reducing Lock Contention in RocksDB
- Improving Point-Lookup Using Data Block Hash Index
- RocksDB Secondary Cache
- Asynchronous IO in RocksDB
- Reduce Write Amplification by Aligning Compaction Output File Boundaries
2、RocksDB调优(当做文章索引,后续按需检索)
- RocksDB Tuning Guide
- 还有wiki里面
Performance
分组下的一些文章,比如 Write Stalls
- 还有wiki里面
- Rocksdb 调优指南
3、另外的一些性能相关博客和文章:
- z_stand – RocksDB相关博客文章
- 作者对RocksDB的一些模块梳理和公开论文笔记值得一看,可作为后续参考
- Rocksdb加SPDK改善吞吐能力建设
- 暂不展开,Ceph中再看
- 从 C++20 协程,到 Asio 的协程适配
- PS:发现之前也看过该博主相关文章,梳理存储io栈 时看过的 Linux 内核的 blk-mq(Block IO 层多队列)机制
- 还是上面博客中的一些文章
说明:本博客作为个人学习实践笔记,可供参考但非系统教程,可能存在错误或遗漏,欢迎指正。若需系统学习,建议参考原链接。
2. RocksDB相关博客
2.1. 索引SST文件以提升查找性能
1、可以先了解下 LevelDB
中的SST
文件结构(可见:LevelDB学习笔记(五) – sstable实现):
出处,在此基础上添加说明
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 version(
SuperVersion
类) 来整合memtable
、immutable memtables
和version
的引用计数,避免需频繁各自加锁进行引用计数的增减 - 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 cache
和table cache
)中的锁处理:- 读查询时,引入 旁路(bypass)table cache 模式,通过
options.max_open_files=-1
开启该模式,并由用户负责SST
文件的缓存和读取,而不通过LRU缓存 - 对于内存文件系统(ramfs/tmpfs),则引入 PlainTable Format 格式来优化
SST
,不通过block
的方式组织数据,当然也没有block cache
- 读查询时,引入 旁路(bypass)table cache 模式,通过
经过上述优化后,锁不再是瓶颈。内存负载下的性能数据,可见:RocksDB In Memory Workload Performance Benchmarks。
2.3. RocksDB中的异步IO
Iterator
和 MultiGet
中利用异步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有高度的灵活性和可配置性,另外随着这些年的发展,自身也有很高的自适应性。
- 指南中建议:如果是运行在
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中的基本设计
- 公开演讲:Talks
- 论文:Publication
3.1. RocksDB统计
RocksDB统计信息非常全面,单独一个小节特别说明下。(相比而言,自己前段时间刚进行一次项目的性能测试,出现瓶颈后发现缺乏各种指标!部分节点安装使用eBPF都费劲,还需要重新编译内核和配套的多个驱动)
statistics
介绍和使用,见:Statisticscompaction
和db
状态,见:Compaction Stats and DB Status- RocksDB会定期(
stats_dump_period_sec
配置项)dump统计信息到日志文件里 - 也可以手动获取:
db->GetProperty("rocksdb.stats")
- RocksDB会定期(
- Perf 上下文和 IO 统计数据上下文,见:Perf Context and IO Stats Context
- 包含
iostats_context.h
和perf_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
进行调优
- Perf Context的
- 硬盘读取IOPS:注意,硬件能一直持续的稳定
IOPS
规格常常会比硬件厂商提供的spec规格更低。建议使用工具(如fio
)或系统指标进行基准规格测试。- 如果IOPS已达到硬件基准规格的饱和值,则去检查
compaction
- 并尝试提高
block cache
的缓存命中率 - 问题的可能原因:读取的索引、过滤器、大block,不同原因处理的方式也不一样
- 如果IOPS已达到硬件基准规格的饱和值,则去检查
- CPU:通常受
compaction
的读取路径影响- 有很多会影响CPU的参数选项,比如:compaction, compression, bloom filters, block size
- Space(空间):在技术上一般不是瓶颈,但是当系统指标未达到饱和、RocksDB性能足够好且已经几乎填满SSD空间时,通常会说性能受到空间的瓶颈影响。
- 空间效率的调优,可见:Space Tuning
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
- SSD远未饱和,但受到
- 无法快速写入(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。