原文发布于 quant67.com,转载请保留出处。
第 7 篇 的 GetImpl 在 MemTable 未命中后会进入 Version::Get → TableCache::Get,对每个候选 SST 读取 Index、Filter、Data Block。第 8 篇 的范围扫描在同一 Table Cache 上打开更多 InternalIterator。两层缓存------Table Cache (缓存已打开的 TableReader)与 Block Cache (缓存 SST 内的块)------加上 Bloom / Ribbon Filter 与 Partitioned Index ,共同决定 LSM 点查的 实际读放大。
本文是系列 第 9 篇 ,源码锚点:RocksDB 9.x 的 db/table_cache.cc、table/block_based/block_based_table_reader.*、table/block_based/full_filter_block.cc、table/block_based/partitioned_index_reader.cc。Bloom 误判率公式、Block Cache 容量规划、LRU vs HyperClockCache 对比等 通用读优化推导 见 storage/68 读取性能优化------本篇只写 RocksDB 内核中的挂载点与 CF 选项边界,不重复该文表格与 benchmark 口径。
版本锚定 :RocksDB 9.x。FPR 公式与 BPK 对照表引用 storage/68(A/B 级),不在此重抄。
一、读放大在 Get 路径上的落点
结合 storage/68 第一节 的 LSM 点查示意图,RocksDB 生产路径可细化为:
字节读放大 与 I/O 读放大 在 storage/68 已定义:LSM 层数深、L0 文件多、Bloom 误判、Index 过大都会推高分子。本篇关注 RocksDB 如何用 Cache 与 Filter 压低分子。
| 组件 | 缓存对象 | 未命中代价 |
|---|---|---|
| Table Cache | TableReader + 文件描述符 |
打开 SST、读 Footer、建 Index/Filter 句柄 |
| Block Cache | Index / Filter / Data / Compression Dict 等 块 | 一次或多次 RandomAccessFile::Read |
| Bloom / Ribbon | 通常作为 Filter Block 进入 Block Cache | 误判时多读 Data Block |
二、Table Cache:SST 文件级缓存
TableCache(db/table_cache.h、db/table_cache.cc)按 (column_family_id, file_number) 缓存 已打开的 TableReader。
FindTable 流程概要:
cache_.Lookup(key)--- 命中则直接返回TableReader*。- 未命中:
GetTableReader打开磁盘 SST,解析 Footer,构造BlockBasedTable。 cache_.Insert(key, table_reader, charge, handle)--- 插入 Table Cache。
TableCache::Get(点查)与 NewIterator(扫描)在持有 TableReader 后,调用 BlockBasedTable::Get / NewIterator,进入块级读取。
与 Block Cache 的分工:
- Table Cache 避免重复 open / 解析 Footer / 构造 reader。
- Block Cache 避免重复 读同一 Data/Index/Filter 块。
- Iterator 在 SST 间切换时,Table Cache 使 reader 复用;热 Data Block 仍依赖 Block Cache。
Table Cache 大小由 max_open_files 与 LRU 驱逐间接控制;文件句柄耗尽时会 关闭冷文件 并从 Table Cache 剔除,下次读重新 open(TableCache 注释:error 结果不缓存,便于修复后恢复)。
三、Block Cache:块级缓存与优先级
Block Cache 接口在 rocksdb/cache.h;默认实现为 分片 LRU 或 HyperClockCache (8.x+,高并发友好)。配置挂在 BlockBasedTableOptions::block_cache 或 DBOptions::row_cache(行级,另议)。
BlockBasedTableReader 读块时(table/block_based/block_based_table_reader.cc):
- 按
(cache_key)查 Block Cache。 - 命中:解压(若启用 compression)并解析。
- 未命中:从文件读取 → 可选 decompress →
Insert进 Cache。
缓存键 与文件 identity、块 offset、块类型相关;RocksDB ≥ 6.24 对同 DB 内 cache key 更稳定(见 HISTORY 中 block cache key 说明)。
高优先级池
Index Block 与 Filter Block 可标记 高优先级 (cache_priority::HIGH),Data Block 为 LOW 。配合 Cache::SetHighPriorityPoolRatio(storage/68 第四节示例),减少 「Data 挤掉 Index/Filter」 导致的读放大尖刺。这是工程上常见的 点查 P99 优化。
pin_l0_index_and_filter_blocks
BlockBasedTableOptions::pin_l0_index_and_filter_blocks 等选项把 L0 的 index/filter 固定在内存,适合 L0 文件多、点查多的 workload;代价是 内存占用上升,需与 Block Cache 总容量一并规划(storage/68 容量规划一节;本篇不重复命中率--延迟算术)。
四、Bloom Filter 与 Ribbon Filter
作用位置
每个 SST 的 Filter Block (Meta Block)回答:KeyMayMatch(user_key)?
- 返回 false --- key 一定不在 本文件,跳过 Data Block I/O(无假阴性)。
- 返回 true --- 可能在;需继续 Index → Data 查找(存在假阳性)。
FullFilterBlockReader::KeyMayMatch(table/block_based/full_filter_block.cc)在 whole_key_filtering 下对整 key 查询;有 prefix_extractor 时可同时维护 prefix 与 whole key(AddKeyAndAlt)。
Full Filter vs Block-based Filter(历史)
RocksDB 7.0 起默认倾向 Full Filter (整文件一个 filter block),较旧的 block-based filter(每 Data Block 一个 filter 子块)在大文件、高 BPK 下 I/O 次优。生产配置应显式:
cpp
rocksdb::BlockBasedTableOptions table_options;
table_options.filter_policy.reset(rocksdb::NewBloomFilterPolicy(10, false));
// 第二个参数 false = 非 block-based,即 full filter 路径
bits_per_key 与理论 FPR 的关系见 storage/68 第三节 公式与 BPK 表;默认 10 bits/key 约 0.82% FPR(该文引用数据,不在此重算)。
Ribbon Filter
NewRibbonFilterPolicy(util/bloom_test.cc、include/rocksdb/filter_policy.h)用 Ribbon(近似 Bloom 构造) 在相同 FPR 下 更省空间 或 更低 CPU (RocksDB Blog, Ribbon Filter,B 级)。db_bench 中 --use_ribbon_filter 可切换(tools/db_bench_tool.cc)。
适用边界:
- 大 SST、高 BPK、compaction 构建 filter CPU 成为热点时值得 A/B。
- 极短 key / 极低 QPS 场景收益有限。
optimize_filters_for_hits
假设 几乎总是命中 时,可对 最大层 跳过 filter 构建以省空间与 compaction CPU;若 workload 含大量 Get 不存在 key ,会 显著增加 无效 Data Block 读(storage/68 已警告;默认慎用)。
4.1 文献谱系:从 Bloom 到 Ribbon
| 工作 | 年份 / 出处 | 本文引用点 |
|---|---|---|
| Burton Bloom, Space/Time Trade-offs in Hash Coding | Communications of the ACM 1970 | 假阳性、无假阴性 的 membership test 起源 |
| Kirsch & Mitzenmacher, Less Hashing, Same Performance | Random Structures & Algorithms 2006 | 双 hash 构造 Bloom 的等价性(工程实现常用) |
| LSM 读放大 | O'Neil 1996;Dayan & Idreos SIGMOD 2017--18 | sorted run 数 决定 probe 次数;Filter 剪枝 块级 I/O (第 10 篇) |
| RocksDB Ribbon Filter | RocksDB Blog(B 级) | 同 FPR 下 更省空间/CPU 的 filter 构造;NewRibbonFilterPolicy |
争论边界 :提高 bits_per_key 降低 FPR,但 增大 SST 与 compaction CPU ------与 RUM / 三种放大中 用空间换读 同构(第 1 篇 §2.4)。Learned index (db-frontier/04)在 静态 workload 下可 beat Bloom,但 更新与 crash 一致 仍是开放问题------不能 默认替代生产 Bloom/Ribbon。
4.2 工程间隙
| 论文 / storage/68 公式 | TiKV / Flink 嵌入 |
|---|---|
| 均匀随机 Get | KeyGroup 热点 → 少数 SST 过热 |
| 单机 Block Cache | Flink managed memory 与 Write Buffer 分池 |
| 固定 BPK | changelog 恢复阶段 全表 scan 使 cache 失效 |
4.3 开放问题
- Ribbon 默认化 :新版 RocksDB 是否应在 低 QPS 嵌入 场景默认 Ribbon?取决于 compaction CPU 与读 SLA,无 universal 答案。
- Filter 与 disaggregated cache:远程 Block Cache 下 filter 是否仍本地 pin?产品与 CXL 论文方向并存。
- Range tombstone + Bloom :Bloom 对 tombstone 不剪枝 ;长 scan 读放大仍由 第 8 篇 MergeIterator 决定。
五、Partitioned Index 与 Filter
当 SST 达到 GB 级,单体 Index Block 可能达数十 MB,一次 cache miss 成本高。Partitioned Index 把 Index 切成多个 partition,由 Top-Level Index 指向(table/block_based/partitioned_index_reader.cc)。
启用方式(与 storage/68 一致,此处只列内核相关项):
cpp
table_options.index_type =
rocksdb::BlockBasedTableOptions::kTwoLevelIndexSearch;
table_options.metadata_block_size = 4096; // partition 目标大小
table_options.partition_filters = true; // Filter 也可分区
table_options.pin_top_level_index_and_filter = true;
读路径变化:
- 先读 Top-Level Index(常 pin 在内存)→ 定位 Index Partition。
- 读对应 Index Partition → 定位 Data Block。
- Filter 分区时同理:Top-Level Filter → Filter Partition →
MayMatch。
pin_top_level_index_and_filter = true 保证点查最多 多一次 partition I/O ,而不是加载整棵巨型 Index。Partitioned Filter 与 Ribbon 可叠加,用于 宽表、大 key 空间(TiKV 等场景常见)。
六、与 GetImpl / Version::Get 的衔接
Version::Get 循环中(db/version_set.cc):
cpp
*status = table_cache_->Get(
read_options, *internal_comparator(), *f->file_metadata, ikey,
&get_context, mutable_cf_options_, file_read_hist, skip_filters, level, ...);
skip_filters 由 Version::IsFilterSkipped(level, ...) 决定------例如 L0 最后文件 、已知 key 必存在 的优化路径、或 ReadOptions 层指示。Filter 跳过后 直接 Index/Data,读放大上升但延迟可能降低(极特殊场景)。
GetContext::MarkKeyMayExist vs SaveValue:Bloom 可能误判 时仍进入块内查找,最终 kNotFound;Statistics 中 BLOOM_FILTER_USEFUL (filter 帮跳过)与 BLOOM_FILTER_FULL_POSITIVE 等 tick 可观测 filter 效果(需打开 Statistics)。
Block Cache 追踪:BLOCK_CACHE_HIT / BLOCK_CACHE_MISS、BLOCK_CACHE_INDEX_HIT 等(详见 storage/68 GetTickerCount 示例,本篇不重复代码块)。
七、调参边界(不展开 storage/32 cookbook)
| 目标 | 首选旋钮 | 内核侧注意 |
|---|---|---|
| 降低不存在 key 的 I/O | 提高 BPK / Ribbon | compaction 构建 filter 更慢、SST 更大 |
| 降低存在 key 的 Index I/O | Partitioned Index + pin top | 内存 pin 与 cache 重复计费 |
| 提高 cache 命中 | 增大 Block Cache、HIGH priority 给 index/filter | 与 MemTable / OS page cache 争内存 |
| 降低 open 开销 | 提高 max_open_files |
进程 fd 与 Table Cache 压力 |
| Scan 友好 | 较大 block_size、iterator readahead |
点查可能多读无关 Data |
200+ 参数模板与 db_bench 填参见 storage/32 RocksDB 实践;三种放大 定义见 storage/31 与 lsm-tree 全景。
八、与相邻篇目分工
| 话题 | 本篇 | 他处 |
|---|---|---|
| FPR 公式、BPK 表、Cache 命中率--延迟 | 引用 | storage/68 |
| Get / Iterator 路径 | 已写 | 第 7--8 篇 |
| Compaction 降低 L0 / 层数 | 不展开 | 第 10--11 篇 |
| Flink state cache 现象 | 不展开 | stream/12、第 16 篇 |
九、小结
- Table Cache 缓存
TableReader,避免重复 open SST;Block Cache 缓存 Index/Filter/Data 块,是抗读放大的主战场。 - Full / Ribbon Filter 在 SST 级做 不存在 key 的 I/O 剪枝;谱系从 Bloom (1970) 到 Ribbon(B 级博客),FPR 由 BPK 决定(详见 storage/68)。
- Partitioned Index/Filter 把巨型索引拆成 top + partition,配合 pin 控制点查 I/O 次数上界。
- 调参需同时看 L0 文件数(结构读放大) 与 cache/filter(块读放大);Statistics tick 是生产判读入口。
- 通用读优化理论、LRU/HyperClock、覆盖索引等 跨引擎内容 不在此重复,统一查阅 storage/68。
下一篇 :Leveled Compaction
返回 系列目录
参考资料
- RocksDB 9.x 源码:
db/table_cache.cc;table/block_based/block_based_table_reader.cc;table/block_based/full_filter_block.cc;table/block_based/partitioned_index_reader.cc;include/rocksdb/filter_policy.h(A 级)。 - RocksDB Wiki, Block Cache / Bloom Filter / Index Block(A 级)。
- Burton Bloom, Space/Time Trade-offs in Hash Coding, CACM 1970(Bloom filter 奠基;A 级)。
- Kirsch, Adam & Mitzenmacher, Michael, Less Hashing, Same Performance, RSA 2006(A 级)。
- storage/68 读取性能优化(Bloom 公式、Block Cache 架构、Partitioned Index 配置;本篇引用不重复推导)。
- storage/31 LSM 调参;storage/32 RocksDB 实践(调参边界)。
- 第 1 篇 §2.4 RUM;第 7--8 篇 Get/Iterator 路径。
- db-frontier/04 Learned Index(与 Bloom 争论边界;B 级接口)。