1---
2title: "DeleteRange: A New Native RocksDB Operation"
3layout: post
4author:
5- abhimadan
6- ajkr
7category: blog
8---
9## Motivation
10
11### Deletion patterns in LSM
12
13Deleting a range of keys is a common pattern in RocksDB. Most systems built on top of
14RocksDB have multi-component key schemas, where keys sharing a common prefix are
15logically related. Here are some examples.
16
17MyRocks is a MySQL fork using RocksDB as its storage engine. Each key's first
18four bytes identify the table or index to which that key belongs. Thus dropping
19a table or index involves deleting all the keys with that prefix.
20
21Rockssandra is a Cassandra variant that uses RocksDB as its storage engine. One
22of its admin tool commands, `nodetool cleanup`, removes key-ranges that have been migrated
23to other nodes in the cluster.
24
25Marketplace uses RocksDB to store product data. Its key begins with product ID,
26and it stores various data associated with the product in separate keys. When a
27product is removed, all these keys must be deleted.
28
29When we decide what to improve, we try to find a use case that's common across
30users, since we want to build a generally useful system, not one that has many
31one-off features for individual users. The range deletion pattern is common as
32illustrated above, so from this perspective it's a good target for optimization.
33
34### Existing mechanisms: challenges and opportunities
35
36The most common pattern we see is scan-and-delete, i.e., advance an iterator
37through the to-be-deleted range, and issue a `Delete` for each key. This is
38slow (involves read I/O) so cannot be done in any critical path. Additionally,
39it creates many tombstones, which slows down iterators and doesn't offer a deadline
40for space reclamation.
41
42Another common pattern is using a custom compaction filter that drops keys in
43the deleted range(s). This deletes the range asynchronously, so cannot be used
44in cases where readers must not see keys in deleted ranges. Further, it has the
45disadvantage of outputting tombstones to all but the bottom level. That's
46because compaction cannot detect whether dropping a key would cause an older
47version at a lower level to reappear.
48
49If space reclamation time is important, or it is important that the deleted
50range not affect iterators, the user can trigger `CompactRange` on the deleted
51range. This can involve arbitrarily long waits in the compaction queue, and
52increases write-amp. By the time it's finished, however, the range is completely
53gone from the LSM.
54
55`DeleteFilesInRange` can be used prior to compacting the deleted range as long
56as snapshot readers do not need to access them. It drops files that are
57completely contained in the deleted range. That saves write-amp because, in
58`CompactRange`, the file data would have to be rewritten several times before it
59reaches the bottom of the LSM, where tombstones can finally be dropped.
60
61In addition to the above approaches having various drawbacks, they are quite
62complicated to reason about and implement. In an ideal world, deleting a range
63of keys would be (1) simple, i.e., a single API call; (2) synchronous, i.e.,
64when the call finishes, the keys are guaranteed to be wiped from the DB; (3) low
65latency so it can be used in critical paths; and (4) a first-class operation
66with all the guarantees of any other write, like atomicity, crash-recovery, etc.
67
68## v1: Getting it to work
69
70### Where to persist them?
71
72The first place we thought about storing them is inline with the data blocks.
73We could not think of a good way to do it, however, since the start of a range
74tombstone covering a key could be anywhere, making binary search impossible.
75So, we decided to investigate segregated storage.
76
77A second solution we considered is appending to the manifest. This file is
78append-only, periodically compacted, and stores metadata like the level to which
79each SST belongs. This is tempting because it leverages an existing file, which
80is maintained in the background and fully read when the DB is opened. However,
81it conceptually violates the manifest's purpose, which is to store metadata. It
82also has no way to detect when a range tombstone no longer covers anything and
83is droppable. Further, it'd be possible for keys above a range tombstone to disappear
84when they have their seqnums zeroed upon compaction to the bottommost level.
85
86A third candidate is using a separate column family. This has similar problems
87to the manifest approach. That is, we cannot easily detect when a range
88tombstone is obsolete, and seqnum zeroing can cause a key
89to go from above a range tombstone to below, i.e., disappearing. The upside is
90we can reuse logic for memory buffering, consistent reads/writes, etc.
91
92The problems with the second and third solutions indicate a need for range
93tombstones to be aware of flush/compaction. An easy way to achieve this is put
94them in the SST files themselves - but not in the data blocks, as explained for
95the first solution. So, we introduced a separate meta-block for range tombstones.
96This resolved the problem of when to obsolete range tombstones, as it's simple:
97when they're compacted to the bottom level. We also reused the LSM invariants
98that newer versions of a key are always in a higher level to prevent the seqnum
99zeroing problem. This approach has the side benefit of constraining the range
100tombstones seen during reads to ones in a similar key-range.
101
102![](/static/images/delrange/delrange_sst_blocks.png)
103{: style="display: block; margin-left: auto; margin-right: auto; width: 80%"}
104
105*When there are range tombstones in an SST, they are segregated in a separate meta-block*
106{: style="text-align: center"}
107
108![](/static/images/delrange/delrange_key_schema.png)
109{: style="display: block; margin-left: auto; margin-right: auto; width: 80%"}
110
111*Logical range tombstones (left) and their corresponding physical key-value representation (right)*
112{: style="text-align: center"}
113
114### Write path
115
116`WriteBatch` stores range tombstones in its buffer which are logged to the WAL and
117then applied to a dedicated range tombstone memtable during `Write`. Later in
118the background the range tombstone memtable and its corresponding data memtable
119are flushed together into a single SST with a range tombstone meta-block. SSTs
120periodically undergo compaction which rewrites SSTs with point data and range
121tombstones dropped or merged wherever possible.
122
123We chose to use a dedicated memtable for range tombstones. The memtable
124representation is always skiplist in order to minimize overhead in the usual
125case, which is the memtable contains zero or a small number of range tombstones.
126The range tombstones are segregated to a separate memtable for the same reason
127we segregated range tombstones in SSTs. That is, we did not know how to
128interleave the range tombstone with point data in a way that we would be able to
129find it for arbitrary keys that it covers.
130
131![](/static/images/delrange/delrange_write_path.png)
132{: style="display: block; margin-left: auto; margin-right: auto; width: 70%"}
133
134*Lifetime of point keys and range tombstones in RocksDB*
135{: style="text-align: center"}
136
137During flush and compaction, we chose to write out all non-obsolete range
138tombstones unsorted. Sorting by a single dimension is easy to implement, but
139doesn't bring asymptotic improvement to queries over range data. Ideally, we
140want to store skylines (see “Read Path” subsection below) computed over our ranges so we can binary search.
141However, a couple of concerns cause doing this in flush and compaction to feel
142unsatisfactory: (1) we need to store multiple skylines, one for each snapshot,
143which further complicates the range tombstone meta-block encoding; and (2) even
144if we implement this, the range tombstone memtable still needs to be linearly
145scanned. Given these concerns we decided to defer collapsing work to the read
146side, hoping a good caching strategy could optimize this at some future point.
147
148
149### Read path
150
151In point lookups, we aggregate range tombstones in an unordered vector as we
152search through live memtable, immutable memtables, and then SSTs. When a key is
153found that matches the lookup key, we do a scan through the vector, checking
154whether the key is deleted.
155
156In iterators, we aggregate range tombstones into a skyline as we visit live
157memtable, immutable memtables, and SSTs. The skyline is expensive to construct but fast to determine whether a key is covered. The skyline keeps track of the most recent range tombstone found to optimize `Next` and `Prev`.
158
159|![](/static/images/delrange/delrange_uncollapsed.png)	|![](/static/images/delrange/delrange_collapsed.png)	|
160
161*([Image source: Leetcode](https://leetcode.com/problems/the-skyline-problem/description/)) The skyline problem involves taking building location/height data in the
162unsearchable form of A and converting it to the form of B, which is
163binary-searchable. With overlapping range tombstones, to achieve efficient
164searching we need to solve an analogous problem, where the x-axis is the
165key-space and the y-axis is the sequence number.*
166{: style="text-align: center"}
167
168### Performance characteristics
169
170For the v1 implementation, writes are much faster compared to the scan and
171delete (optionally within a transaction) pattern. `DeleteRange` only logs to WAL
172and applies to memtable. Logging to WAL always `fflush`es, and optionally
173`fsync`s or `fdatasync`s. Applying to memtable is always an in-memory operation.
174Since range tombstones have a dedicated skiplist memtable, the complexity of inserting is O(log(T)), where T is the number of existing buffered range tombstones.
175
176Reading in the presence of v1 range tombstones, however, is much slower than reads
177in a database where scan-and-delete has happened, due to the linear scan over
178range tombstone memtables/meta-blocks.
179
180Iterating in a database with v1 range tombstones is usually slower than in a
181scan-and-delete database, although the gap lessens as iterations grow longer.
182When an iterator is first created and seeked, we construct a skyline over its
183tombstones. This operation is O(T\*log(T)) where T is the number of tombstones
184found across live memtable, immutable memtable, L0 files, and one file from each
185of the L1+ levels. However, moving the iterator forwards or backwards is simply
186a constant-time operation (excluding edge cases, e.g., many range tombstones
187between consecutive point keys).
188
189## v2: Making it fast
190
191`DeleteRange`’s negative impact on read perf is a barrier to its adoption. The
192root cause is range tombstones are not stored or cached in a format that can be
193efficiently searched. We needed to design DeleteRange so that we could maintain
194write performance while making read performance competitive with workarounds
195used in production (e.g., scan-and-delete).
196
197### Representations
198
199The key idea of the redesign is that, instead of globally collapsing range tombstones,
200 we can locally “fragment” them for each SST file and memtable to guarantee that:
201
202* no range tombstones overlap; and
203* range tombstones are ordered by start key.
204
205Combined, these properties make range tombstones binary searchable. This
206 fragmentation will happen on the read path, but unlike the previous design, we can
207 easily cache many of these range tombstone fragments on the read path.
208
209### Write path
210
211The write path remains unchanged.
212
213### Read path
214
215When an SST file is opened, its range tombstones are fragmented and cached. For point
216 lookups, we binary search each file's fragmented range tombstones for one that covers
217 the lookup key. Unlike the old design, once we find a tombstone, we no longer need to
218 search for the key in lower levels, since we know that any keys on those levels will be
219 covered (though we do still check the current level since there may be keys written after
220 the range tombstone).
221
222For range scans, we create iterators over all the fragmented range
223 tombstones and store them in a list, seeking each one to cover the start key of the range
224 scan (if possible), and query each encountered key in this structure as in the old design,
225 advancing range tombstone iterators as necessary. In effect, we implicitly create a skyline.
226 This requires significantly less work on iterator creation, but since each memtable/SST has
227its own range tombstone iterator, querying range tombstones requires key comparisons (and
228possibly iterator increments) for several iterators (as opposed to v1, where we had a global
229collapsed representation of all range tombstones). As a result, very long range scans may become
230 slower than before, but short range scans are an order of magnitude faster, which are the
231 more common class of range scan.
232
233## Benchmarks
234
235To understand the performance of this new design, we used `db_bench` to compare point lookup, short range scan,
236 and long range scan performance across:
237
238* the v1 DeleteRange design,
239* the scan-and-delete workaround, and
240* the v2 DeleteRange design.
241
242In these benchmarks, we used a database with 5 million data keys, and 10000 range tombstones (ignoring
243those dropped during compaction) that were written in regular intervals after 4.5 million data keys were written.
244Writing the range tombstones ensures that most of them are not compacted away, and we have more tombstones
245in higher levels that cover keys in lower levels, which allows the benchmarks to exercise more interesting behavior
246when reading deleted keys.
247
248Point lookup benchmarks read 100000 keys from a database using `readwhilewriting`. Range scan benchmarks used
249`seekrandomwhilewriting` and seeked 100000 times, and advanced up to 10 keys away from the seek position for short range scans, and advanced up to 1000 keys away from the seek position for long range scans.
250
251The results are summarized in the tables below, averaged over 10 runs (note the
252different SHAs for v1 benchmarks are due to a new `db_bench` flag that was added in order to compare performance with databases with no tombstones; for brevity, those results are not reported here). Also note that the block cache was large enough to hold the entire db, so the large throughput is due to limited I/Os and little time spent on decompression. The range tombstone blocks are always pinned uncompressed in memory. We believe these setup details should not affect relative performance between versions.
253
254### Point Lookups
255
256|Name	|SHA	|avg micros/op	|avg ops/sec	|
257|v1	|35cd754a6	|1.3179	|759,830.90	|
258|scan-del	|7528130e3	|0.6036	|1,667,237.70	|
259|v2	|7528130e3	|0.6128	|1,634,633.40	|
260
261### Short Range Scans
262
263|Name	|SHA	|avg micros/op	|avg ops/sec	|
264|v1	|0ed738fdd	|6.23	|176,562.00	|
265|scan-del	|PR 4677	|2.6844	|377,313.00	|
266|v2	|PR 4677	|2.8226	|361,249.70	|
267
268### Long Range scans
269
270|Name	|SHA	|avg micros/op	|avg ops/sec	|
271|v1	|0ed738fdd	|52.7066	|19,074.00	|
272|scan-del	|PR 4677	|38.0325	|26,648.60	|
273|v2	|PR 4677	|41.2882	|24,714.70	|
274
275## Future Work
276
277Note that memtable range tombstones are fragmented every read; for now this is acceptable,
278 since we expect there to be relatively few range tombstones in memtables (and users can
279 enforce this by keeping track of the number of memtable range deletions and manually flushing
280 after it passes a threshold). In the future, a specialized data structure can be used for storing
281 range tombstones in memory to avoid this work.
282
283Another future optimization is to create a new format version that requires range tombstones to
284 be stored in a fragmented form. This would save time when opening SST files, and when `max_open_files`
285is not -1 (i.e., files may be opened several times).
286
287## Acknowledgements
288
289Special thanks to Peter Mattis and Nikhil Benesch from Cockroach Labs, who were early users of
290DeleteRange v1 in production, contributed the cleanest/most efficient v1 aggregation implementation, found and fixed bugs, and provided initial DeleteRange v2 design and continued help.
291
292Thanks to Huachao Huang and Jinpeng Zhang from PingCAP for early DeleteRange v1 adoption, bug reports, and fixes.
293