块缓存

块缓存是RocksDB在内存中缓存数据以便读取的地方。用户可以将缓存对象传递给具有所需容量(大小)的RocksDB实例。 一个缓存对象可以由同一进程中的多个RocksDB实例共享,从而允许用户控制整个缓存容量。块缓存存储未压缩的块。 用户可以选择设置第二个存储压缩块的块缓存。读取将首先从未压缩的块缓存中获取数据块,然后从压缩的块缓存中获取数据块。 如果使用直接io,压缩块缓存可以替代OS页面缓存。

在RocksDB中有两种缓存实现,即LRUCache和ClockCache。这两种类型的缓存都被分片以减少锁争用。 容量平均分配给每个碎片,而碎片不共享容量。默认情况下,每个缓存最多分成64个碎片,每个碎片的容量不少于512k字节。

用法

开箱即用,RocksDB将使用基于lru的块缓存实现,容量为8MB。要设置自定义块缓存,请调用NewLRUCache()或NewClockCache()创建缓存对象,并将其设置为基于表选项的块。 用户还可以通过实现缓存接口来拥有自己的缓存实现

  1. std::shared_ptr<Cache> cache = NewLRUCache(capacity);
  2. BlockBasedTableOptions table_options;
  3. table_options.block_cache = cache;
  4. Options options;
  5. options.table_factory.reset(new BlockBasedTableFactory(table_options));

要设置压缩块缓存:

  1. table_options.block_cache_compressed = another_cache;

如果block_cache被设置为nullptr,那么RocksDB将创建默认的块缓存。要完全禁用块缓存:

  1. table_options.no_block_cache = true;

LRU Cache LRU缓存

开箱即用,RocksDB将使用基于lru的块缓存实现,容量为8MB。缓存的每个碎片都维护自己的LRU列表和自己的哈希表来进行查找。 同步是通过每个片的互斥量来完成的。查找和插入缓存都需要碎片的锁定互斥锁。用户可以通过调用NewLRUCache()来创建LRU缓存。 该函数提供了几个有用的选项来设置缓存:

  • capacity: 缓存的总大小。
  • num_shard_bits: 缓存键中用作切分id的位的数量。缓存将被切分为2^num_shard_bits切分。
  • strict_capacity_limit: 在极少数情况下,块缓存大小可能超过其容量。这是指在块缓存中对DB pin块进行读取或迭代时,固定块的总大小超过容量。如果有进一步的读取尝试插入块到块缓存中,如果strict_capacity_limit=false(默认值),缓存将不能遵守其容量限制并允许插入。如果主机没有足够的内存,这可能会造成不必要的OOM错误,导致数据库崩溃。将该选项设置为true将拒绝进一步插入缓存,并导致读取或迭代失败。该选项基于每个碎片工作,这意味着一个碎片可能在插入已满时拒绝插入,而另一个碎片仍然有额外的未固定空间。
  • high_pri_pool_ratio: 保留给高优先级块的容量比率。有关更多信息,请参见下面的缓存索引和筛选块一节。(https://github.com/facebook/rocksdb/wiki/Block-Cache#caching-index-and-filter-blocks)

Clock Cache 时钟缓存

ClockCache实现时钟算法。每个时钟缓存碎片都维护一个缓存条目的循环列表。 时钟句柄在循环列表上运行,寻找要驱逐的未固定条目,但如果自上次扫描以来一直使用该条目,则每个条目都有第二次机会留在缓存中。tbb::concurrent_hash_map用于查找。

与LRUCache相比,它的优点是具有更细粒度的锁定。对于LRU缓存,每个碎片互斥锁甚至在查找时也必须被锁定,因为它需要更新它的LRU-list。 从时钟缓存中查找不需要对每个切分互斥锁进行锁定,只需要查找具有细粒度锁定的并发散列映射。只有insert才需要锁定每个片的互斥锁。 使用时钟缓存,我们可以在心满意足的环境中看到LRU缓存上读取吞吐量的提高(请参阅cache/clock_cache.cc中的内线注释)。基准设定):

  1. Threads Cache Cache ClockCache LRUCache
  2. Size Index/Filter Throughput(MB/s) Hit Throughput(MB/s) Hit
  3. 32 2GB yes 466.7 85.9% 433.7 86.5%
  4. 32 2GB no 529.9 72.7% 532.7 73.9%
  5. 32 64GB yes 649.9 99.9% 507.9 99.9%
  6. 32 64GB no 740.4 99.9% 662.8 99.9%
  7. 16 2GB yes 278.4 85.9% 283.4 86.5%
  8. 16 2GB no 318.6 72.7% 335.8 73.9%
  9. 16 64GB yes 391.9 99.9% 353.3 99.9%
  10. 16 64GB no 433.8 99.8% 419.4 99.8%

