- https://github.com/facebook/rocksdb/wiki/DeleteRange-Implementation#tombstone-fragments
# Tombstone Fragments
- 问题:原始的 range tombstones 相互重叠无法二分搜索
- 例如 `[c, d)@4`, `[g, h)@7` 和 `[a, z)@10`, 查找 `e@1`
- 无论按 start key 还是 end key 排序后二分搜索都无法找到 `[a, z)@10`
- 如果使用 linear search,开销又会很大
- 解决:将 tombstone 切分为 seq 相同的多个连续区间即 fragment,让 framgments 之间无重叠
- 例如 `[a, d)@4`可等价拆分为 `[a, b)@4` 和 `[b, d)@4`。
- 上述的例子中可拆分为:
- `[a, c)@10`,`[c, d)@4`, `[c, d)@10`,`[d, g)@10`,`[g, h)@10`,`[g, h)@7`, `[h, z)@10`
- 算法: db/range_tombstone_fragmenter.cc.
---
# WritePath
## Memtable
- 写入到一个专用的 memtable,格式为 `start : end`
- 写入时不进行 fragment,而是在读的时候进行 fragment。
## SST Files
- 存储到专门的 tombstones meta-block 中。
- 与 memtable 不同,写入的是 fragmented 之后的。
- 第一个创建 table reader 时,会对 fragment 进行缓存。
---
# Read Path
## Point Lookups
- 先检查 `FragmentedRangeTombstoneIterator::MaxCoveringTombstoneSeqnum()`
- 如果找到了 tombstone,则不需要搜索 lower levels,因为他们的 merge operands / point values 都被删除了
- 只需要处理在 tombstone 之后写入的数据
## Range Scans
- 以 forward scan 为例,需要维护三个数据结构:
- **active heap**:按 end key 排序,fragmented tombstone iterators(**每 table 一个**)的小顶堆
- **active seqnum**:按 seq 排序,fragmented tombstone iterators 的有序集合
- **inactive heap**:按 start key 排序的 tombstone小顶堆
- active heap
- 其中的迭代器都指向了覆盖了最近 lookup ikey 的 tombstone fragments。
- actvie seqnum:
- 包含了 active heap 中的迭代器,虽然只使用它们的序列号
- inactive heap:
> contains iterators pointing at tombstone fragments that start after the most recent lookup key
- 其中的迭代器指向了最近 lookup ikey 之后的 tombstone fragments
- 一个迭代器不能同时出现在 active 和 inactive set 中。
```mermaid
block-beta
columns 1
MemTable["Memtable \n[a, b)@40\n[a, b)@35\n ."]
block: L0
1.sst["1.sst \n[a, c)@15\n[d, f)@20\n ."]
2.sst["2.sst \n[b, e)@5\n[e, x)@10\n ."]
end
```
- 示例图:一个 memtable,两个 **L0** 文件。
- 例如 DBIter 中的 merging iterator 指向 ikey `a@4`:
- active iteratorss 将会包含:
- `1.sst`的 tombstone iterator 指向了 `[a, c)@15`
- memtable tombstone iterator 指向了 `[a, b)@40`
- inactive iterators:
- `2.sst` 的 tombstone iterator 指向 `[b, e)@5`
- active seqnum set:`{15, 40}`
- 从以上结构可以得知,最大的 covering tombstone 拥有 seqnum 40,大于 4,因此 `a@4` 被删除了,我们需要检查另外一个 key。
- 接下来我们检查 `b@50`
- active heap:
- `1.sst` tombstone iterator 指向 `[a, c)@15`
- `2.sst` 指向 `[b, e)@5`
- inactive heap 为空
- active seqnum set: `{5, 15}`
- 因此 最大的 covering tombstone 拥有 seqnum 15,`b@50`没有被覆盖
- 详细实现:[db/range_del_aggregator.cc](https://github.com/facebook/rocksdb/blob/main/db/range_del_aggregator.cc)
---
# Compaction
- 在 flush 和 compactions,range tombstones 需要支持两个主要的操作:
1. 识别位于 snapshot stripe 中可以被删除的 point keys
- snapshot stripe:两个相邻的快照的 seqnum 范围
2. 往 output file 中写入 range tombstones
- 对于 1 ,使用跟 range scans 相同的数据结构,但是每个 snapshot stripe 一个。
- 迭代时跳过不在 snapshot stripe seq range 中的 tomebstones
- 对于 2,创建一个 merging iterator,包含了在 output file 范围内的所有 fragmented tombstone iterator, 创建 table builder 的 iterator 时传入它。
## File Boundaries
- 如果只有 point keys 数据,确定 SST file 的边界非常直观,只需要找到文件中最小和最大的 user key 即可。
- 但对于 range tombstones,就会变得复杂,一种简单策略是允许 tombstone 的 start 和 end key 扮演文件边界:
- start key 的 seqno 为 tombstone 的 seqno
- end key 的 seqno 为 `kMaxSequenceNumber`(同 user key 中第一个 ikey)
- 对于 L0 文件没有问题,但是其它对于层次需要保证各个文件范围不重叠。
- 如果 tombstone 范围很大,覆盖很多 keys,会导致创建非常大的 SST 文件。
- 为了解决这个问题,我们限制一个文件的最大 user key 不大于下一个文件的最小 user key
- 如果一个 range tombstone 跨越两个文件,文件最大 key 的 seq 和 type 会被设置为 `kMaxSequenceNumber` 和 `kTypeRangeDeletion`
- 即假装 tombstone 不会超过下一个文件的起始
- edge case:某个 user key 跨越了两个相同的文件,即一个文件的最大 ukey 是下一个文件的最小 ukey
- 在这种情况下,即使有 tombstone 覆盖了这个间隙,也不会改变文件的边界。
## Range Tombstone Truncation
- 在过去,如果一个 compaction input file 包含了一个 range tombstone,我们会将包含相同 tombstone 的其它文件也加入到 compaction。
- 问题:对于范围特别大的 tombstone,会导致非常大的 compaction。
- 解决:clean cut 停止在最大 key 具有 range tombstone footer 的文件
- footer: (kMaxSequenceNumber, kTypeRangeDeletion)
- 但这与 RocksDB 的另一个优化有冲突 :将 bottommost level 所有 key seqs 设置为0,如果这些 key 在该 level 只有一个版本。考虑以下例子:
- DB 有三个 levels(Lmax 为 2),没有快照。
- L1 有一个 range tombstone `[a, f)@10`
- L1 有两个文件:
- `1.sst`范围 `[a@5, c@3]`
- `2.sst`范围 `[e@20, g@7]`
- 如果 `2.sst` compacted 到 L2,它的内部 key `e@20` seqnum 会被设置为 0
- 用户创建迭代器并扫描到 `e@0` 时, `1.sst`中的 tombstone `[a, f)@10`被认定为 active,并且覆盖了 `e` ,导致 e 被错误认定为删除了。
- 为了防止这种情况,我们需要找到一种方法来防止 `1.sst` 中的 tombstone 覆盖 `2.sst` 中的 keys。更普遍地说,需要防止 range tombstones 覆盖他们所需文件的范围之外的 keys。
- 方案分为两个部分:
- internal key range tombstone boundaries
- atomic compaction units
## Iternal Key Range Tombstone Boundaries
- 直观的想法:按照文件的 user key boundaries 截断 range tombstone
- 问题:range tombstones 的 end key 是 exclusive 的,所以即使他们应该覆盖文件中的最大 key,截断后就不行了。
- 一个不直观的想法是使用 internal keys 作为 range tombstone boundaries。
- [ ] 对于前面 largest key 跨越两个 SST 文件的场景,tomebstone boundary 的 seqnum 为 largest key 的减一。
- 详细解释见 `TruncatedRangeDelIterator` 的注释
## Atomic Compaction Units
- 即使我们在内存中使用 internal keys 作为 range tombstone boundaries,在磁盘上我们仍然需要使用 user keys,因为这是原始格式所要求的。
- 这意味着在 compaction 时,需要将 internal key-truncated tombstone 转换为 user key-truncated tombstone。这就意味着之前的问题会重现。
- 从广义上讲,解决此问题的方法是在 compaction 截断 tombstone 时,使其超过文件的最大键。
- 一种简单的想法是使用 input level 的 compaction boundaries,但是由于我们可以在 tombstone 结束之前停止 compaction,我们可能会遇到如下情况:
1. DB 有三个 levels,无快照。tombstone `[a, g)@10`分割在两个 L1 文件之间:
- `1.sst`范围 `[a@4, c@6]`
- `2.sst`范围 `[e@12, k@7]`
2. compact `2.sst` 到 L2,key `e@12`输出到 `3.sst`并且 seqnum 被设置为 0
3. 另一个 compaction 将 `1.sst` compact 到 L2,范围为 `[a, f)`,这也引入了 `3.sst`. range tombstone 被截断为 `[a, f)@10`,可以覆盖 `e@0`,导致被错误地认定被 tombstone 覆盖。
- 问题的根源在于一个重复存在于多个文件的 tombstone,可以出现在边界重叠的不同 compactions 中。
- 为了解决这个,我们必须为涉及这些文件的所有 compactions 选择不相交的边界,我们称之为atomic compaction unit boundries。