# BlockBased Format
- RocksDB wiki:
- https://github.com/facebook/rocksdb/wiki/Rocksdb-BlockBasedTable-Format
- https://github.com/facebook/rocksdb/wiki/Index-Block-Format
- https://github.com/facebook/rocksdb/wiki/Partitioned-Index-Filters
- <img src="https://img.jonahgao.com/oss/note/2025p2/rocksdb_sst_format2.svg" width="650" height="auto" />
- Top-Level Filter Block: 索引 filter partition blocks
- Full Filter Block:全量 filter 数据
- Top-Level Index Block: 索引 index partition blocks
- Full Index Block:直接索引 data blocks
- **BlockHandler**: 指向文件内的 Block(Data Block 或者 Meta Block)
- **offset**:varint64
- **size**: varint64
- **Meta Block** 包括
- Filter block
- Index block
- Compresion dicationary block
- Range deletion block
- Stats/Properties block
- Meta Block 的格式与 Data Block **一样**,压缩可选。
## internal key 编码
| user_key | seq_and_type( 固定 64 bits,小端编码) |
| -------- | ------------------------------ |
- seq_and_type:低 8 bits 为 type,高 56 bits 为 seqno
```cpp
// Pack a sequence number and a ValueType into a uint64_t
inline uint64_t PackSequenceAndType(uint64_t seq, ValueType t) {
return (seq << 8) | t;
}
```
-----
# Data Block
## 格式
- data block 内存储**有序**的KV 对。
- **前缀压缩**(prefix-compressed)
- 存储 key 时,丢弃跟==前一个== key 的共同前缀
- 每个 N 个 Key,存储一份全量 Key。称为 restart point。
- Block 尾部存储所有 restart points 的 offsets
- 方便在 Block 内二分搜索某个 key。
- 开关:`BlockBasedTableOptions::use_delta_encoding`
- 一个 KV entry 的存储结构:
- **shared_bytes**: `varint32`
- **unshared_bytes**: `varint32`
- **value_length**: `varint32` ^958fdf
- **key_delta**: `char[unshared_bytes]`
- **value**: `char[value_length]`
- Block 尾部结构:
- **restarts**:`uint32[num_restarts]` // 每个 restart point 的在 block 内的偏移
- **num_restarts**: `uint32`