要创建时钟缓存,请调用NewClockCache()。为了使时钟缓存可用,需要将RocksDB链接到Intel TBB库(https://www.threadingbuildingblocks.org/)。 同样,在创建时钟缓存时,用户可以设置几个选项:

  • capacity: 与LRUCache相同
  • num_shard_bits: 与LRUCache相同
  • strict_capacity_limit: 与LRUCache相同

Caching Index and Filter Blocks 缓存索引和筛选块

默认情况下,索引和筛选块缓存在块缓存之外,用户无法控制应该使用多少内存来缓存这些块,只能设置max_open_files。 用户可以选择在块缓存中缓存索引和过滤块,从而更好地控制RocksDB使用的内存。将索引和筛选块缓存在块缓存中:

  1. BlockBasedTableOptions table_options;
  2. table_options.cache_index_and_filter_blocks = true;

通过将索引块和筛选块放在块缓存中,这些块必须与数据块竞争以保持在缓存中。尽管索引和筛选器块的访问比数据块更频繁,但是在某些情况下,这些块可能会发生抖动。这是不需要的,因为索引和筛选块往往比数据块大得多,而且它们通常在缓存中具有更高的值。有两个选项可以调优以缓解这个问题:

  • cache_index_and_filter_blocks_with_high_priority: 将块缓存中的索引和筛选块的优先级设置为high。到目前为止,它只影响LRUCache,并且在调用NewLRUCache()时需要与high_pri_pool_ratio一起使用。
    1. 如果启用了该特性,LRU缓存中的LRU-list将被分成两部分,一部分用于高pri块,另一部分用于低pri块。数据块将被插入到低优先级池的头部。
    2. 索引和过滤器块将插入到高优先级池的头部。如果high-pri池中的总使用量超过*capacity * high_pri_pool_ratio*,那么high-pri池尾部的块将溢出到low-pri池的头部,之后它将与数据块竞争以保持缓存。驱逐将从低优先级资源池的尾部开始。
  • pin_l0_filter_and_index_blocks_in_cache: Pin level-0文件的索引和筛选块在块缓存中,以避免他们被驱逐。0级索引和过滤器通常访问得更频繁。而且它们的大小更小,所以希望将它们固定在缓存中不会消耗太多的容量。

Simulated Cache 模拟缓存

SimCache是一个实用程序,当缓存容量或碎片数量发生变化时,它可以预测缓存命中率。它封装了DB正在使用的实际缓存对象,并运行一个模拟给定容量和碎片数量的影子LRU缓存,并测量影子缓存的缓存命中率和命中率。 当用户想要打开一个缓存大小为4GB的数据库,但是想知道如果缓存大小扩展到64GB,缓存命中率会变成多少时,这个实用程序非常有用。要创建模拟缓存:

  1. // This cache is the actual cache use by the DB.
  2. std::shared_ptr<Cache> cache = NewLRUCache(capacity);
  3. // This is the simulated cache.
  4. std::shared_ptr<Cache> sim_cache = NewSimCache(cache, sim_capacity, sim_num_shard_bits);
  5. BlockBasedTableOptions table_options;
  6. table_options.block_cache = sim_cache;

模拟缓存的额外内存开销小于sim_capacity的2%。

Statistics 统计数据

可以通过选项访问块缓存计数器列表。如果它是non-null非空的统计信息。

  1. // total block cache misses
  2. // REQUIRES: BLOCK_CACHE_MISS == BLOCK_CACHE_INDEX_MISS +
  3. // BLOCK_CACHE_FILTER_MISS +
  4. // BLOCK_CACHE_DATA_MISS;
  5. BLOCK_CACHE_MISS = 0,
  6. // total block cache hit
  7. // REQUIRES: BLOCK_CACHE_HIT == BLOCK_CACHE_INDEX_HIT +
  8. // BLOCK_CACHE_FILTER_HIT +
  9. // BLOCK_CACHE_DATA_HIT;
  10. BLOCK_CACHE_HIT,
  11. // # of blocks added to block cache.
  12. BLOCK_CACHE_ADD,
  13. // # of failures when adding blocks to block cache.
  14. BLOCK_CACHE_ADD_FAILURES,
  15. // # of times cache miss when accessing index block from block cache.
  16. BLOCK_CACHE_INDEX_MISS,
  17. // # of times cache hit when accessing index block from block cache.
  18. BLOCK_CACHE_INDEX_HIT,
  19. // # of times cache miss when accessing filter block from block cache.
  20. BLOCK_CACHE_FILTER_MISS,
  21. // # of times cache hit when accessing filter block from block cache.
  22. BLOCK_CACHE_FILTER_HIT,
  23. // # of times cache miss when accessing data block from block cache.
  24. BLOCK_CACHE_DATA_MISS,
  25. // # of times cache hit when accessing data block from block cache.
  26. BLOCK_CACHE_DATA_HIT,
  27. // # of bytes read from cache.
  28. BLOCK_CACHE_BYTES_READ,
  29. // # of bytes written into cache.
  30. BLOCK_CACHE_BYTES_WRITE,

参见:Memory-usage-in-RocksDB#block-cache(https://github.com/facebook/rocksdb/wiki/Memory-usage-in-RocksDB#block-cache)