这里我们尝试解释一下RocksDB是如何使用内存的。RocksDB 中有几个组件会影响内存使用:
- 块缓存
- 索引和布隆过滤器
- 内存表
- 迭代器
我们将依次描述它们。
块缓存
块缓存是RocksDB缓存未压缩数据块的地方。您可以通过设置 BlockBasedTableOptions 的 block_cache 属性来配置块缓存的大小:
rocksdb::BlockBasedTableOptions table_options;
table_options.block_cache = rocksdb::NewLRUCache(1 * 1024 * 1024 * 1024LL);
rocksdb::Options options;
options.table_factory.reset(new rocksdb::BlockBasedTableFactory(table_options));
如果在块缓存中没有找到数据块,RocksDB 会使用缓冲 IO 从文件中读取它。这意味着它还使用操作系统的页面缓存来存储原始文件块,通常包含压缩数据。从某种程度上来说,RocksDB的缓存是两层的:块缓存和页缓存。与直觉相反,减少块缓存大小不会增加 IO。节省的内存可能会用于页面缓存,因此将缓存更多数据。然而,CPU 使用率可能会增加,因为 RocksDB 需要解压缩从页面缓存读取的页面。
要了解块缓存使用了多少内存,您可以在块缓存对象上调用函数 GetUsage() 或在 DB 对象上调用 getProperty():
table_options.block_cache->GetUsage();
db->getProperty("rocksdb.block-cache-usage")
在MongoRocks中,可以通过调用获取块缓存的大小
> db.serverStatus()["rocksdb"]["block-cache-usage"]
索引和过滤块
索引和过滤块可能会占用大量内存,并且默认情况下它们不计入您为块缓存分配的内存中。这有时会给用户带来困惑:您为块缓存分配了 10GB,但 RocksDB 使用了 15GB 内存。这种差异通常是通过索引和布隆过滤器块来解释的。
我们不断地使索引和过滤器变得更加紧凑。要利用最新的改进,请通过BlockBasedTableOptions.format_version. 需要显式启用其他一些较新的功能:
- 设置BlockBasedTableOptions.optimize_filters_for_memory为更适合 jemalloc 的布隆过滤器尺寸。
- 还可以考虑使用新的带状过滤器
有关布隆过滤器的更多信息,请参阅RocksDB 布隆过滤器。
以下是粗略计算和管理索引和过滤块大小的方法:
- 对于每个数据块,我们在索引中存储三个信息:键、偏移量和大小。因此,有两种方法可以减小索引的大小。如果增加块大小,块的数量就会减少,因此索引大小也会线性减小。默认情况下,我们的块大小为 4KB,尽管我们通常在生产中运行 16-32KB。减少索引大小的第二种方法是减少键大小,尽管这对于某些用例来说可能不是一个选项。
- 计算过滤器块的大小很容易。如果您将布隆过滤器配置为每个键 10 位(默认情况下,误报率为 1%),则布隆过滤器大小为number_of_keys * 10 bits. 不过,您可以在这里玩一个技巧。如果您确定 Get() 大部分都能找到您要查找的键,则可以设置options.optimize_filters_for_hits = true. 启用此选项后,我们将不会在包含 90% 数据库的最后一级构建布隆过滤器。因此,布隆过滤器的内存使用量将减少 10 倍。不过,您将为每个在数据库中找不到数据的 Get() 支付一次 IO 费用。
有两个选项可以配置我们在内存中容纳多少索引和过滤器块:
- 如果设置cache_index_and_filter_blocks为 true,索引和过滤块将与所有其他数据块一起存储在块缓存中。这也意味着它们可以被调出。如果您的访问模式非常本地化(即您有一些非常冷的密钥范围),则此设置可能有意义。然而,在大多数情况下,这会损害你的性能,因为你需要有索引和过滤器来访问某个文件。cache_index_and_filter_blocks=true设置 时 L0 是一个例外pin_l0_filter_and_index_blocks_in_cache=true,这可能是一个很好的折衷设置。
- 如果cache_index_and_filter_blocks为 false(默认值),则索引/过滤器块的数量由 option 控制max_open_files。如果您确定 ulimit 始终大于数据库中的文件数,我们建议设置max_open_files为 -1,这意味着无穷大。此选项将预加载所有过滤器和索引块,并且不需要维护文件的 LRU。设置max_open_files为 -1 将为您带来最佳性能。
但是,无论选项如何,默认情况下每个数据库实例中的每个列族都将拥有自己的块缓存实例,并具有自己的内存限制。要共享单个块缓存,请block_cache在您的各种中设置BlockBasedTableOptions为使用相同的shared_ptr,或者为您的各个工厂共享相同的值,或者在您的各种或等中BlockBasedTableOptions共享相同的BlockBasedTableFactory 。table_factoryOptionsColumnFamilyOptions
要了解索引和过滤块使用了多少内存,您可以使用 RocksDB 的 GetProperty() API:
std::string out;
db->GetProperty("rocksdb.estimate-table-readers-mem", &out);
在 MongoRocks 中,只需从 mongo shell 调用此 API:
> db.serverStatus()["rocksdb"]["estimate-table-readers-mem"]
在分区索引/过滤器中,每个分区的索引和过滤器始终存储在块缓存中。顶级索引可以通过配置存储在堆或块缓存中cache_index_and_filter_blocks。
内存表
您可以将内存表视为内存中的写入缓冲区。每个新的键值对首先写入内存表。Memtable 大小由选项控制write_buffer_size。除非使用许多列族和/或数据库实例,否则它通常不会消耗大量内存。然而,memtable 的大小会反过来影响写入放大:memtable 的内存越多,写入放大就越小。如果您增加内存表大小,请务必同时增加 L1 大小!L1 的大小由选项控制max_bytes_for_level_base。
要获取当前内存表大小,可以使用:
std::string out;
db->GetProperty("rocksdb.cur-size-all-mem-tables", &out);
在 MongoRocks 中,等效的调用是
> db.serverStatus()["rocksdb"]["cur-size-all-mem-tables"]
从 5.6 版本开始,您可以将 memtable 的内存预算作为块缓存的一部分。检查写入缓冲区管理器以获取信息。
与块缓存类似,默认情况下,memtable 大小是按列族、每个数据库实例计算的。使用写入缓冲区管理器来限制跨列族和/或数据库实例的内存表内存。
迭代器
由迭代器固定的块通常不会对整体内存使用产生太大影响。然而,在某些情况下,当同时发生 10 万个读取事务时,可能会对内存造成压力。固定块的内存使用情况很容易计算。每个迭代器恰好为每个 L0 文件固定一个数据块,并为每个 L1+ 级别固定一个数据块。因此,固定块的总内存使用量约为num_iterators * block_size * ((num_levels-1) + num_l0_files)。要获取有关此内存使用情况的统计信息,请对块缓存对象调用 GetPinnedUsage() 或对 db 对象调用 getProperty():
table_options.block_cache->GetPinnedUsage();
db->getProperties("rocksdb.block-cache-pinned-usage");