### Hash 索引
- 当 `data_block_index_type` 为 `kDataBlockBinaryAndHash` 时生效(默认不启用)。
- 在原来 restarts array 二分索引的基础上,构建==附加==的 hash 索引(索引一个 block 的所有 keys)。
- 索引条目:user key hash 值 <-> restart index
- 即根据 key 的 hash 值可以快速定位到该 key 位于 block 内的哪个 restart interval 中。
- 结构:
- 根据 `data_block_hash_table_util_ratio` 确定 buckets 数量
- `num_buckets = num_keys_in_block / ratio`
- ratio 越小,buckets 越多,冲突概率越低
- `buckets[key_hash % num_buckets] = restart_index`
- 冲突:两个 key 位于相同的 bucket ,但它们的 `restart_idex` 不同
- 存储一个特殊值 kCollision(相当于索引失效了)
- 存储:每个 buckets 的 `restart_index` 加上 buckets 的数量
- 每个 bucket 占用一个字节
- 存储在 block restart array 之后
> [!summary]
> - 优势:可以提升点查性能;
> - 劣势:
> - 空间占用(磁盘+内存,大小取决于 keys 量级)
> - 占用 block cache,加大 miss。
> - 限制:
> - 最多支持 253 个 restart intervals
> - 对自定义 Comparator 有[限制](https://github.com/facebook/rocksdb/wiki/Data-Block-Hash-Index#things-that-need-attention)
## BlockBuilder
^897012
- 用于写一个 block
- 文件:*block_builder.cc*
- 成员变量:
- **`last_key_`**: `string`, 保存上一个写入的 Key,用于计算共同前缀
- **`buffer_`**: `string` , block 写入buffer
- **`restarts_`**:`std::vector<uint32_t>` , restart point 在 buffer_ 内的偏移
- 初始为 `{0}`
- **`use_delta_encoding_`**: `bool`,是否启用 key 前缀压缩
- **`counter_`**:添加过的 Key 计数,用于判断何时插入 restart point
- **AddWithLastKey()**
- 参数 last_key_param:上一个 key
- 可以使用 BlockBuilder 内保存的 last_key_
- 也可以由外部直接提供, 就不需要维护 last_key_ 了(避免拷贝)。
- value delta encoding
- 当 `use_value_delta_encoding_` 打开时,适用于 index block
- 这种格式不用编码 [[block based#^958fdf|value_length]],index value 内容固定,自解释
- 当 key shared_bytes 不为空时,编码 delta value (BlockHandle 中 的 size delta);否则编码完整的 value
- **Add()**
- 写入一个 Key Value,使用内部维护的 last_key_ 调用 AddWithLastKey
- 如果启用了 key 前缀压缩,更新 last_key_
- 注意:需要保证 key 大于之前添加的 key
- Add 和 AddWithLastKey 不能穿插调用,除非调用了 Reset
- **Finish()**
- block 末尾写入 restart array
- 写入 footer,包含了
- 索引类型 DataBlockIndexType
- restart array 的长度
## Block 读取相关
### BlockContents
- 代表从 SST 中读出的一个 block。
- 成员:
- **`data`**:`Slice`,指向 block 数据
- **`allocation`**:`unique_ptr<char[], CustomDeleter>`,数据的物理分配,如果不为空表示由当前类持有 block 数据的所有权。
### BlockIter
- 用于迭代一个 Block 中的 entries,继承 `InternalIteratorBase<TValue>`
- TValue: 迭代器返回的 value 类型
- 主要实现了 InternalIteratorBase 的`Seek*`/`Prev`/`Next` 等方法:
1. 调用子类相应的 Impl 方法
2. UpdateKey:设置 `key()` 的返回值,统一==应用 `global_seqno_`逻辑==。
- 迭代返回的 keys 可以是 ikey 或者 ukey,取决于子类的配置。
- 成员:
- **`data_`**:`const char *`,目标 block 数据,包含了 entries 和 restarts 数据
- **`num_restarts_`**:`uint32_t`,restart points 个数
- **`restarts_`**:restarts array 在 `data_`中的 offset
- 即第一个 restart point 位于 `data_ + restarts_`
- **`restart_index_`**:当前 entry 位于第几个 restart block 内
- **`current_`**:当前 entry 在 `data_` 中的 offset
- 大于 `restarts_`时视为非 Valid(即迭代到了末尾)
- **`raw_key_`**:`IterKey`,block 内存储的原始 key,格式可能为 ikey 或 ukey。
- 取决于子类初始化时的设置,通过 `raw_key_.SetIsUserKey(bool)`。
- **`key_`**:`Slice`,迭代器 `key()` 函数的返回值,格式取决于 `raw_key_`
- **`global_seqno_`**:`SequenceNumber`,SST 注入相关,值不为 `kDisableGlobalSequenceNumber`起作用,返回的 ikey 将都使用该 seqno
- **`value_`**:`Slice`,当前 entry 的 value。
- *`cur_entry_idx_`*:当前 entry 是 block 内的第几个
- 仅当开启 `block_protection_bytes_per_key`时有效
- 计算依赖 `block_restart_interval_`(不开启 `block_protection_bytes_per_key` 时该值为 0)
- PR:[Block per key-value checksum #11287](https://github.com/facebook/rocksdb/pull/11287)
- **InitializeBase()**
- 初始化类的各个数据成员。
- **UpdateKey()**
- 根据 `raw_key_` 更新 `key_`
- 如果 `raw_key_` 为 user key,则更新为 `raw_key_.GetUserKey()`
- 如果 `raw_key_` 为 internal key,尝试应用 `global_seqno_`
- **ParseNextKey()**
- 参数:
- **`is_shared`**: `bool *`,函数内设置,表示当前 key 跟前一个 key 是否有 shared bytes。
- **`DecodeEntryFunc`**:模版参数,用于解码 entry,子类指定。
- 作用:解析下一个 entry
- 更新 `current_`,调用 DecodeEntryFunc 解码 entry,结果保存到 `raw_key_` 和 `value_` 中
- `current_` 更新为上一个 `value_`的后一字节
- 当前 key 没有 shared bytes 时,则尝试更新 `restart_index_`:
- 不变性:下一个 restart point interval 的起始一定大于当前 entry 的起始 offset
- *NOTE:没有 shared bytes 并不一定意味着是新的 restart point,restart point 的分布只取决于 `block_restart_interval` 参数*
- **SeekToRestartPoint()**
- 参数:
- **`index`**: `uint32_t`
- 移动到第 `index` 个 restart point 的起始处
- 主要是更新 `restart_index_` 和 `value_`( ParseNextKey 依赖 `value_`)
- **GetRestartInterval()**
- 获取 `block_restart_interval` ,计算方式:
- SeekToFirstImpl, 再调用 NextImpl 遍历,计算到下一个 restart point 之前有多少 entries。
- **BinarySeek()** ^9e6711
- 二分搜索 restarts array。
- 参数:
- **`target`**:`const Slice &`,搜索目标
- **`index`**:`uint32_t *`,返回值,目标位于第几个 restart interval 内。
- **`is_index_key_result`**:`bool *`,返回值,index 处的 restart key 正好是目标 key,不用继续在 restart interval 中线性扫描。
- **FindKeyAfterBinarySeek()** ^471e60
- 参数:
- **`target`**:`const Slice &`,搜索目标
- **`index`**:`uint32_t`,BinarySeek() 的结果,搜索第几个 restart interval
- **`is_index_key_result`**:`bool`,BinarySeek() 的结果
- 在目标 restart interval 中线性扫描第一个大于 target。
- 如果没找到,则指向第 `index+1` restart inteval 的首个 key。
### DataBlockIter
- 继承自 `BlockIter<Slice>`,迭代普通的 data block。
- **SeekToFirstImpl()**:
- 移动到首个 restart point,然后解析 entry
- **SeekToLastImpl()**:
- 收到到最后一个 restart point,依次解析 entries,直到 restart interval 的最后一个 entry。
- **SeekImpl(target)**:
1. 调用 BinnarySeek() 定位 restart interval
2. 调用 FindKeyAfterBinarySeek() 在 restart interval 中顺序查找
- **NextImpl()**
- 调用 ParseNextDataKey() 移动到并解析下一个 entry
- **SeekForGetImpl(target)**
- 为点查而优化的 seek,跟 SeekImpl() 区别在于会使用 hash index 如果存在。
- 如果 target 不可能存在(包括下一个 block 内),则返回 false。
- **PrevImpl()**
- 基础实现:
1. 移动到当前 entry offset (`current_`)之前的 restart point
2. 顺序解析 restart interval 的 entries 并跳过,直到当前 entry 的前一个 entry
- 即 `NextEntryOffset() < current`
- cache 优化:
- 顺序解析 restart interval 中的 entries 时将解析结果缓存到一个entry 数组,下次调用 PrevImpl() 直接读取缓存(从数组后面开始)
- 由此可知 PrevImpl 比 NextImpl 要耗时,需要移动 restart point 并解析跳过,NextImpl 按 entry 依次往下解析即可。
- **SeekForPrevImpl(target)**
1. 类似 SeekImpl,跳转到大于等于 target 的位置
2. 循环调用 PrevImpl(),直到当前 key 小于等于 target
> [!summary]
> - Seek类实现先定位、移动到目标 restart interval;
>- PrevImpl 需要先移动到当前 entry 之前的 restart interval,再顺序解析,并跳过当前 entry 之前的。
> - 缓存跳过的 entries,下次 PrevImpl 可以直接读取缓存(只要 prev 范围还在当前 restart interval 内)。
> - data block 的访问会记录入统计信息(读放大)
> - 依赖 `read_amp_bytes_per_bit`选项
### MetaBlockIter
- 继承自 `BlockIter<Slice>`,迭代 meta block。
- 与 DataBlockIter 的区别:
- meta block 存储的是 user keys(无 seqno),固定使用 BytewiseComparator。
- 通过 `raw_key_.SetIsUserKey(true)` 设置
- PrevImpl 无缓存优化。
- 相比 data block,调用不频繁?
- 对 meta block 的访问不记录统计信息。
### Block 类
- Block 在内存中的表示,已解压缩且解析过。
- 涵盖 data block、index block、meta block 等。
- 核心成员:
- **`contents_`**:`BlockContents`, block 底层数据
- **`data_`**:`const char *`,即 `contents_.data.data()`
- **`size_`**:`size_t`,即 `contents_.data.size()`
- **`restart_offset_`**:`uint32_t`,restart array 在 `data_` 中的偏移
- **`num_restarts_`**:`uint32_t`,restart points 的数量
- **`data_block_hash_index_`**:`DataBlockHashIndex`,block 的 hash 索引
- 在构造函数中解析 block, 初始化 `restart_offset_`,`num_restarts_`,`data_block_hash_index_`等成员。
- 提供了方法创建各类 BlockIter(如 DataBlockIter、MetaBlockIter、IndexBlockIter)。
-------
# Index Block
- index block 一个索引条目对应一个 data block,参数 `BlockBasedTableOptions::index_type` 设置不同的索引类型,默认 `kBinarySearch`。
- 一个索引条目包含 Key 和 Value,所有的条目按照 data block 的格式存储到一个或多个 blocks(partitioned) 中。
- key:满足大于等于 block last key 且 小于下一个 block first key
- value:block 在文件内的偏移和大小等信息
- 
- **IndexType**:
- **`kBinarySearch`**:默认类型,一个 sst 中存在一个 index block。
- 一条 index entry:
- key:基于 block last key
- value:block handle
- **`kHashSearch`**:需要配置 `Options.prefix_extractor`
- **`kTwoLevelIndexSearch`**:两层索引,存在多个 index blocks(partitions)以及在其上的一个 top-level index block。
- second level index blocks 始终使用 block cache。
- **`kBinarySearchWithFirstKey`**:类似 `kBinarySearch`,但索引内容里包含了每个 block 的首个 key。
- 针对 short range scans 场景可以减少读放大。
- 否则迭代器 seek 时,每个 L0 文件 和 L0之外的每层,都至少要读取一个 block(MergingIterator MinHeap 逻辑)
- 可能会导致索引数据很大,特别当 keys 很长时。
- PR: [Add an option to put first key of each sst block in the index #5289](https://github.com/facebook/rocksdb/pull/5289)
## IndexValue
- IndexValue: ^fbbace
- **`handle`**:`BlockHandle`
- **`offset_`**:`uint64_t`,block 在文件内的偏移
- **`size_`**:`uint64_t`,block 的大小
- **`first_internal_key`**:`Slice`,IndexType 为 `kBinarySearchWithFirstKey` 时启用
### 编码
- IndexValue::EncodeTo()
- 不使用 `value_delta_encoding`时:
- block first key:仅当 `BlockBasedTableOptions::IndexType` 为 `kBinarySearchWithFirstKey` 才包含,格式为 Internal key。
| block offset(Varint64) | block size(Varint64) | block first key (可选) |
| ---------------------- | -------------------- | -------------------- |
- 使用 `value_delta_encoding`:
- 条件:`table_opt.format_version >= 4 && !table_opt.block_align`
- 仅在同一个 restart interval 中第二个 entry 开始才使用该格式(==隐式条件==)
- 不需要编码 BlockHandle 的 offset
- 解码时根据上一个 block 的 offset 和 size,可以计算出来
- block size delta:当前 block size 跟上一个 block 的差值
| block size delta(Varsignedint64) | block first key可选) |
| -------------------------------- | ------------------ |
```cpp
// IndexValue::DecodeFrom()
handle = BlockHandle(previous_handle->offset() + previous_handle->size() +
BlockBasedTable::kBlockTrailerSize,
previous_handle->size() + delta);
```
```
restart_point 0: k, v (off, sz), k, v (delta-sz), ..., k, v (delta-sz)
restart_point 1: k, v (off, sz), k, v (delta-sz), ..., k, v (delta-sz)
...
restart_point n-1: k, v (off, sz), k, v (delta-sz), ..., k, v (delta-sz)
where, k is key, v is value, and its encoding is in parenthesis.
```
## IndexBuilder
- builder 的基类,主要是定义了统一的 virtual 方法列表。
- **IndexBlocks**
- **`index_block_contents`**: `Slice`
- index block 数据
- **`meta_blocks`**:`map<string, Slice>`
- 可选地,primary index block 的 metadata,hash index 专用
- **virtual AddIndexEntry() = 0**
- 添加一个 block index entry。
- 参数:
- **`last_key_in_current_block`**:`std::string *`
- 当前 block 的最后一个 key,用作 index key,可以会被具体的实现优化修改。
- **`first_key_in_next_block`**:`const Slice *`
- 下一个 block 的首 key,可以为空(当前为最后一个 block)。
- **`block_handle`**: `const BlockHandle&`
- block 的地址和大小,index entry 的 value。
- **virtual OnKeyAdd(key) {}**
- 每当 BlockBasedTableBuilder 添加了一个 key 后调用,用以通知,默认无操作。
- **virtual Status Finish() = 0**
- 完成索引构建。
- 可能会返回 `InComplete`,表示是 partitioned index,调用者需要继续调用 Finish 直到返回 `Status::OK`。
- 参数:
- **`index_blocks`**:`IndexBlock *`
- 填充构建结果。
- **`last_partition_block_handle`**:`const BlockHandle&`
- 上一个构建完成的 index partition 的 block handle,用于构建 2nd level index。
- **ShouldUseKeyPlusSeqAsSeparator()**
- 给出当前 block 的 last key 和 下一个 block 的 first key,返回 index key 是否需要添加 seq(即使用 internal key)。
- 只有当两个 key 的 user key 相同时才需要(此时需要添加 seq 才能做出区分)
## ShortenedIndexBuilder
- 优化:
- 默认 `index_block_restart_interval` 为 1, 即没有前缀压缩,加快搜索(二分即可,不要额外线性扫描)。
- 使用缩短后的 index key(不使用 full key,只需要足够分割两个 block 即可)。
- **IndexShorteningMode** 配置:
- `kNoShortening`:使用 full key
- `kShortenSeparators`:除文件的最后一个 block 外,其他 blocks 使用 shorten。默认。
- `kShortenSeparatorsAndSuccessor`:所有 blocks 都使用 shorten。
- 核心成员:
- **`seperator_is_key_plus_seq_`**: `bool`
- index key 是否需要包含 seqno,默认为 false
- 当 ShouldUseKeyPlusSeqAsSeparator() 返回 true,该成员不可逆地改为 true。
- 当两个 block 边界的 user key 相同时,必须添加 seqno 才能区分。
- 每次 AddIndexEntry() 进行检查更新
- **`index_block_builder_`**:`BlockBuilder`
- 用于构建 index block,index key 格式为 internal
- **`index_block_builder_without_seq_`**:`BlockBuilder`
- 用于构建 index block,index key 格式为 user,仅在`seperator_is_key_plus_seq_` 为 true 之前才更新
- Finish() 时,两个 builders 只使用其中的一个,取决于 `seperator_is_key_plus_seq_`的最终值。
- **`last_encoded_handle_`**:`BlockHandler`
- 上一个 block 的 handler,用于实现 index value(offset)的 delta encoding
- PR:[Index value delta encoding #3983](https://github.com/facebook/rocksdb/pull/3983)
- **FindShortestInternalKeySeparator(start, limit)**
- 查找位于`[start, limit)` 范围内的最短 key,更新回 start 中。
- 如果 start 确实缩短(更新了),会重设 start 的 type 和 seqno 为排序上最小的。
- 即 `(kMaxSequenceNumber, kValueTypeForSeek)`
- **AddIndexEntry()**
1. 根据是否为最后一个 block、IndexShorteningMode 确定实际的 index key;
2. 编码 [[block based#^fbbace|IndexValue]];
3. 将 index key 和 index value 写入 BlockBuilder
> [!summary] ShortenedIndexBuilder
> - 使用缩短后的 index key,并尽量不携带 seqno。
> - 优势:减少索引大小
> - 劣势:block的范围变得不再精准,可能引发不必要的读取
> - 例如两个 block: `[ ... AAA] B [C ...]`,B 为缩短后的索引 key,则 seek `(AAA, B]` 范围内的 keys 时,会读取第一个 block(这是不必要的)
> - 特例:文件末尾的 block
> - 默认配置使用 kShortenSeparators,最后一个 data block 的 index key 使用 full key,针对 last block 修复该问题。
## PartitionedIndexBuilder
- 两级索引,避免单个 index block 过大。
```
[index block - 1st level]
[index block - 1st level]
...
[index block - 1st level]
[index block - 2nd level]
```
- 每级均使用 ShortenedIndexBuilder 构建。
- 核心成员:
- **`entries_`**: `list<Entry>`, partition builders 列表
- Entry:表示一个 partition
- **`key`**: `string`,parition 的 last key
- **value**:`unique_ptr<ShortendIndexBuilder>`,partition 对应的 builder
- **`index_block_builder_`**, **`index_block_builder_without_seq_`**
- top-level builder,两个 builder 作用类似 `ShortendIndexBuilder`
- **`seperator_is_key_plus_seq_`**:`bool`
- 作用同 `ShortendIndexBuilder`,每次 AddIndexEntry 时更新,其值为所有 sub indexes `seperator_is_key_plus_seq_` 的==或==。
- 即只要有一个为 true,两级索引都将为 true。整个文件统一,体现在 `TableProperties::index_key_is_user_key` 中。
- **`sub_index_builder_`**:`ShortenedIndexBuilder *`
- 当前构建中的 partition index builder
- **`partition_cut_requested_`**:`bool`
- 外部请求切换到新的 index partition
- **`cut_filter_block`**:`bool`
- 标记,通知外部(PartitionedFilterBlockBuilder)切换到新 filter partition
- **`flush_policy_`**:`unique_ptr<FlushBlockPolicy>`
- block 的 flush/切换策略,默认按 `metadata_block_size` 进行flush
- Partition flush:
- index partition 和 filter parition 的切换会进行对齐,由 index builder 做决定,然后通过`cut_filter_block`同步给 filter builder。切换原因由两个:
- 来自外部请求 (`partition_cut_requested_`标记 ,filter partition keys 到达固定数量后设置)
- 内部的 block flush policy(block 大小达到 `metadata_block_size`限制)
- **AddIndexEntry()**
1. 检查当前 partition 是否需要 flush(条件见上述),需要则进行 flush
- flush:将当前 `sub_index_builder_` 追加到 `entries_` 后面,并设置 `cut_filter_block` 通知 filter builder
2. 按需创建新的 `sub_index_builder_` (首次调用或者刚 flush 完会为 nullptr)
3. 调用 `sub_index_builder_->AddIndexEntry()`
4. 从 `sub_index_builder_`同步 `seperator_is_key_plus_seq_`
- 如果有变化,则需要更新 `flush_policy_`,让其使用最新的 BlockBuilder,来正确计算当前 block 的大小
- **Finish()**
- 每次调用只 Build/Finish 一个 partition(`entries_`的头部),返回 Incomplete
- 需要向 sub-builder 同步 `seperator_is_key_plus_seq_`,保证全文件统一
- 将对上一个 partition block 的索引写入 top-level buidler
- key 为 该 partition 的 last key
- value 为 partition 的 BlockHandle,并使用 value delta encoding
- 调用 BlockBuilder::Add() 直接写(因为 key 已经 shorten 过)
- 最后一次调用 Finish top-level index builder(`entries_`为空),并返回 Status::OK
## HashIndexBuilder
- 对应 `IndexType::kHashSearch`,前置要求:设置 `prefix_extractor`。
- 索引由两部分组成:
- primary index:ShortenedIndex, 单 block,等同于 `IndexType::kBinarySearch`
- prefix index:
- index key:文件内所有不相同的 key prefixes
- index value:prefix 覆盖的 data blocks 范围
- 从文件内第几个 data block 开始及覆盖的 blocks 数量
- 该 index 数据存储在两个 meta blocks 中。
- 一个 meta block (`kHashIndexPrefixesBlock`) 记录所有的 prefixes
- 另一个 `kHashIndexPrefixesMetadataBlock` 存储每个 prefix 的大小及 index value。
- 核心成员:
- **`primary_index_builder_`**:`ShortenedIndexBuilder`,构建 primary index
- **`current_restart_index_`**:`uint64_t`,当前是第几个 data block
- 每次 AddIndexEntry() 时加一即可
- **`pending_entry_index_`**:`uint64_t`,出现新 prefix 时位于第几个 data block。
- 在 OnKeyAdded 检测 Prefix 变化
- **`pending_block_num_`**:`uint64_t`,当前 prefix 占据了多少 data blocks。
- **AddIndexEntry()**
- 更新 primary index
- 更新 `current_restart_index_`
- **OnKeyAdded()**
- 检测 prefix 变化,变化后则更新 hash index;不变化则尝试累加 `pending_block_num_`(需要检测`current_restart_index_`变化)
> [!summary] Prefix Hash Index
> - 加速 prefix seek(快速定位某个 prefix 位于文件的哪些 data blocks 内),需要配合 `prefix_extractor` 使用
> - primary index 单 block,有过大的风险
- 如何根据 block index 快速定位 block handle:
- hash index 的 `index_block_restart_interval` 只能为 1, 查找 restart array 即可。
```cpp
// BlockBasedTableFactory::InitializeOptions()
if (table_options_.index_type == BlockBasedTableOptions::kHashSearch &&
table_options_.index_block_restart_interval != 1) {
// Currently kHashSearch is incompatible with
// index_block_restart_interval > 1
table_options_.index_block_restart_interval = 1;
}
```
---
# Index Reader
## IndexBlockIter
- 继承自 `BlockIter<IndexValue>`,即 value 类型为 [[block based#^fbbace|IndexValue]],用来迭代一个 index block 中的 index entries。
- key 的格式(是否包含 seqno)取决于 `TableProperties::index_key_is_user_key`
- Initialize函数的 `key_includes_seq` 参数
- 核心成员:
- **`value_delta_encoded_`**:`bool`,是否使用了 value delta 编码,来自 `TableProperties::index_value_is_delta_encoded`
- **`have_first_key_`**:`bool`,index value 是否包含了 block 首 key,解码时需要
- 当 `IndexType == kBinarySearchWithFirstKey` 时为true
- **`prefix_index_`**:`BlockPrefixIndex`,hash index
- 当 `IndexType == kHashSearch`不为空
- 查询 key prefix 可能存在哪些 data blocks 中
- **`global_seqno_state_`** :`unique_ptr<GlobalSeqnoState>`
- 文件存在 global seqno 且 `have_first_key_` 为 true 时有效,用于实现对 `first_internal_key`的 seqno 进行重写。
- state 中的 `first_internal_key` 成员保存每次重写后的结果,解码后的 IndexValue 中的 `first_internal_key` Slice 再指向它。
- **`decoded_value_`**:`IndexValue`
- 当前 IndexValue,当 value 解码逻辑比较特殊时使用
- 比如使用了 value delta 编码、需要对 `first_internal_key` 应用 global seqno 等。
- **CompareBlockKey(block_index, target)**
- 第 `block_index` 个 data block 的 last key 与 target 比较
- 仅 hash index 使用,restart interval 只能为 1,通过 restart array 定位该 block 的 index entry。
- **BinaryBlockIndexSeek()**
- 对一组 `block_ids` 进行二分搜索,找到首个大于等于 target 的 block
- 仅 hash index 使用
- **PrefixSeek()**
- 通过 hash index 获取目标 prefix 可能存在的 blocks 范围,再调用 BinaryBlockIndexSeek 二分搜索,返回 target 所属的 block index
- **SeekImpl(target)**
1. 如果有 hash index,则使用 PrefixSeek,否则使用 [[block based#^9e6711|BinarySeek]]
2. 调用 [[block based#^471e60|FindKeyAfterBinarySeek]] 移动到目标 entry
> [!summary] IndexBlockIter Summary
> - seek 会使用 index block 特有的 hash index
> - 不存在则退化为二分搜索 restart array
> - 解码特殊( delta value encoding)
> - 同 MetaBlockIter,没有 prev cache。
> - 对 `first_internal_key` 应用 global seqno
> - index key 部分由 BlockIter 基类负责
## PartitionedIndexIterator
^d722d4
- 迭代 partitioned index,继承自 `InternalIteratorBase<IndexValue>`
- 核心成员:
- **`index_iter_`**:`unique_ptr<InternalIteratorBase<IndexValue>>`
- top-level index block 的 IndexBlockIter
- **`block_iter_`**:`IndexBlockIter`
- 当前的 second-level index block iter
- **`block_iter_points_to_real_block_`**:`bool`
- `block_iter_` 是否已经初始化
- **`prev_block_offset_`**:`uint64_t`,`block_iter`所属 partition block 的 offset
- 用途:当初始化 `block_iter_` 时,如果上次的 offset 跟本次的目标 block offset 一样,则不用重复初始化
- **InitPartitionedIndexBlock()**
- 读取 `index_iter_` (top-level)当前指向的 partition,读取该 parition block 并初始化`block_iter_`
- **FindBlockForward()**
- 当前 `block_iter_`(即 partition) 已经迭代完成,切换到下一个==非空的== parttition
- 切换 parition:对`index_iter_`调用 Next,并初始化 `block_iter_`执行新 partition
- **FindKeyForward()**
- 检测当前 partition 是否已经迭代结束,结束的话调用 FindBlockForward() 切换到下一个partition
- **SeekImpl(target)**
1. 两层 seek:
- 先对`index_iter_`进行 seek,初始化 `block_iter_`指向当前 partition;
- 再对 `block_iter_`调用 seek
2. 调用 FindKeyForward 跳过空 partition(迭代不出数据的)
- 如果 target 为 nullptr,则 SeekToFirst
- **Next()**
1. 对 `block_iter_`调用 Next()
2. 调用 FindKeyForward() ,跳过空 partition
> [!summary] PartitionedIndexIterator
> - Seek 依次对两层 index 进行 seek
> - Next 操作需要处理 partition 切换
> - 切换新 partition 有 IO 操作( InitPartitionedIndexBlock 函数中)
## TwoLevelIndexIterator
- 功能上等同于 [[block based#^d722d4|PartitionedIndexIterator]]。
- 区别在于所有 partition blocks 都已经从磁盘读取并缓存(partition map 中),切换 partition 时不需要 IO(从 partition map 中查找即可)。
- partition map: `Map<uint64_t, CachableEntry<Block>>`,key 是 partition block offset
- 典型应用场景:`pin_l0_filter_and_index_blocks_in_cache`
❓是否可以将 PartitionedIndexIterator 实现为一个 TwoLevelIndexIterator
## IndexReader
- 接口类,定义了访问 index 的方法列表。
- **NewIterator()**
- 构建 index 迭代器。返回 `InternalIterBase<IndexValue> *`
- **ApproximateMemoryUsage()**
- 报告 reader 占用了多少内存(不含 block cache 中分配的)
## IndexReaderCommon
^f01308
- 继承了 IndexReader,主要管理 top-level index block,包含 cache、pin 等逻辑。
- 核心成员:
- **`index_block_`**:`CachableEntry<Block>`,可能为空
- **GetOrReadIndexBlock()**
- 如果 `index_block_` 为空,则进行读取(如果开启 `cache_index_and_filter_blocks`则会走 block cache)。
- 不为空,直接返回其引用,无 IO 和 cache 操作
- `index_block_` 的几种情况:
- `cache_index_and_filter_blocks` 为 false,如果不为空则直接持有 block;
- 会显著影响每个 TableReader(打开的 SST 文件) 的内存占用
- `cache_index_and_filter_blocks` 为 true 且 `pin_top_level_index_and_filter` 也为 true,则持有 cache handle(实现了 reader pin 住 cache)
## PartitionIndexReader
- table/block_based/partitioned_index_reader.cc
- 继承自 `IndexReaderCommon`。
- **CacheDependencies()**
- 读取并将所有 partition blocks 加载到 block cache 中
- 先读取并迭代 top-level index block
- 如果 pin 参数为 true,则持有 cache handle (保存到 `partition_map_`中)
- `partition_map_`要么为空要么包含所有 partitions
- **NewIterator()**
- 构建 index 迭代器,根据 `partition_map_`是否为空,创建 TwoLevelIndexIterator 或者 PartitionedIndexIterator
## BinarySearchIndexReader
- **NewIterator()**
- 单 index block,直接在 index block 之上创建 IndexBlockIter
## HashIndexReader
- **NewIterator()**
- 同 BinarySearchIndexReader,单 index block,直接在 index block 之上调用 NewIndexIterator 创建 IndexBlockIter,但携带了 prefix_index
## 总结
```mermaid
classDiagram
direction TD
class IndexReader {
<<interface>>
+InternalIteratorBase~IndexValue~* NewIterator()
+CacheDependencies()
}
IndexReader <|-- IndexReaderCommon: 实现
class IndexReaderCommon {
#ReadIndexBlock()
#GetOrReadIndexBlock()
-const BlockBasedTable* table_
-CachableEntry~Block~ index_block_
}
IndexReaderCommon <|-- PartitionIndexReader: 继承
IndexReaderCommon <|-- BinarySearchIndexReader: 继承
IndexReaderCommon <|-- HashIndexReader: 继承
class PartitionIndexReader {
- UnorderedMap~uint64_t, CachableEntry~Block~~ partition_map_;
}
class HashIndexReader {
- std::unique_ptr~BlockPrefixIndex~ prefix_index_;
}
PartitionIndexReader --> PartitionedIndexIterator: 创建
PartitionIndexReader --> TwoLevelIndexIterator: 创建
BinarySearchIndexReader --> IndexBlockIter: 创建
HashIndexReader --> IndexBlockIter: 创建
```
- TableReader 根据 index_type 创建不同的 IndexReader
- index_type 存储在 sst 的 table_properties 中
```c++
const std::string BlockBasedTablePropertyNames::kIndexType =
"rocksdb.block.based.table.index.type";
```
- PartitionIndexReader 根据 partition blocks 是否已全部 pin 在缓存中,来创建不同的 index iterator(TwoLevelIndexIterator 或者 PartitionedIndexIterator)
- BinarySearchIndexReader 和 HashIndexReader 都是单 index block,创建 index iterator 时区别在于有没有 hash prefix index
----------------
# Filter Block
## FilterBlockBuilder
- 抽象类,提供了构建 filter 的相关方法。
- **void Add(const Slice& key) = 0**
- 添加一个 key
- **Slice Finish() = 0**
- 完成构建,并返回构建完的数据
- 参数:
- **`tmp`**: `const BlockHandle& tmp`
- 仅对 partitioned filter 有效,表示上一个 filter partition 的 block handle,用于构建 filter partitions 的索引。
- **`status`**:`Status *`,状态码
- **`filter_data`**:`std::unique_ptr<const char[]>*`
- 如果不为空,将构建完的数据转移给 filter_data 即 caller
- 返回值:构建完后的数据的 Slice(即数据的引用)
- **bool isEmpty() = 0**
- empty:即没有添加过 key
- **Status MaybePostVerifyFilter(const Slice& filter_content)**
- 构建完后对 filter 数据进行校验是否损坏
- 当 `BlockBasedTableOptions::detect_filter_construct_corruption` 为 true 时启用,默认不启用
- PR: [Detect (new) Bloom/Ribbon Filter construction corruption #9342](https://github.com/facebook/rocksdb/pull/9342)
## FullFilterBlockBuilder
- FilterBlockBuilder 的实现类,主要用途:
- 构建 一个 SST 的单个 filter block。
- 启用 partitioned filter 后,用于构建一个 partition。
> [!note] SeekForPrev 在 prefix mode 下的问题
> - 为了保证 SeekForPrev 在 prefix mode + partitioned filter 下能正确工作,每个 filter partition 需要包含上一个 partition last key 的 prefix (*each filter partition should include the bloom of the prefix of the last key in the previous partition*)
>
>- 原因:
> - prefix seek 时可能会定位到下一个 partition,例如 filter partition 最后一个 key 是 `abc`,Seek `abcd`时,先查找 top-level index,`abcd > abc`,因此会定位到下一个 partition,PrefixMayMatch 可能就会返回 false。
>
> - PRs:
> - [Fix a bug for SeekForPrev with partitioned filter and prefix #8137](https://github.com/facebook/rocksdb/pull/8137)
> - [Fix major bug with prefixes, SeekForPrev, and partitioned filters ](https://github.com/facebook/rocksdb/pull/12872)
> - [Reduce cases of impacted performance from bug fix #12874](https://github.com/facebook/rocksdb/pull/12874)
- 核心成员:
- **`filter_bits_builder_`**:`unique_ptr<FilterBitsBuilder>`,内存中的 filter 构建类
- 通常为 [[bloom filter#^34417e|FastLocalBloomBitsBuilder]]
- **`prefix_extractor_`**:`const SliceTransform *`,来自 Options,不为空的话会将 key 的前缀加入 filter
- **`whole_key_filtering_`**:`bool`,来自 Options,将完整 key 加入 filter(而不仅仅是 key 的前缀),默认 true
- **`last_prefix_recorded_`**:`bool`,确保一个 partition 只记录一次 `last_prefix`
- **`last_prefix_str_`**:`bool`,上一个 partition 的最后一个 prefix,每次 AddPrefix() 时更新
- **`last_whole_key_recorded_`**, **`last_whole_key_str_`**:
- whole key 去重,只有跟上次 whole key 不同才添加到 filter
- 这是因为如果 whole keys 之间穿插了 prefix,则 FilterBitsBuilder 无法准确去重(依赖判断上一次的 hash 值)
- PR:[Skip duplicate bloom keys when whole_key and prefix are mixed #3764](https://github.com/facebook/rocksdb/pull/3764)
- **void AddKey(const slice& key)**
- 调用 `filter_bits_builder_->AddKey(key)`,将完整的 key 加入 filter
- **void AddPrefix(const Slice& key)**
- 添加 key 的前缀加入
- **void Add(const Slice& key)**
- 分别判断和添加 prefix 和 whole key
- 添加 prefix 的前提:`prefix_extractor_ && prefix_extractor_->InDomain(key_without_ts)`
- 添加 whole key 前提:`whole_key_filtering_` 为 true 且与上次 whole key 不同(去重)
- **void Reset()**
- 设置 `last_whole_key_recorded_` 和 `last_prefix_recorded_` 为 false
- PartitionedFilterBlockBuilder 会重复利用同一个 FullFilterBlockBuilder 来构建不同的 partition,切换 partitition 时会调用 Reset
## PartitionedFilterBlockBuilder
- 继承了 FullFilterBlockBuilder,利用它构建 per partition 的 filter。
- 一个 SST 的 filter 数据组织为多个 filter partition blocks 加一个 filter index block
- 
- 读取时先根据 top-level index block 查询目标 key 位于哪个 filter partition,再读取该 partition。
- 核心成员:
- **`index_on_filter_block_builder_`**, **`index_on_filter_block_builder_without_seq_`**
- filter 的 top-level index builder
- **`p_index_builder_`**:`PartitionedIndexBuilder *`
- 关联的 PartitionedIndexBuilder,保持两类 builder 的 partition 数量相同
- **`filters`**:`deque<FilterEntry>`
- FilterEntry: `<index_key, filter_data, filter>`
- 每个 partition 的构建结果
- `index_key`来自跟其同步构建的 `p_index_builder_`(Shorten 后的),用于构建 top-level filter index
- **`keys_per_partition_`**:`uint32_t`
- 每个 filter partition 预期包含多少个 key, 超出后会切换到新 partition
- 计算方式:`partition_size * 8 / bits_per_key`
- `partition_size`:近似于 `metadata_block_size` 配置
- **MaybeCutAFilterBlock()**
- 每次添加 key 时都会调用,检测满足 `keys_per_partition_`,则向 `p_index_builder_` 请求切换 partition
- `p_index_builder_`切换 partition 后,构建当前 filter partition,放入 `filters` 中缓存(内存中),并且 Reset 当前 parition 的 filter builder。
- **Add(key)**
- 调用 MaybeCutAFilterBlock(),执行 partition 切换逻辑
- 添加到当前 parition (调用 FullFilterBlockBuilder::Add)
- **Slice Finish()**
- 类似于IndexBuilder,每次调用 Finish 仅返回一个 partition 的构建结果,且 status 返回 InComplete,最后一次调用时构建 top-level index block,并返回 OK。
- 构建 filter partition 时传入上一个 partition 的 handler,写入 index builder
> [!summary] PartitionedFilterBlockBuilder
> - partition 切换需要跟 index builder 进行同步
> - top-level index 可能基于 internal key 构建
## FilterBlockReader
- 所有 filter reader 的基类,用于读取一个 SST 的 filter。
- **bool KeyMayMatch(key)**
- 判断 user key 是否可能存在。
- 对于 partitioned filter 需要额外提供 internal key(查询 top-level index)。
- **void KeysMayMatch(range)**
- 检查 MGet 对应的多个 keys 是否存在,对于确定不存在的从 range 中进行剔除
- 提供了默认实现:迭代每个 key 并依次调用 KeyMayMatch
- **PrefixMayMatch(prefix)**,**PrefixesMayMatch(range)**
- 类似 KeyMayMatch 和 KeysMayMatch,检查 prefix 或者 range 内 keys 的前缀是否可能存在
## FilterBlockReaderCommon
- 类似于 [[block based#^f01308|IndexReaderCommon]],管理 full filter block 或者 filter 的 top-level index block(启用 partitioned filter 后)。
- 核心成员:
- **`filter_block_`**:`CachableEntry<TBlocklike>`
- TBlocklike 模版类,`ParsedFullFilterBlock` 或者是 `Block_kFilterPartitionIndex`
## FullFilterBlockReader
- 继承 FilterBlockReaderCommon,适用于 full filter。
- 类似于 FullFilterBlockBuilder,相比 FilterBitsReader 多了 whole key 和 prefix 的相关逻辑。
- 例如 `whole_key_filtering` 为 false,则 KeyMayMatch 始终返回 true(无 filter)。
## PartitionedFilterBlockReader
- 读取某个 SST 的 partitioned filter。
- 核心成员:
- **`filter_map_`**: `UnorderedMap<uint64_t, CachableEntry<ParsedFullFilterBlock>>
- 实现 pin filter partitions,例如开启 `pin_l0_filter_and_index_blocks_in_cache`
- CacheDependencies() 中填充
- **GetFilterPartitionHandle(ikey)**
- 从 top-level index 中搜索 ikey,返回对应 filter partition 的 BlockHandle
- `filter_block->NewIndexIterator()->Seek(ikey)`
- **GetFilterPartitionBlock(handle)**
- 根据 filter partition 的 BlockHandle 读取对应 block,返回 `CachableEntry<ParsedFullFilterBlock>`
- 先尝试从 `filter_map_` 中读取
- **MayMatch()**
1. 获取 top-level block:`FilterBlockReaderCommon::GetOrReadFilterBlock()`
2. 迭代 Seek top-level block,获取 partition BlockHandle:`GetFilterPartitionHandle()`
3. 读取 partition block:`GetFilterPartitionBlock()`
4. 查询 parition block: `FullFilterBlockReader::KeyMayMatch()`
> [!summary] PartitionedFilterBlockReader
> - 两级查询:先从 top-level 按 ikey 搜索 partition,再从 partition 中查询
> - 每个 partition 复用 FullFilterBlockReader 查询
## 总结
### Builders
```mermaid
classDiagram
direction TD
class FilterBlockBuilder {
<<interface>>
+Add()
+Finish()
+IsEmpty()
+ResetFilterBitsBuilder()
+MaybePostVerifyFilter()
}
FilterBlockBuilder <|-- FullFilterBlockBuilder: 实现
FullFilterBlockBuilder <|-- PartitionedFilterBlockBuilder: 继承
class FullFilterBlockBuilder {
-FilterBitsBuilder* filter_bits_builder_
}
class PartitionedFilterBlockBuilder {
-BlockBuilder index_on_filter_block_builder_
-std::deque<FilterEntry> filters
}
```
- FullFilterBlockBuilder 主要处理 whole key 和 prefix key 相关逻辑;
- PartitionedFilterBlockBuilder 利用父类的 FullFilterBlockBuilder 构建 per-partition,每个 partition 构建完都先缓存到内存中(data blocks 还没写完,无法确定 filter 的写入位置)。
### Readers
```mermaid
classDiagram
direction TD
class FilterBlockReader {
<<interface>>
+KeyMayMatch()
+KeysMayMatch()
+PrefixMayMatch()
+PrefixesMayMatch()
+RangeMayExist()
}
FilterBlockReader <|-- FilterBlockReaderCommon: 实现
class FilterBlockReaderCommon {
#ReadFilterBlock()
#GetOrReadFilterBlock()
-const BlockBasedTable* table_
-CachableEntry~Block~ filter_block_
}
FilterBlockReaderCommon <|-- FullFilterBlockReader: 继承
FilterBlockReaderCommon <|-- PartitionedFilterBlockReader: 继承
class PartitionedFilterBlockReader {
-UnorderedMap~uint64_t, CachableEntry~ParsedFullFilterBlock~~ filter_map_;
}
```
-----
# Meta Index block
- 用于按名字索引 meta block
- meta index block 里的每条 index entry 对应一个 meta block
- Index key:meta block 的名字
- Index value:block handle,meta block 的位置和大小

## MetaIndexBuilder
- 用于构建 meta index block,封装了 [[block based#^897012|BlockBuilder]],每条 entry 为 `<key, encoded_block_handle>`。
- key 为 meta block 的名字
- `block_restart_interval`固定为 1。
----
# BlockBasedTableBuilder
- 用于构建一个 SST 文件
- table/block_based/block_based_table_builder.h
-----
# BlockBasedTable
- Reader 类,用于读取一个 SST 文件。
- table/block_based/block_based_table_reader.h
## ApproximateMemoryUsage()
^8a19fc
- 包含以下内存占用:
- Rep 类大小
- FilterBlockReader 和 IndexReader 类大小
- UncompressionDictReader 类大小
- TableProperties 占用大小
--------
# TODO
- [ ] BlockBasedTableOptions
- [ ] BuildTable
- [x] block index 使用 ikey 还是 ukey
- 尽量使用 ukey,按需使用 ikey
- [ ] kMaxBlockSizeSupportedByHashIndex 为什么有这个限制
- [ ] BlockReadAmpBitmap
- [ ] `protection_bytes_per_key`
- [Add WriteOptions protection_bytes_per_key #10037](https://github.com/facebook/rocksdb/pull/10037)
- [Block per key-value checksum #11287](https://github.com/facebook/rocksdb/pull/11287)
- [ ] BlockPrefetcher
- [ ] RangeMayExist
- [ ] 自定义 table property
- [ ] PrefetchTail
- [x] 是否包含 filter/index partitions
- [ ] prefetch 的数据是否过大?
----
# 实验
- [x] 验证 `value_delta_encoding`
- 默认 `index_block_restart_interval = 1` 是否不起作用
- [x] 启用 PartitionedIndex 后,如果 data block 数量很少,是否也一定会有两级 index
- 是的,最坏情况一个 top-level index block 和一个 second-level index block
❓build table 时是否能识别这种情况,退化为非 partitioned?以及是否有必要