github.com/cockroachdb/pebble@v1.1.1-0.20240513155919-3622ade60459/docs/RFCS/20221122_virtual_sstable.md (about) 1 - Feature Name: Virtual sstables 2 - Status: in-progress 3 - Start Date: 2022-10-27 4 - Authors: Arjun Nair 5 - RFC PR: https://github.com/cockroachdb/pebble/pull/2116 6 - Pebble Issues: 7 https://github.com/cockroachdb/pebble/issues/1683 8 9 10 ** Design Draft** 11 12 # Summary 13 14 The RFC outlines the design to enable virtualizing of physical sstables 15 in Pebble. 16 17 A virtual sstable has no associated physical data on disk, and is instead backed 18 by an existing physical sstable. Each physical sstable may be shared by one, or 19 more than one virtual sstable. 20 21 Initially, the design will be used to lower the read-amp and the write-amp 22 caused by certain ingestions. Sometimes, ingestions are unable to place incoming 23 files, which have no data overlap with other files in the lsm, lower in the lsm 24 because of file boundary overlap with files in the lsm. In this case, we are 25 forced to place files higher in the lsm, sometimes in L0, which can cause higher 26 read-amp and unnecessary write-amp as the file is moved lower down the lsm. See 27 https://github.com/cockroachdb/cockroach/issues/80589 for the problem occurring 28 in practice. 29 30 Eventually, the design will also be used for the disaggregated storage masking 31 use-case: https://github.com/cockroachdb/cockroach/pull/70419/files. 32 33 This document describes the design of virtual sstables in Pebble with enough 34 detail to aid the implementation and code review. 35 36 # Design 37 38 ### Ingestion 39 40 When an sstable is ingested into Pebble, we try to place it in the lowest level 41 without any data overlap, or any file boundary overlap. We can make use of 42 virtual sstables in the cases where we're forced to place the ingested sstable 43 at a higher level due to file boundary overlap, but no data overlap. 44 45 ``` 46 s2 47 ingest: [i-j-------n] 48 s1 49 L6: [e---g-----------------p---r] 50 a b c d e f g h i j k l m n o p q r s t u v w x y z 51 ``` 52 53 Consider the sstable s1 in L6 and the ingesting sstable s2. It is clear that 54 the file boundaries of s1 and s2 overlap, but there is no data overlap as shown 55 in the diagram. Currently, we will be forced to ingest the sstable s2 into a 56 level higher than L6. With virtual sstables, we can split the existing sstable 57 s1 into two sstables s3 and s4 as shown in the following diagram. 58 59 ``` 60 s3 s2 s4 61 L6: [e---g]-[i-j-------n]-[p---r] 62 a b c d e f g h i j k l m n o p q r s t u v w x y z 63 ``` 64 65 The sstable s1 will be deleted from the lsm. If s1 was a physical sstable, then 66 we will keep the file on disk as long as we need to so that it can back the 67 virtual sstables. 68 69 There are cases where the ingesting sstables have no data overlap with existing 70 sstables, but we can't make use of virtual sstables. Consider: 71 ``` 72 s2 73 ingest: [f-----i-j-------n] 74 s1 75 L6: [e---g-----------------p---r] 76 a b c d e f g h i j k l m n o p q r s t u v w x y z 77 ``` 78 We cannot use virtual sstables in the above scenario for two reasons: 79 1. We don't have a quick method of detecting no data overlap. 80 2. We will be forced to split the sstable in L6 into more than two virtual 81 sstables, but we want to avoid many small virtual sstables in the lsm. 82 83 Note that in Cockroach, the easier-to-solve case happens very regularly when an 84 sstable spans a range boundary (which pebble has no knowledge of), and we ingest 85 a snapshot of a range in between the two already-present ranges. 86 87 slide in between two existing sstables is more likely to happen. It occurs when 88 we ingest a snapshot of a range in between two already present ranges. 89 90 `ingestFindTargetLevel` changes: 91 - The `ingestFindTargetLevel` function is used to determine the target level 92 of the file which is being ingested. Currently, this function returns an `int` 93 which is the target level for the ingesting file. Two additional return 94 parameters, `[]manifest.NewFileEntry` and `*manifest.DeletedFileEntry`, will be 95 added to the function. 96 - If `ingestFindTargetLevel` decides to split an existing sstable into virtual 97 sstables, then it will return new and deleted entries. Otherwise, it will only 98 return the target level of the ingesting file. 99 - Within the `ingestFindTargetLevel` function, the `overlapWithIterator` 100 function is used to quickly detect data overlap. In the case with file 101 boundary overlap, but no data overlap, in the lowest possible level, we will 102 split the existing sstable into virtual sstables and generate the 103 `NewFileEntry`s and the `DeletedFileEntry`. The `FilemetaData` section 104 describes how the various fields in the `FilemetaData` will be computed for 105 the newly created virtual sstables. 106 107 - Note that we will not split physical sstables into virtual sstables in L0 for 108 the use case described in this RFC. The benefit of doing so would be to reduce 109 the number of L0 sublevels, but the cost would be additional implementation 110 complexity(see the `FilemetaData` section). We also want to avoid too many 111 virtual sstables in the lsm as they can lead to space amp(see `Compaction` 112 section). However, in the future, for the disaggregated storage masking case, 113 we would need to support ingestion and use of virtual sstables in L0. 114 115 - Note that we may need an upper bound on the number of times an sstable is 116 split into smaller virtual sstables. We can further reduce the risk of many 117 small sstables: 118 1. For CockroachDB's snapshot ingestion, there is one large sst (up to 512MB) 119 and many tiny ones. We can choose the apply this splitting logic only for 120 the large sst. It is ok for the tiny ssts to be ingested into L0. 121 2. Split only if the ingested sst is at least half the size of the sst being 122 split. So if we have a smaller ingested sst, we will pick a higher level to 123 split at (where the ssts are smaller). The lifetime of virtual ssts at a 124 higher level is smaller, so there is lower risk of littering the LSM with 125 long-lived small virtual ssts. 126 3. For disaggregated storage implementation, we can avoid masking for tiny 127 sstables being ingested and instead write a range delete like we currently 128 do. Precise details on the masking use case are out of the scope of this 129 RFC. 130 131 `ingestApply` changes: 132 - The new and deleted file entries returned by the `ingestFindTargetLevel` 133 function will be added to the version edit in `ingestApply`. 134 - We will appropriately update the `levelMetrics` based on the new information 135 returned by `ingestFindTargetLevel`. 136 137 138 ### `FilemetaData` changes 139 140 Each virtual sstables will have a unique file metadata value associated with it. 141 The metadata may be borrowed from the backing physical sstable, or it may be 142 unique to the virtual sstable. 143 144 This rfc lists out the fields in the `FileMetadata` struct with information on 145 how each field will be populated. 146 147 `Atomic.AllowedSeeks`: Field is used for read triggered compactions, and we can 148 populate this field for each virtual sstable since virtual sstables can be 149 picked for compactions. 150 151 `Atomic.statsValid`: We can set this to true(`1`) when the virtual sstable is 152 created. On virtual sstable creation we will estimate the table stats of the 153 virtual sstable based on the table stats of the physical sstable. We can also 154 set this to `0` and let the table stats job asynchronously compute the stats. 155 156 `refs`: The will be turned into a pointer which will be shared by the 157 virtual/physical sstables. See the deletion section of the RFC to learn how the 158 `refs` count will be used. 159 160 `FileNum`: We could give each virtual sstable its own file number or share 161 the file number between all the virtual sstables. In the former case, the virtual 162 sstables will be distinguished by the file number, and will have an additional 163 metadata field to indicate the file number of the parent sstable. In the latter 164 case, we can use a few of the most significant bits of the 64 bit file number to 165 distinguish the virtual sstables. 166 167 The benefit of using a single file number for each virtual sstable, is that we 168 don't need to use additional space to store the file number of the backing 169 physical sstable. 170 171 It might make sense to give each virtual sstable its own file number. Virtual 172 sstables are picked for compactions, and compactions and compaction picking 173 expect a unique file number for each of the files which it is compacting. 174 For example, read compactions will use the file number of the file to determine 175 if a file picked for compaction has already been compacted, the version edit 176 will expect a different file number for each virtual sstable, etc. 177 178 There are direct references to the `FilemetaData.FileNum` throughout Pebble. For 179 example, the file number is accessed when the the `DB.Checkpoint` function is 180 called. This function iterates through the files in each level of the lsm, 181 constructs the filepath using the file number, and reads the file from disk. In 182 such cases, it is important to exclude virtual sstables. 183 184 `Size`: We compute this using linear interpolation on the number of blocks in 185 the parent sstable and the number of blocks in the newly created virtual sstable. 186 187 `SmallestSeqNum/LargestSeqNum`: These fields depend on the parent sstable, 188 but we would need to perform a scan of the physical sstable to compute these 189 accurately for the virtual sstable upon creation. Instead, we could convert 190 these fields into lower and upper bounds of the sequence numbers in a file. 191 192 These fields are used for l0 sublevels, pebble tooling, delete compaction hints, 193 and a lot of plumbing. We don't need to worry about the L0 sublevels use case 194 because we won't have virtual sstables in L0 for the use case in this RFC. For 195 the rest of the use cases we can use lower bound for the smallest seq number, 196 and an upper bound for the largest seq number work. 197 198 TODO(bananabrick): Add more detail for any delete compaction hint changes if 199 necessary. 200 201 `Smallest/Largest`: These, along with the smallest/largest ranges for the range 202 and point keys can be computed upon virtual sstable creation. Precisely, these 203 can be computed when we try and detect data overlap in the `overlapWithIterator` 204 function during ingestion. 205 206 `Stats`: `TableStats` will either be computed upon virtual sstable creation 207 using linear interpolation on the block counts of the virtual/physical sstables 208 or asynchronously using the file bounds of the virtual sstable. 209 210 `PhysicalState`: We can add an additional struct with state associated with 211 physical ssts which have been virtualized. 212 213 ``` 214 type PhysicalState struct { 215 // Total refs across all virtual ssts * versions. That is, if the same virtual 216 // sst is present in multiple versions, it may have multiple refs, if the 217 // btree node is not the same. 218 totalRefs int32 219 220 // Number of virtual ssts in the latest version that refer to this physical 221 // SST. Will be 1 if there is only a physical sst, or there is only 1 virtual 222 // sst referencing this physical sst. 223 // INVARIANT: refsInLatestVersion <= totalRefs 224 // refsInLatestVersion == 0 is a zombie sstable. 225 refsInLatestVersion int32 226 227 fileSize uint64 228 229 // If sst is not virtualized and in latest version 230 // virtualSizeSumInLatestVersion == fileSize. If 231 // virtualSizeSumInLatestVersion > 0 and 232 // virtualSizeSumInLatestVersion/fileSize is very small, the corresponding 233 // virtual sst(s) should be candidates for compaction. These candidates can be 234 // tracked via btree annotations. Incrementlly updated in 235 // BulkVersionEdit.Apply, when updating refsInLatestVersion. 236 virtualSizeSumInLatestVersion uint64 237 } 238 ``` 239 240 The `Deletion` section and the `Compactions` section describe why we need to 241 store the `PhysicalState`. 242 243 ### Deletion of physical and virtual sstables 244 245 We want to ensure that the physical sstable is only deleted from disk when no 246 version references it, and when there are no virtual sstables which are backed 247 by the physical sstable. 248 249 Since `FilemetaData.refs` is a pointer which is shared by the physical and 250 virtual sstables, the physical sstable won't be deleted when it is removed 251 from the latest version as the `FilemetaData.refs` will have been increased 252 when the virtual sstable is added to a version. Therefore, we only need to 253 ensure that the physical sstable is eventually deleted when there are no 254 versions which reference it. 255 256 Sstables are deleted from disk by the `DB.doDeleteObsoleteFiles` function which 257 looks for files to delete in the the `DB.mu.versions.obsoleteTables` slice. 258 So we need to ensure that any physical sstable which was virtualized is added to 259 the obsolete tables list iff `FilemetaData.refs` is 0. 260 261 Sstable are added to the obsolete file list when a `Version` is unrefed and 262 when `DB.scanObsoleteFiles` is called when Pebble is opened. 263 264 When a `Version` is unrefed, sstables referenced by it are only added to the 265 obsolete table list if the `FilemetaData.refs` hits 0 for the sstable. With 266 virtual sstables, we can have a case where the last version which directly 267 references a physical sstable is unrefed, but the physical sstable is not added 268 to the obsolete table list because its `FilemetaData.refs` count is not 0 269 because of indirect references through virtual sstables. Since the last Version 270 which directly references the physical sstable is deleted, the physical sstable 271 will never get added to the obsolete table list. Since virtual sstables keep 272 track of their parent physical sstable, we can just add the physical sstable to 273 the obsolete table list when the last virtual sstable which references it is 274 deleted. 275 276 `DB.scanObsoleteFiles` will delete any file which isn't referenced by the 277 `VersionSet.versions` list. So, it's possible that a physical sstable associated 278 with a virtual sstable will be deleted. This problem can be fixed by a small 279 tweak in the `d.mu.versions.addLiveFileNums` to treat the parent sstable of 280 a virtual sstable as a live file. 281 282 Deleted files still referenced by older versions are considered zombie sstables. 283 We can extend the definition of zombie sstables to be any sstable which is not 284 directly, or indirectly through virtual sstables, referenced by the latest 285 version. See the `PhysicalState` subsection of the `FilemetaData` section 286 where we describe how the references in the latest version will be tracked. 287 288 289 ### Reading from virtual sstables 290 291 Since virtual sstables do not exist on disk, we will have to redirect reads 292 to the physical sstable which backs the virtual sstable. 293 294 All reads to the physical files go through the table cache which opens the file 295 on disk and creates a `Reader` for the reads. The table cache currently creates 296 a `FileNum` -> `Reader` mapping for the physical sstables. 297 298 Most of the functions in table cache API take the file metadata of the file as 299 a parameter. Examples include `newIters`, `newRangeKeyIter`, `withReader`, etc. 300 Each of these functions then calls a subsequent function on the sstable 301 `Reader`. 302 303 In the `Reader` API, some functions only really need to be called on physical 304 sstables, whereas some functions need to be called on both physical and virtual 305 sstables. For example, the `Reader.EstimateDiskUsage` usage function, or the 306 `Reader.Layout` function only need to be called on physical sstables, whereas, 307 some function like, `Reader.NewIter`, and `Reader.NewCompactionIter` need to 308 work with virtual sstables. 309 310 We could either have an abstraction over the physical sstable `Reader` per 311 virtual sstable, or update the `Reader` API to accept file bounds of the 312 sstable. In the latter case, we would create one `Reader` on the physical 313 sstable for all of the virtual sstables, and update the `Reader` API to accept 314 the file bounds of the sstable. 315 316 Changes required to share a `Reader` on the physical sstable among the virtual 317 sstable: 318 - If the file metadata of the virtual sstable is passed into the table cache, on 319 a table cache miss, the table cache will load the Reader for the physical 320 sstable. This step can be performed in the `tableCacheValue.load` function. On 321 a table cache hit, the file number of the parent sstable will be used to fetch 322 the appropriate sstable `Reader`. 323 - The `Reader` api will be updated to support reads from virtual sstables. For 324 example, the `NewCompactionIter` function will take additional 325 `lower,upper []byte` parameters. 326 327 Updates to iterators: 328 - `Reader.NewIter` already has `lower,upper []byte` parameters so this requires 329 no change. 330 - Add `lower,upper` fields to the `Reader.NewCompactionIter`. The function 331 initializes single level and two level iterators, and we can pass in the 332 `lower,upper` values to those. TODO(bananabrick): Make sure that the value 333 of `bytesIterated` in the compaction iterator is still accurate. 334 - `Reader.NewRawRangeKeyIter/NewRawRangeDelIter`: We need to add `lower/upper` 335 fields to the functions. Both iterators make use of a `fragmentBlockIter`. We 336 could filter keys above the `fragmentBlockIter` or add filtering within the 337 `fragmentBlockIter`. To add filtering within the `fragmentBlockIter` we will 338 initialize it with two additional `lower/upper []byte` fields. 339 - We would need to update the `SetBounds` logic for the sstable iterators to 340 never set bounds for the iterators outside the virtual sstable bounds. This 341 could lead to keys outside the virtual sstable bounds, but inside the physical 342 sstable bounds, to be surfaced. 343 344 TODO(bananabrick): Add a section about sstable properties, if necessary. 345 346 ### Compactions 347 348 Virtual sstables can be picked for compactions. If the `FilemetaData` and the 349 iterator stack changes work, then compaction shouldn't require much, if any, 350 additional work. 351 352 Virtual sstables which are picked for compactions may cause space amplification. 353 For example, if we have two virtual sstables `a` and `b` in L5, backed by a 354 physical sstable `c`, and the sstable `a` is picked for a compaction. We will 355 write some additional data into L6, but we won't delete sstable `c` because 356 sstable `b` still refers to it. In the worst case, sstable `b` will never be 357 picked for compaction and will never be compacted into and we'll have permanent 358 space amplification. We should try prioritize compaction of sstable `b` to 359 prevent such a scenario. 360 361 See the `PhysicalState` subsection in the `FilemetaData` section to see how 362 we'll store compaction picking metrics to reduce virtual sstable space-amp. 363 364 ### `VersionEdit` decode/encode 365 Any additional fields added to the `FilemetaData` need to be supported in the 366 version edit `decode/encode` functions.