跳转至

性能与 IO 上下文可以帮助用户理解每个 DB 操作的性能瓶颈。Options.statistics 存储了从打开 DB 开始的所有线程的所有操作累积下来的统计信息。性能和 IO 统计上下文则针对每个独立的操作进行分析。

这里是性能上下文的头文件 https://github.com/facebook/rocksdb/blob/master/include/rocksdb/perf_context.h

这里是 IO 信息上下文的头文件: https://github.com/facebook/rocksdb/blob/master/include/rocksdb/iostats_context.h

他们的剖析等级受下面这个文件中的同一个方法控制: https://github.com/facebook/rocksdb/blob/master/include/rocksdb/perf_level.h

性能和 IO 上下文使用同样的机制。惟一的区别是性能上下文测量 RocksDB 的函数,而 IO 状态上下文测量 IO 相关的调用。这些功能需要在那些等待被剖析的查询被执行的线程中打开。如果剖析等级比 disable 高,rocksdb 会更新一个线程本地数据结构的计数器。查询之后,我们可以从该结构中读取计数器信息。

如何使用

这里有一个使用性能和 IO 上下文的典型的例子:

#include “rocksdb/iostat_context.h”
#include “rocksdb/perf_context.h”

rocksdb::SetPerfLevel(rocksdb::PerfLevel::kEnableTimeExceptForMutex);

rocksdb:: get_perf_context()->Reset();
rocksdb::get_iostats_context()->Reset();

... // run your query

rocksdb::SetPerfLevel(rocksdb::PerfLevel::kDisable);

... // evaluate or report variables of rocksdb::get_perf_context() and/or rocksdb::get_iostats_context()

注意同样的剖析等级会被应用于性能上下文和 IO 上下文。

你还可以调用 rocksdb::get_perf_context->ToString() 和 rocksdb::get_iostats_context->ToString() 来得到一个易读的报告。

剖析等级和开销

就跟平时一样,统计数量和开销之间总有一个权衡关系,我们设计了多个剖析等级供你选择:

  • kEnableCount 只打开计数器
  • kEnableTimeExceptForMutex 打开计数器统计和大多数时间开销统计,除了那些需要在一个共享互斥锁中调用一个函数的时间。
  • kEnableTime 进一步增加互斥锁请求和等待的时间。

kEnableCount 避免进一步的昂贵的系统时间获取开销,会带来更小的额外开销。我们通常使用这个等级测量所有的操作,并且在某些计数器不正常的时候报告他们。

使用 kEnableTimeExceptForMutex,RocksDB 的一次操作可能会调用好几次计时函数。我们通常的实践方式是在需要采样的地方打开,或者当用户需要的时候才打开。用户需要非常小心地选择采样率,因为计时函数在不同的平台的开销差异比较大。

kEnableTime 进一步允许了在共享互斥锁里面的计时,但是对一个操作的剖析可能会拖慢其他操作。当我们怀疑互斥锁是性能瓶颈的时候,我们使用这个等级来验证问题。

我们如何处理那些在某个等级上被关闭的计数器?如果一个计数器被关闭了,我们仅仅是不更新他们。

统计

我们给出一些典型的例子,讲解怎么使用这些信息来解决你的问题。我们不会介绍所有的统计信息。一个完整的对所有统计信息的描述可以再头文件中找到。

性能上下文

二分搜索开销

user_key_comparison_count 帮助我们找出一个二分搜索里面太多的比较是不是问题的根源,特别是当一个更加昂为的比较器被使用的时候。更进一步,由于比较的次数通常因 memtable 的大小,level 0 的 SST 文件的大小和其他层的大小 而有所差异,但是一个显著增加的计数器意味着有一个不合预期的 LSM 树结构。你可能需要检查落盘、压缩是不是能保持写速度。

块缓存和 OS 页缓存效率

block_cache_hit_count 告诉我们从块缓存中读取数据块的次数,block_read_count 告诉我们我们不得从文件系统中读取块的次数(不管块缓存被关闭了还是缓存未命中)。我们可以通过观察这两个值计算快缓存效率。

block_read_byte 告诉我们有多少 byte 数据是从文件系统上读取的。他可以告诉我们一个慢查询是不是因为大量块需要从文件系统读取导致。索引和 bloom 过滤块通常是巨大的块。一个巨大的块也可以因为有一个非常大键值对。

在非常多 RocksDB 的设置中,我们依赖 OS 的页缓存来减少设备 IO。事实上,在多数通用硬件设置上,我们建议用户把 OS 的页缓存设置到足够大来保存除了最底层以外的所有数据,这样我们可以限制一个读查询在一个 IO 内完成。在这种设定下,我们会发起多个文件系统调用,但是只有一个会真正读取设备。为了验证是否如此,我们可以使用计数器 block_read_time 来检查花费在从文件系统读取块的次数是不是如我们预期的一样。

墓碑

当删除一个 key,RocksDB 仅仅是加一个成为墓碑的标记,到 memtable。在我们真正把包含有这个墓碑的 key 的文件压缩之前,原始的 key 都不会被删除。墓碑可能在原始的值被删除之后都还存在。所以如果我们有大量的连续的 key 被删除,一个用户可能在迭代通过这些墓碑的时候遇到慢查询。计数器 internal_delete_skipped_count 告诉我们有多少个墓碑被我们跳过了。internal_key_skipped_count 则覆盖其他被我们跳过的 key。

Get 步骤

我们可以使用 "get_*" 统计在一个 Get 查询中的步骤。最重要的两个是 get_from_memtable_time 和 get_from_output_files_time。计数器告诉我们这个慢查询是不是因为 memtable,SST 文件,或者两者都有的原因导致。seek_on_memtable_time 可以告诉我们花费在搜索 memtable 的时间。

写步骤

"write_*" 统计写操作过程中的写步骤。write_wal_time,write_memtable_time 和 write_delay_time 告诉我们花费在写 WAL,memtable 的时间,或者出在减速激活状态。write_pre_and_post_process_time 主要意味着花费在写队列的等待时间。如果写操作被赋值给一个提交组,但是他不是组长,write_pre_and_post_process_time 会包括等待组提交组长的时间。

迭代操作步骤

"seek_*" 和 find_next_user_entry_time 把迭代操作分步。最有趣的一个是 seek_child_seek_count,他告诉我们有多少子迭代,通常也就是 LSM 树的排序结果数量。

IO 统计上下文

我们有计数器来统计在主要文件系统调用的时间花费。如果你发现写路径有问题,写相关的计数器通常更有趣。他告诉我们,我们因为哪个文件系统调用而变慢,或者他不是因为文件系统调用导致的