
RocksDB Partitioned Index/Filters 分区索引与过滤器两级索引架构下的内存与 IO 优化实战【免费下载链接】rocksdbA library that provides an embeddable, persistent key-value store for fast storage.项目地址: https://gitcode.com/gh_mirrors/ro/rocksdb导读随着数据库容量相对内存越来越大DB/mem 比率不断上升SST 文件中索引块index block与过滤器块filter block的内存占用变得不可忽视。本文围绕 RocksDB 官方博客《Partitioned Index/Filters》展开系统讲解为什么大尺寸的 index/filter 会成为性能瓶颈、分区Partitioned机制如何通过两级索引 按需加载分区化解这一难题并结合当前仓库源码给出完整的配置参数、实现原理与验证方法。读完本文你将掌握kTwoLevelIndexSearch、partition_filters、metadata_block_size等核心选项的正确用法能够为内存受限场景设计出可落地的优化方案。问题背景一个 SST 文件里藏着多大的索引与过滤器RocksDB 默认采用 Block-based Table 格式每个 SST 文件都包含若干数据块data block以及用于定位这些数据块的索引块和用于加速点查的过滤器块。在原文档的描述中一个典型的配置是256MB 的 SST 文件索引块约 0.5MB、过滤器块约 5MB而单个数据块的典型大小只有4–32KB。也就是说元数据块的尺寸往往是数据块的数十到上千倍。这一比例在源码的默认值中也能得到印证include/rocksdb/table.h中BlockBasedTableOptions::block_size的默认值是4 * 10244KB而metadata_block_size的默认值是40964KB它正是分区元数据的目标块大小见 include/rocksdb/table.h 中metadata_block_size的注释Target block size for partitioned metadata应用于kTwoLevelIndexSearch的索引与partition_filters的过滤器。原文档指出当所有 index/filter 都能完整放进内存时每个 SST 生命周期内它们只需读取一次此时大尺寸无伤大雅可一旦它们需要与数据块竞争 block cache 空间、并可能多次从磁盘重读问题就会迅速放大。大尺寸 index/filter 的两大核心问题原文档将问题概括为两个层面它们本质上是同一个矛盾——稀缺的 block cache与巨大的元数据块之间的冲突。1. 与数据块争抢 block cache 空间抬高数据块缓存未命中率当cache_index_and_filter_blocks开启、index/filter 被放入 block cache 时它们实际上与数据块以及彼此争夺这块稀缺资源。一个 5MB 的过滤器占据的空间本可以缓存数千个 4KB 的数据块——这直接导致数据块缓存未命中率上升。与此同时巨大的 index/filter 之间也更容易互相挤出缓存进一步恶化元数据自身的高缓存未命中率。更关键的一点是一个 index/filter 块在缓存中存活的整个生命周期里真正被访问到的往往只是其中一小部分。为一个点查point lookup加载整块 5MB 过滤器绝大多数字节是无用功。这正是按需加载分区这一设计思路的原始动机。2. 缓存未命中后从磁盘重读放大磁盘 IO 开销一旦 index/filter 发生缓存未命中就必须从磁盘重新读取而其巨大的尺寸使 IO 成本居高不下。原文档给出了一个直观的对比一次简单的点查最多只需要从 LSM 的每一层各读取一个数据块4KB 量级但在此之前它可能已经加载了数 MB 的 index/filter 块。如果这种情况频繁发生磁盘的大量带宽将被元数据吞噬而不是服务于真正需要的数据块。什么是分区PartitionedIndex/Filter两级索引架构针对上述问题RocksDB 在 5.14 版本引入了分区索引与分区过滤器本文档发布于 2017 年 5 月即该特性的官方介绍文章。其核心思想可用一句话概括把一个大元数据块切成多个小分区partition再为这些分区构建一个体积很小的顶层索引top-level index。具体工作流程如下写入阶段SST 构建时索引/过滤器不再作为一个整体块写出而是被切分成多个尺寸接近metadata_block_size的小分区块每个分区对应一段 key 区间读取阶段读取 index/filter 时首先只把顶层索引载入内存。顶层索引记录每个分区的边界 key 与块句柄BlockHandle按需加载查询时先用顶层索引做一次二分查找定位到所需的分区再将该分区按需加载进 block cache而不是一次读入整块元数据驻留位置可调顶层索引内存占用极小根据cache_index_and_filter_blocks的设置可以存放在堆上table reader 内或 block cache 中见 docs/_posts/2017-05-12-partitioned-index-filter.markdown 原文描述。这套顶层索引 分区 按需加载的架构在源码中体现得非常清晰include/rocksdb/table.h的IndexType枚举中kTwoLevelIndexSearch 0x02的注释明确写道A two-level index implementation. Both levels are binary search indexes. Second level index blocks (partitions) use block cache even when cache_index_and_filter_blocksfalse.两级索引实现两级均为二分查找索引第二级索引块即分区即使在cache_index_and_filter_blocksfalse时也使用 block cache。同理partition_filters选项的注释也强调Filter partition blocks use block cache even when cache_index_and_filter_blocksfalse。配置指南从零开启分区索引与过滤器前置条件与依赖关系从include/rocksdb/table.h的注释可以确认以下依赖关系分区过滤器依赖分区索引partition_filters的注释明确写着currently this option requires kTwoLevelIndexSearch to be set as well即开启分区过滤器必须先开启两级索引分区过滤器与 block-based filter 不兼容partition_filters只适用于 full filter如NewBloomFilterPolicy生成的 Bloom/Ribbon 过滤器partition_filters注释指出该选项 incompatible with block-based filters分区块无条件走 block cache无论cache_index_and_filter_blocks取值如何索引/过滤器分区块总是使用 block cache只有顶层索引的位置受该选项控制。C API 配置示例以下配置开启两级索引与分区过滤器#include rocksdb/table.h #include rocksdb/filter_policy.h rocksdb::BlockBasedTableOptions table_options; // 1. 开启两级索引这是分区机制的基础 table_options.index_type rocksdb::BlockBasedTableOptions::IndexType::kTwoLevelIndexSearch; // 2. 开启分区过滤器full filter 专用 table_options.filter_policy.reset(rocksdb::NewBloomFilterPolicy(10, false)); table_options.partition_filters true; // 3. 元数据分区目标大小默认 4096 字节 table_options.metadata_block_size 4096; // 4. 按需控制顶层索引的驻留位置与固定行为 // 顶层索引/过滤器块默认置入 cache 并 pin 住pin_top_level_index_and_filter 默认 true table_options.cache_index_and_filter_blocks true; rocksdb::Options options; options.table_factory.reset(rocksdb::NewBlockBasedTableFactory(table_options));关键参数一览表以下参数均定义于 include/rocksdb/table.h 的BlockBasedTableOptions参数默认值作用与说明index_typekBinarySearch索引结构类型设为kTwoLevelIndexSearch启用两级分区索引两级均使用二分查找partition_filtersfalse为每个 SST 文件使用分区 full filter需要同时开启kTwoLevelIndexSearch与 block-based filter 不兼容metadata_block_size4096分区元数据分区索引、分区过滤器的目标块大小decouple_partitioned_filterstrue分区索引与分区过滤器使用相互独立的切分边界使两者都更精确地命中目标尺寸降低 block cache 中的碎片与元数据开销false时两者共享切分边界cache_index_and_filter_blocksfalse为 false 时 table reader 在初始化阶段预加载 index/filter分区块始终使用 block cache本选项只影响顶层索引的位置堆 vs block cachecache_index_and_filter_blocks_with_high_prioritytrue将 index/filter 等元数据块以高优先级放入 block cache降低其相对数据块被淘汰的概率pin_top_level_index_and_filtertrue顶层索引/过滤器块存入 cache 的同时在 table reader 中持有引用并 pin 住仅在 table reader 释放时淘汰不限于 L0pin_l0_filter_and_index_blocks_in_cachefalse已废弃的 L0 固定选项现由MetadataCacheOptions的partition_pinning/unpartitioned_pinning取代optimize_filters_for_memorytrue生成过滤器时优化内存内部碎片需format_version 5且支持malloc_usable_size实测在 Jemalloc 下可节省约 10% 过滤器内存占用关于 pinning 行为的更细粒度控制源码提供了MetadataCacheOptions结构同文件其中top_level_index_pinning、partition_pinning、unpartitioned_pinning三个PinningTier字段分别控制顶层索引、分区块、未分区元数据块三个层级的固定策略PinningTier支持kFallback、kNone、kFlushedAndSimilar、kAll四档kFlushedAndSimilar指来自 memtable flush 且体积小于 1.5 倍write_buffer_size的 L0 文件。OPTIONS 文件配置分区相关选项同样可以在 OPTIONS 文件中以字符串形式配置仓库中的 examples/rocksdb_option_file_example.ini 展示了block_based_table_factory的配置范式其中index_typekBinarySearch为默认值。对应地可写为[DBOptions] ... [TableOptions/BlockBasedTable] index_typekTwoLevelIndexSearch partition_filterstrue metadata_block_size4096 cache_index_and_filter_blockstrue动态调整依据table.h的说明除no_block_cache等少数例外这些选项大多支持通过SetOptions动态调整只对新构建的 SST 生效例如db-SetOptions({{block_based_table_factory, {index_typekTwoLevelIndexSearch;partition_filterstrue;}}});需要说明的是index_type属于写路径选项只影响新文件构建而 pin 类选项属于读路径选项表读取器table reader可能存活到 SST 文件本身的生命周期结束因此读路径选项的生效存在仅对新打开的文件的时间窗口。源码级实现剖析从构建到查询的完整链路分区机制在仓库中有完整的实现相关文件集中在table/block_based/目录table/block_based/partitioned_index_reader.hPartitionIndexReader继承自BlockBasedTable::IndexReaderCommon负责在两级索引结构中进行二分查找。其NewIterator返回一个两级迭代器第一级在分区索引上内部维护partition_map_用于缓存已 pin 的分区块table/block_based/partitioned_index_iterator.hPartitionedIndexIterator封装对分区的遍历。它持有第一级index_iter_在顶层索引上迭代与block_iter_在具体分区内迭代通过Seek先定位顶层索引条目再按需加载对应分区块并维护prev_block_offset_避免重复拉取同一个数据块table/block_based/partitioned_filter_block.hPartitionedFilterBlockBuilder写路径与PartitionedFilterBlockReader读路径。Builder 继承自FullFilterBlockBuilder内部维护一个过滤器分区的索引index_on_filter_block_builder_并通过keys_per_partition_控制每个分区的键数量Reader 通过KeyMayMatch/PrefixMayMatch以及 MultiGet 批量版本完成查询先SeekFilterPartitionHandle定位分区句柄再GetFilterPartitionBlock按需加载分区。从这些实现可以推断出读取路径的完整调用链点查/范围查询 → 顶层索引二分查找PartitionIndexReader::NewIterator → 定位目标分区句柄BlockHandle → 按需加载分区块到 block cachePartitionedFilterBlockReader::GetFilterPartitionBlock → 在分区内完成索引二分或过滤器判存KeyMayMatch值得注意的设计细节来自源码注释分区分块策略PartitionedFilterBlockBuilder中DecideCutAFilterBlock()决定何时切开一个新的过滤器分区metadata_block_size直接参与目标尺寸计算见 table/block_based/partitioned_filter_block_test.cc 中对table_options_.metadata_block_size的大量构造与断言索引与过滤器分区对齐的历史设计注释说明目前索引与过滤器保持相同数量的分区以便将来优化如果该优化未实现则可改用不同分区数——这正是后来decouple_partitioned_filters默认 true引入独立切分边界的原因它使两类元数据都能更精确地命中目标尺寸减少 block cache 的碎片并让淘汰策略对待各块更公平并行压缩的线程安全UpdateFilterSizeEstimate等路径在并行压缩场景下可能被后台工作线程调用因此相关的分区尺寸统计使用原子类型RelaxedAtomic。如何验证分区生效测试与观测手段单元测试仓库中的 table/block_based/partitioned_filter_block_test.cc 是验证分区过滤器行为最直接的测试它组合使用NewBloomFilterPolicy与metadata_block_size参数覆盖了不同目标分区尺寸下的构建与读取路径包括将metadata_block_size设为 1 等极端值来强制大量分区。运行方式在仓库根目录编译后./partitioned_filter_block_test此外table/block_based/partitioned_index_iterator.cc对应的两级索引迭代器也有专门的测试覆盖PartitionedIndexIteratorTest可验证 Seek/Next/Prev 在分区边界上的正确性。运行期观测block cache 统计开启partition_filters后过滤器分区以CacheEntryRole::kFilterPartition过滤器分区、索引分区以kIndexPartition的角色计入 block cache 统计可通过db-GetProperty(rocksdb.block-cache-entry-stats)观察各角色的占用与命中情况确认元数据不再以整块大对象形式驻留sst_dump 检查布局使用 tools/sst_dump.cc 提供的sst_dump --filexxx.sst可以查看 SST 内部 block 布局观察索引/过滤器是否被切分为多个小分区块并附带顶层索引。适用场景与注意事项什么时候该用分区 Index/Filter根据原文档的问题分析与源码选项语义分区机制在以下场景收益最大DB/mem 比率很高数据总量远超 block cache 容量元数据必须与数据竞争缓存空间元数据块尺寸与数据块尺寸差距悬殊如大 SST256MB 甚至更大配合较大 bits-per-key 的过滤器内存非常受限的部署如原文档中100TB 数据配 60MB block cache的极端场景。需要权衡的代价分区索引/过滤器本身增加了一层顶层索引查找查询路径多一次间接寻址但由于第二级分区普遍远小于整块元数据点查的实际 IO 往往显著下降partition_filters与kTwoLevelIndexSearch强绑定若当前索引形态是 hash indexkHashSearch或依赖kBinarySearchWithFirstKey需要评估切换成本分区过滤器只适用于 full filterblock-based filter 无法使用旧版本 SST 若以整块元数据格式写出新配置不会重写已有文件需要依赖 compaction 逐步重写才能全面生效配置只影响新构建的 SST。实测数据参考原文档案例原文档提供了两组官方测试数据用以说明分区的实际收益。需要说明这两组数据来自 2017 年该特性发布时的测试环境具体数值会随 RocksDB 版本、硬件与配置变化此处仅作为理解量级与收益方向的参考场景一HDD 100TB 级数据库环境86GB 数据库部署在 HDD 上通过 direct IO绕过 OS 文件缓存 仅 60MB 的极小 block cache 来模拟100TB 数据对应的小内存场景结果分区后吞吐从5 op/s 提升到 55 op/s提升约 11 倍。场景二SSD Linkbench 工作负载环境300GB 数据库部署在 SSD 上同样使用 direct IOblock cache 分别设为 6GB 与 2GB模拟同一节点上存在多个 DB 时内存被压缩的情况结果不分区时block cache 从 6GB 降到 2GBLinkbench 吞吐从 38k tps 跌至 23k tps启用分区后同样缩容只跌至30k tps——在内存减半的情况下保留了约 79% 的吞吐。两组数据的共同结论是当元数据成为缓存与 IO 瓶颈时分区机制用很小的顶层索引开销换来了大幅的吞吐改善尤其适合内存与磁盘资源严重不平衡的部署形态。总结Partitioned Index/Filters 是 RocksDB 面向大数据量、小内存场景的关键优化它把每个 SST 中动辄数 MB 的整块索引/过滤器切分为多个 4KB 量级的小分区通过两级索引按需加载从根源上缓解了元数据与数据块争抢 block cache、以及大块元数据重读放大磁盘 IO 两个问题。实践中只需两步即可开启——设置index_typekTwoLevelIndexSearch并打开partition_filters再结合metadata_block_size、cache_index_and_filter_blocks与 pinning 相关选项做精细调优。其完整的构建、读取与测试实现都可以在当前仓库的table/block_based/目录与 partitioned_filter_block_test.cc 中深入研读。【免费下载链接】rocksdbA library that provides an embeddable, persistent key-value store for fast storage.项目地址: https://gitcode.com/gh_mirrors/ro/rocksdb创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考