Perf上下文和IO Stats上下文

Perf上下文和IO Stats上下文可以帮助我们理解单个DB操作的性能瓶颈。 Options.statistics存储自数据库打开以来来自所有线程的所有操作的累积统计信息。 Perf上下文和IO Stat上下文查看单个操作。

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

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

这两个概要文件的级别由这个头文件中的同一个函数控制: https://github.com/facebook/rocksdb/blob/master/include/rocksdb/perf_level.h

Perf上下文和IO Stats上下文使用相同的机制。唯一的区别是,Perf上下文度量RocksDB的函数,而IO统计上下文度量I/O相关调用。 需要在执行要概要文件的查询的线程中启用这些特性。如果配置文件级别高于disable,则RocksDB将更新线程本地数据结构中的计数器。 查询之后,我们可以从结构中读取计数器。

如何使用它们

下面是一个使用Perf上下文和IO Stats上下文的典型例子:

  1. #include rocksdb/iostat_context.h
  2. #include rocksdb/perf_context.h
  3. rocksdb::SetPerfLevel(rocksdb::PerfLevel::kEnableTimeExceptForMutex);
  4. rocksdb::get_perf_context()->Reset();
  5. rocksdb::get_iostats_context()->Reset();
  6. ... // run your query
  7. rocksdb::SetPerfLevel(rocksdb::PerfLevel::kDisable);
  8. ... // evaluate or report variables of rocksdb::get_perf_context() and/or rocksdb::get_iostats_context()

注意,相同的perf级别同时应用于perf上下文和IO Stats上下文。

您还可以调用rocksdb::get_perf_context->ToString()和rocksdb::get_iostats_context->ToString()来获得人类可读的报告。

配置文件级别和成本

和往常一样,统计量和开销之间也有权衡,所以我们设计了几个概要文件级别可供选择:

  • kEnableCount将只启用计数器。
  • 引入kEnableTimeAndCPUTimeExceptForMutex是因为引入了CPU时间计数器。使用此级别,将启用非互斥定时相关计数器以及CPU计数器。
  • kEnableTimeExceptForMutex启用计数器状态和大多数持续时间的状态,除非需要在共享互斥对象中调用计时函数。
  • kEnableTime进一步添加了互斥量获取和等待时间的统计信息。

kEnableCount避免了更昂贵的获取系统时间的调用,从而降低了开销。我们经常使用这个级别度量所有操作,并在特定计数器异常时报告它们。

使用kEnableTimeExceptForMutex, RocksDB可以为一个操作调用计时函数数十次。我们的常见做法是使用采样操作或用户请求时打开它。 用户在选择采样率时需要谨慎,因为不同平台的计时功能的成本不同。

kEnableTime还允许在共享互斥锁中计时,但是分析一个操作可能会减慢其他操作。 当我们怀疑互斥锁争用是性能瓶颈时,我们使用这个级别来验证问题。

我们如何处理一个级别中禁用的计数器?如果一个计数器被禁用,它将不会被更新。

Stats 状态

我们将给出一些典型的例子来说明如何使用这些统计数据来解决您的问题。我们没有在这里介绍所有的统计数据。所有统计数据的完整描述可以在头文件中找到。

性能环境

二进制搜索成本

user_key_comparison_count帮助我们确定在二进制搜索中是否存在比较过多的问题,特别是在使用更昂贵的比较器时。 此外,由于比较的数量通常基于memtable大小、第0级的SST文件大小和其他级别的大小是一致的,计数器的显著增加可以表明意外的LSM-tree形状。 您可能想要检查flush/compaction是否能够跟上写入速度。

块缓存和OS页面缓存效率

block_cache_hit_count告诉我们从块缓存中读取数据块的次数,而block_read_count告诉我们必须从文件系统中读取块的次数(块缓存被禁用或缓存丢失)。 我们可以通过观察两个计数器来评估块缓存效率。

block_read_byte告诉我们从文件系统中读取的总字节数。它可以告诉我们,从文件系统中读取大块是否会导致查询变慢。 索引块和布鲁姆过滤器块通常是大块。大块也可以是非常大的键或值的结果。

在RocksDB的许多设置中,我们依赖于OS页面缓存来减少来自设备的I/O。事实上,在最常见的硬件设置中,我们建议用户调优OS页面缓存大小,使其足够大,可以容纳除最后一层之外的所有数据,这样我们就可以限制就绪查询发出的I/O不超过一个。 有了这个设置,我们将发出多个文件系统调用,但其中最多只有一个是从设备读取的。为了验证是否存在这种情况,我们可以使用计数器block_read_time来查看是否期望从文件系统中读取块所花费的总时间。

Tombstones 墓碑

删除键时,RocksDB只是将一个名为tombstone的标记放到memtable中。在用tombstone压缩包含key的文件之前,不会删除密钥的原始值。 甚至在删除原始值之后,墓碑的寿命也可能更长。因此,如果我们删除了许多连续的键,用户在遍历这些tombstone时可能会感到速度很慢。 计数器internal_delete_skipped_count告诉我们跳过了多少个墓碑。internal_key_skipped_count包含我们跳过的一些其他键。

Get()崩溃

我们可以使用”get_*”统计信息来分解Get()查询中的时间。最重要的两个是get_from_memtable_time和get_from_output_files_time。 计数器告诉我们慢度是由memtable、SST表造成的,还是两者都造成的。 seek_on_memtable_time可以告诉我们在查找memtables上花费了多少时间

Write崩溃

“write_*”统计数据分解了写操作。write_wal_time、write_memtable_time和write_delay_time告诉我们花在写WAL、memtable或active slow-down上的时间。 write_pre_and_post_process_time主要表示在写入器队列中等待的时间。如果写被分配给一个提交组,但它不是组长, write_pre_and_post_process_time也将包括等待组长完成提交的时间。

Iterator操作崩溃

“seek_*”和”find_next_user_entry_time”分解了迭代器操作。 最有趣的一个是seek_child_seek_count。它告诉我们有多少子迭代器,这主要是指LSM树中排序后的运行次数。

每层崩溃

为了更深入地了解LSM树结构的性能含义,引入了每级PerfContext (PerfContextByLevel)来按级别分解计数器。 添加level_to_perf_context是为了维护从级别号到PerfContextByLevel对象的映射。用户可以通过调用*(rocksdb::get_perf_context()->level_to_perf_context) [level_number]直接访问它。

默认情况下,每个级别的PerfContext是禁用的,可以调用EnablePerLevelPerfContext()、DisablePerLevelPerfContext()和ClearPerLevelPerfContext来enable/disable/clear它。 启用每级性能上下文时,rocksdb::get_perf_context->ToString()还将以

  1. bloom_filter_useful = 1@level5, 2@level7

这意味着bloom_filter_useful在第5级增加了一次,在第7级增加了两次。

输入输出数据上下文

我们有用于主要文件系统调用的时间计数器。如果在写路径中看到摊位,那么与写相关的计数器看起来更有趣。 它告诉我们,慢是因为文件系统操作,或者不是由文件系统调用引起的。