第 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 生产路径可细化为:
flowchart TB
GET["Version::Get / TableCache::Get"] --> OPEN{"TableReader 在 Table Cache?"}
OPEN -->|miss| OPENFILE["Open SST + 读 Footer"]
OPEN -->|hit| FILTER["FilterBlock::KeyMayMatch?"]
OPENFILE --> FILTER
FILTER -->|否| SKIP["跳过 Data Block I/O"]
FILTER -->|是/无 Filter| INDEX["Index Block 定位 Data Block"]
INDEX --> BC{"Block Cache 命中?"}
BC -->|hit| DECODE["解压 + 块内二分"]
BC -->|miss| DISK["Read + Insert Block Cache"]
DISK --> DECODE
字节读放大 与 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 次优。生产配置应显式:
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 一致,此处只列内核相关项):
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):
*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。
返回 系列目录
参考资料
- 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 级接口)。
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【RocksDB 内核机制】经典故障与排查
用 GetProperty 与 LOG 钉住 L0 堆积、compaction 落后、Block Cache 未命中与 ENOSPC write stall 四类生产故障;交叉引用 storage/76 磁盘耗尽链,给出 db_bench 复现步骤而不伪造 stats 数字。
【RocksDB 内核机制】LSM 生态全景:存储引擎三角与 RocksDB 生态位
闭合存储引擎三角的最后一角:对照 PG B-Tree 与列存 scan,定位 LSM 写优化引擎在 TiKV、Flink、Kafka Streams、ClickHouse Embedded 中的嵌入方式,并划分本系列与 lsm-tree DIY、storage/31–32、stream/12 的分工边界。
【RocksDB 内核机制】RocksDB 架构演进:相对 LevelDB 的 diff 地图
对照 LevelDB 1.23,用 diff 表与源码路径梳理 RocksDB 9.x 的多线程 flush/compaction、Column Family、RateLimiter、Direct IO、Statistics 与并发 MemTable 写,建立后续读 db_impl 子文件的坐标系。
【RocksDB 内核机制】WAL 与 WriteBatch:持久化与原子批写
从 log::Writer 的 32KB Block 分片 record、WriteBatch 二进制布局,到 DBImpl::WriteImpl 中 Group Commit 与 sync/fsync 语义,钉住写路径第一步:先 WAL 后 MemTable 的提交顺序与原子性边界。