forked from cockroachdb/pebble
-
Notifications
You must be signed in to change notification settings - Fork 0
/
compaction.go
2877 lines (2647 loc) · 99.7 KB
/
compaction.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Copyright 2013 The LevelDB-Go and Pebble Authors. All rights reserved. Use
// of this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
package pebble
import (
"bytes"
"context"
"fmt"
"io"
"math"
"runtime/pprof"
"sort"
"strings"
"sync/atomic"
"time"
"github.com/cockroachdb/errors"
"github.com/cockroachdb/errors/oserror"
"github.com/cockroachdb/pebble/internal/base"
"github.com/cockroachdb/pebble/internal/manifest"
"github.com/cockroachdb/pebble/internal/private"
"github.com/cockroachdb/pebble/internal/rangedel"
"github.com/cockroachdb/pebble/sstable"
"github.com/cockroachdb/pebble/vfs"
)
var errEmptyTable = errors.New("pebble: empty table")
var errFlushInvariant = errors.New("pebble: flush next log number is unset")
var compactLabels = pprof.Labels("pebble", "compact")
var flushLabels = pprof.Labels("pebble", "flush")
var gcLabels = pprof.Labels("pebble", "gc")
// expandedCompactionByteSizeLimit is the maximum number of bytes in all
// compacted files. We avoid expanding the lower level file set of a compaction
// if it would make the total compaction cover more than this many bytes.
func expandedCompactionByteSizeLimit(opts *Options, level int) uint64 {
return uint64(25 * opts.Level(level).TargetFileSize)
}
// maxGrandparentOverlapBytes is the maximum bytes of overlap with level+1
// before we stop building a single file in a level-1 to level compaction.
func maxGrandparentOverlapBytes(opts *Options, level int) uint64 {
return uint64(10 * opts.Level(level).TargetFileSize)
}
// noCloseIter wraps around an internal iterator, intercepting and eliding
// calls to Close. It is used during compaction to ensure that rangeDelIters
// are not closed prematurely.
type noCloseIter struct {
base.InternalIterator
}
func (i noCloseIter) Close() error {
return nil
}
type compactionLevel struct {
level int
files manifest.LevelSlice
}
// Return output from compactionOutputSplitters. See comment on
// compactionOutputSplitter.shouldSplitBefore() on how this value is used.
type compactionSplitSuggestion int
const (
noSplit compactionSplitSuggestion = iota
splitNow
)
// String implements the Stringer interface.
func (c compactionSplitSuggestion) String() string {
if c == noSplit {
return "no-split"
}
return "split-now"
}
// compactionOutputSplitter is an interface for encapsulating logic around
// switching the output of a compaction to a new output file. Additional
// constraints around switching compaction outputs that are specific to that
// compaction type (eg. flush splits) are implemented in
// compactionOutputSplitters that compose other child compactionOutputSplitters.
type compactionOutputSplitter interface {
// shouldSplitBefore returns whether we should split outputs before the
// specified "current key". The return value is splitNow or noSplit.
// splitNow means a split is advised before the specified key, and noSplit
// means no split is advised.
shouldSplitBefore(key *InternalKey, tw *sstable.Writer) compactionSplitSuggestion
// onNewOutput updates internal splitter state when the compaction switches
// to a new sstable, and returns the next limit for the new output which
// would get used to truncate range tombstones if the compaction iterator
// runs out of keys. The limit returned MUST be > key according to the
// compaction's comparator. The specified key is the first key in the new
// output, or nil if this sstable will only contain range tombstones already
// in the fragmenter.
onNewOutput(key *InternalKey) []byte
}
// fileSizeSplitter is a compactionOutputSplitter that makes a determination
// to split outputs based on the estimated file size of the current output.
// Note that, unlike most other splitters, this splitter does not guarantee
// that it will advise splits only at user key change boundaries.
type fileSizeSplitter struct {
maxFileSize uint64
}
func (f *fileSizeSplitter) shouldSplitBefore(
key *InternalKey, tw *sstable.Writer,
) compactionSplitSuggestion {
// The Kind != RangeDelete part exists because EstimatedSize doesn't grow
// rightaway when a range tombstone is added to the fragmenter. It's always
// better to make a sequence of range tombstones visible to the fragmenter.
if key.Kind() != InternalKeyKindRangeDelete && tw != nil &&
tw.EstimatedSize() >= f.maxFileSize {
return splitNow
}
return noSplit
}
func (f *fileSizeSplitter) onNewOutput(key *InternalKey) []byte {
return nil
}
type grandparentLimitSplitter struct {
c *compaction
ve *versionEdit
limit []byte
}
func (g *grandparentLimitSplitter) shouldSplitBefore(
key *InternalKey, tw *sstable.Writer,
) compactionSplitSuggestion {
if g.limit != nil && g.c.cmp(key.UserKey, g.limit) > 0 {
return splitNow
}
return noSplit
}
func (g *grandparentLimitSplitter) onNewOutput(key *InternalKey) []byte {
g.limit = nil
if g.c.rangeDelFrag.Empty() || len(g.ve.NewFiles) == 0 {
// In this case, `limit` will be a larger user key than `key.UserKey`, or
// nil. In either case, the inner loop will execute at least once to
// process `key`, and the input iterator will be advanced.
if key != nil {
g.limit = g.c.findGrandparentLimit(key.UserKey)
} else { // !c.rangeDelFrag.Empty()
g.limit = g.c.findGrandparentLimit(g.c.rangeDelFrag.Start())
}
} else {
// There is a range tombstone spanning from the last file into the
// current one. Therefore this file's smallest boundary will overlap the
// last file's largest boundary.
//
// In this case, `limit` will be a larger user key than the previous
// file's largest key and correspond to a grandparent file's largest user
// key, or nil. Then, it is possible the inner loop executes zero times,
// and the output file contains only range tombstones. That is fine as
// long as the number of times we execute this case is bounded. Since
// `findGrandparentLimit()` returns a strictly larger user key each time
// and it corresponds to a grandparent file largest key, the number of
// times this case can execute is bounded by the number of grandparent
// files (plus one for the final time it returns nil).
//
// n > 0 since we cannot have seen range tombstones at the
// beginning of the first file.
n := len(g.ve.NewFiles)
g.limit = g.c.findGrandparentLimit(g.ve.NewFiles[n-1].Meta.Largest.UserKey)
// This conditional is necessary to maintain the invariant that
// successive calls to finishOutput() have an increasing
// limit, and that the fragmenter's *FlushTo() and Add() calls are
// always made with successively increasing user keys. It's possible
// for the last file's Meta.Largest.UserKey
// to be substantially less than the last key returned by
// the compactionIter, such as in a sequence of consecutive
// rangedel tombstones, of which some but not all get elided.
// Consider this example:
//
// Compaction input (all range tombstones in this snippet):
// a-b
// c-d
// e-f (elided)
// g-h (elided) <-- grandparent limit before this (at ff)
// i-j (elided)
// k-q <-- grandparent limit at k
// m-q
//
// Note that elided tombstones are added to the fragmenter, but
// removed before they make their way from the fragmenter onto
// iter.tombstones. They still affect the fragmenter's internal
// tracking of the key up to which all tombstones have been flushed.
// After the first output is cut with limit = g, the fragmenter
// is empty, so the next grandparent calculation happens with
// key = g, and returns k. We continue adding range tombstones to
// the fragmenter between [g,k], which includes k-q as we only
// switch outputs after the limit is exceeded. So key = m and
// limit = k when finishOutput is called. Since the start key in
// the fragmenter (k) matches the limit, and no point keys were
// added, we actually don't produce a new output (see the
// conditional in finishOutput() on why).
//
// When we try to calculate the next grandparent limit, since
// c.rangeDelFrag.Empty() == false (as k-q is sitting in it),
// we fall into this case, and use the end key of the last written
// sstable (which was [a-d]) to calculate the grandparent limit.
// That gets us g again, which gets us to call
// c.rangeDelFrag.FlushTo(g), which violates the
// invariant that the current flush-to key (g) be greater than the
// last one (k).
//
// To solve this, if the grandparent limit falls "in between"
// ve.NewFiles[n-1].Meta.Largest.UserKey and c.rangeDelFrag.Start(),
// re-calculate a higher limit ahead of that key.
if g.c.rangeDelFrag.Start() != nil && g.c.cmp(g.limit, g.c.rangeDelFrag.Start()) <= 0 {
g.limit = g.c.findGrandparentLimit(g.c.rangeDelFrag.Start())
}
}
return g.limit
}
type l0LimitSplitter struct {
c *compaction
ve *versionEdit
limit []byte
}
func (l *l0LimitSplitter) shouldSplitBefore(
key *InternalKey, tw *sstable.Writer,
) compactionSplitSuggestion {
if l.limit != nil && l.c.cmp(key.UserKey, l.limit) > 0 {
return splitNow
}
return noSplit
}
func (l *l0LimitSplitter) onNewOutput(key *InternalKey) []byte {
l.limit = nil
// For flushes being split across multiple sstables, call
// findL0Limit to find the next L0 limit.
if key != nil {
l.limit = l.c.findL0Limit(key.UserKey)
} else {
// Use the start key of the first pending tombstone to find the
// next limit. All pending tombstones have the same start key.
// We use this as opposed to the end key of the
// last written sstable to effectively handle cases like these:
//
// a.SET.3
// (L0 limit at b)
// d.RANGEDEL.4:f
//
// In this case, the partition after b has only range deletions,
// so if we were to find the L0 limit after the last written
// key at the split point (key a), we'd get the limit b again,
// and finishOutput() would not advance any further because
// the next range tombstone to write does not start until after
// the L0 split point.
startKey := l.c.rangeDelFrag.Start()
if startKey != nil {
l.limit = l.c.findL0Limit(startKey)
}
}
return l.limit
}
// splitterGroup is a compactionOutputSplitter that splits whenever one of its
// child splitters advises a compaction split.
type splitterGroup struct {
cmp Compare
splitters []compactionOutputSplitter
}
func (a *splitterGroup) shouldSplitBefore(
key *InternalKey, tw *sstable.Writer,
) (suggestion compactionSplitSuggestion) {
for _, splitter := range a.splitters {
if splitter.shouldSplitBefore(key, tw) == splitNow {
return splitNow
}
}
return noSplit
}
func (a *splitterGroup) onNewOutput(key *InternalKey) []byte {
var earliestLimit []byte
for _, splitter := range a.splitters {
limit := splitter.onNewOutput(key)
if limit == nil {
continue
}
if earliestLimit == nil || a.cmp(limit, earliestLimit) < 0 {
earliestLimit = limit
}
}
return earliestLimit
}
// userKeyChangeSplitter is a compactionOutputSplitter that takes in a child
// splitter, and splits when 1) that child splitter has advised a split, and 2)
// the compaction output is at the boundary between two user keys (also
// the boundary between atomic compaction units). Use this splitter to wrap
// any splitters that don't guarantee user key splits (i.e. splitters that make
// their determination in ways other than comparing the current key against a
// limit key.
type userKeyChangeSplitter struct {
cmp Compare
splitOnNextUserKey bool
savedKey []byte
splitter compactionOutputSplitter
}
func (u *userKeyChangeSplitter) shouldSplitBefore(
key *InternalKey, tw *sstable.Writer,
) compactionSplitSuggestion {
if u.splitOnNextUserKey && u.cmp(u.savedKey, key.UserKey) != 0 {
u.splitOnNextUserKey = false
u.savedKey = u.savedKey[:0]
return splitNow
}
if split := u.splitter.shouldSplitBefore(key, tw); split == splitNow {
u.splitOnNextUserKey = true
u.savedKey = append(u.savedKey[:0], key.UserKey...)
return noSplit
}
return noSplit
}
func (u *userKeyChangeSplitter) onNewOutput(key *InternalKey) []byte {
return u.splitter.onNewOutput(key)
}
// compactionFile is a vfs.File wrapper that, on every write, updates a metric
// in `versions` on bytes written by in-progress compactions so far. It also
// increments a per-compaction `written` int.
type compactionFile struct {
vfs.File
versions *versionSet
written *int64
}
// Write implements the io.Writer interface.
func (c *compactionFile) Write(p []byte) (n int, err error) {
n, err = c.File.Write(p)
if err != nil {
return n, err
}
*c.written += int64(n)
c.versions.incrementCompactionBytes(int64(n))
return n, err
}
type compactionKind int
const (
compactionKindDefault compactionKind = iota
compactionKindFlush
compactionKindMove
compactionKindDeleteOnly
compactionKindElisionOnly
compactionKindRead
)
func (k compactionKind) String() string {
switch k {
case compactionKindDefault:
return "default"
case compactionKindFlush:
return "flush"
case compactionKindMove:
return "move"
case compactionKindDeleteOnly:
return "delete-only"
case compactionKindElisionOnly:
return "elision-only"
case compactionKindRead:
return "read"
}
return "?"
}
// compaction is a table compaction from one level to the next, starting from a
// given version.
type compaction struct {
kind compactionKind
cmp Compare
formatKey base.FormatKey
logger Logger
version *version
score float64
// startLevel is the level that is being compacted. Inputs from startLevel
// and outputLevel will be merged to produce a set of outputLevel files.
startLevel *compactionLevel
// outputLevel is the level that files are being produced in. outputLevel is
// equal to startLevel+1 except when startLevel is 0 in which case it is
// equal to compactionPicker.baseLevel().
outputLevel *compactionLevel
inputs []compactionLevel
// maxOutputFileSize is the maximum size of an individual table created
// during compaction.
maxOutputFileSize uint64
// maxOverlapBytes is the maximum number of bytes of overlap allowed for a
// single output table with the tables in the grandparent level.
maxOverlapBytes uint64
// disableRangeTombstoneElision disables elision of range tombstones. Used by
// tests to allow range tombstones to be added to tables where they would
// otherwise be elided.
disableRangeTombstoneElision bool
// flushing contains the flushables (aka memtables) that are being flushed.
flushing flushableList
// bytesIterated contains the number of bytes that have been flushed/compacted.
bytesIterated uint64
// bytesWritten contains the number of bytes that have been written to outputs.
bytesWritten int64
// atomicBytesIterated points to the variable to increment during iteration.
// atomicBytesIterated must be read/written atomically. Flushing will increment
// the shared variable which compaction will read. This allows for the
// compaction routine to know how many bytes have been flushed before the flush
// is applied.
atomicBytesIterated *uint64
// The boundaries of the input data.
smallest InternalKey
largest InternalKey
// The range deletion tombstone fragmenter. Adds range tombstones as they are
// returned from `compactionIter` and fragments them for output to files.
// Referenced by `compactionIter` which uses it to check whether keys are deleted.
rangeDelFrag rangedel.Fragmenter
// A list of objects to close when the compaction finishes. Used by input
// iteration to keep rangeDelIters open for the lifetime of the compaction,
// and only close them when the compaction finishes.
closers []io.Closer
// grandparents are the tables in level+2 that overlap with the files being
// compacted. Used to determine output table boundaries. Do not assume that the actual files
// in the grandparent when this compaction finishes will be the same.
grandparents manifest.LevelSlice
// Boundaries at which flushes to L0 should be split. Determined by
// L0Sublevels. If nil, flushes aren't split.
l0Limits [][]byte
// List of disjoint inuse key ranges the compaction overlaps with in
// grandparent and lower levels. See setupInuseKeyRanges() for the
// construction. Used by elideTombstone() and elideRangeTombstone() to
// determine if keys affected by a tombstone possibly exist at a lower level.
inuseKeyRanges []manifest.UserKeyRange
elideTombstoneIndex int
// allowedZeroSeqNum is true if seqnums can be zeroed if there are no
// snapshots requiring them to be kept. This determination is made by
// looking for an sstable which overlaps the bounds of the compaction at a
// lower level in the LSM during runCompaction.
allowedZeroSeqNum bool
metrics map[int]*LevelMetrics
}
func (c *compaction) makeInfo(jobID int) CompactionInfo {
info := CompactionInfo{
JobID: jobID,
Reason: c.kind.String(),
Input: make([]LevelInfo, 0, len(c.inputs)),
}
for _, cl := range c.inputs {
inputInfo := LevelInfo{Level: cl.level, Tables: nil}
iter := cl.files.Iter()
for m := iter.First(); m != nil; m = iter.Next() {
inputInfo.Tables = append(inputInfo.Tables, m.TableInfo())
}
info.Input = append(info.Input, inputInfo)
}
if c.outputLevel != nil {
info.Output.Level = c.outputLevel.level
// If there are no inputs from the output level (eg, a move
// compaction), add an empty LevelInfo to info.Input.
if len(c.inputs) > 0 && c.inputs[len(c.inputs)-1].level != c.outputLevel.level {
info.Input = append(info.Input, LevelInfo{Level: c.outputLevel.level})
}
} else {
// For a delete-only compaction, set the output level to L6. The
// output level is not meaningful here, but complicating the
// info.Output interface with a pointer doesn't seem worth the
// semantic distinction.
info.Output.Level = numLevels - 1
}
return info
}
func newCompaction(pc *pickedCompaction, opts *Options, bytesCompacted *uint64) *compaction {
c := &compaction{
kind: compactionKindDefault,
cmp: pc.cmp,
formatKey: opts.Comparer.FormatKey,
score: pc.score,
inputs: pc.inputs,
smallest: pc.smallest,
largest: pc.largest,
logger: opts.Logger,
version: pc.version,
maxOutputFileSize: pc.maxOutputFileSize,
maxOverlapBytes: pc.maxOverlapBytes,
atomicBytesIterated: bytesCompacted,
}
c.startLevel = &c.inputs[0]
c.outputLevel = &c.inputs[1]
// Compute the set of outputLevel+1 files that overlap this compaction (these
// are the grandparent sstables).
if c.outputLevel.level+1 < numLevels {
c.grandparents = c.version.Overlaps(c.outputLevel.level+1, c.cmp,
c.smallest.UserKey, c.largest.UserKey)
}
c.setupInuseKeyRanges()
switch {
case pc.readTriggered:
c.kind = compactionKindRead
case c.startLevel.level == numLevels-1:
// This compaction is an L6->L6 elision-only compaction to rewrite
// a sstable without unnecessary tombstones.
c.kind = compactionKindElisionOnly
case c.outputLevel.files.Empty() && c.startLevel.files.Len() == 1 &&
c.grandparents.SizeSum() <= c.maxOverlapBytes:
// This compaction can be converted into a trivial move from one level
// to the next. We avoid such a move if there is lots of overlapping
// grandparent data. Otherwise, the move could create a parent file
// that will require a very expensive merge later on.
c.kind = compactionKindMove
}
return c
}
func newDeleteOnlyCompaction(opts *Options, cur *version, inputs []compactionLevel) *compaction {
c := &compaction{
kind: compactionKindDeleteOnly,
cmp: opts.Comparer.Compare,
formatKey: opts.Comparer.FormatKey,
logger: opts.Logger,
version: cur,
inputs: inputs,
}
// Set c.smallest, c.largest.
files := make([]manifest.LevelIterator, 0, len(inputs))
for _, in := range inputs {
files = append(files, in.files.Iter())
}
c.smallest, c.largest = manifest.KeyRange(opts.Comparer.Compare, files...)
return c
}
func adjustGrandparentOverlapBytesForFlush(c *compaction, flushingBytes uint64) {
// Heuristic to place a lower bound on compaction output file size
// caused by Lbase. Prior to this heuristic we have observed an L0 in
// production with 310K files of which 290K files were < 10KB in size.
// Our hypothesis is that it was caused by L1 having 2600 files and
// ~10GB, such that each flush got split into many tiny files due to
// overlapping with most of the files in Lbase.
//
// The computation below is general in that it accounts
// for flushing different volumes of data (e.g. we may be flushing
// many memtables). For illustration, we consider the typical
// example of flushing a 64MB memtable. So 12.8MB output,
// based on the compression guess below. If the compressed bytes
// guess is an over-estimate we will end up with smaller files,
// and if an under-estimate we will end up with larger files.
// With a 2MB target file size, 7 files. We are willing to accept
// 4x the number of files, if it results in better write amplification
// when later compacting to Lbase, i.e., ~450KB files (target file
// size / 4).
//
// Note that this is a pessimistic heuristic in that
// fileCountUpperBoundDueToGrandparents could be far from the actual
// number of files produced due to the grandparent limits. For
// example, in the extreme, consider a flush that overlaps with 1000
// files in Lbase f0...f999, and the initially calculated value of
// maxOverlapBytes will cause splits at f10, f20,..., f990, which
// means an upper bound file count of 100 files. Say the input bytes
// in the flush are such that acceptableFileCount=10. We will fatten
// up maxOverlapBytes by 10x to ensure that the upper bound file count
// drops to 10. However, it is possible that in practice, even without
// this change, we would have produced no more than 10 files, and that
// this change makes the files unnecessarily wide. Say the input bytes
// are distributed such that 10% are in f0...f9, 10% in f10...f19, ...
// 10% in f80...f89 and 10% in f990...f999. The original value of
// maxOverlapBytes would have actually produced only 10 sstables. But
// by increasing maxOverlapBytes by 10x, we may produce 1 sstable that
// spans f0...f89, i.e., a much wider sstable than necessary.
//
// We could produce a tighter estimate of
// fileCountUpperBoundDueToGrandparents if we had knowledge of the key
// distribution of the flush. The 4x multiplier mentioned earlier is
// a way to try to compensate for this pessimism.
//
// TODO(sumeer): we don't have compression info for the data being
// flushed, but it is likely that existing files that overlap with
// this flush in Lbase are representative wrt compression ratio. We
// could store the uncompressed size in FileMetadata and estimate
// the compression ratio.
const approxCompressionRatio = 0.2
approxOutputBytes := approxCompressionRatio * float64(flushingBytes)
approxNumFilesBasedOnTargetSize :=
int(math.Ceil(approxOutputBytes / float64(c.maxOutputFileSize)))
acceptableFileCount := float64(4 * approxNumFilesBasedOnTargetSize)
// The byte calculation is linear in numGrandparentFiles, but we will
// incur this linear cost in findGrandparentLimit too, so we are also
// willing to pay it now. We could approximate this cheaply by using
// the mean file size of Lbase.
grandparentFileBytes := c.grandparents.SizeSum()
fileCountUpperBoundDueToGrandparents :=
float64(grandparentFileBytes) / float64(c.maxOverlapBytes)
if fileCountUpperBoundDueToGrandparents > acceptableFileCount {
c.maxOverlapBytes = uint64(
float64(c.maxOverlapBytes) *
(fileCountUpperBoundDueToGrandparents / acceptableFileCount))
}
}
func newFlush(
opts *Options, cur *version, baseLevel int, flushing flushableList, bytesFlushed *uint64,
) *compaction {
c := &compaction{
kind: compactionKindFlush,
cmp: opts.Comparer.Compare,
formatKey: opts.Comparer.FormatKey,
logger: opts.Logger,
version: cur,
inputs: []compactionLevel{{level: -1}, {level: 0}},
maxOutputFileSize: math.MaxUint64,
maxOverlapBytes: math.MaxUint64,
flushing: flushing,
atomicBytesIterated: bytesFlushed,
}
c.startLevel = &c.inputs[0]
c.outputLevel = &c.inputs[1]
if cur.L0Sublevels != nil {
c.l0Limits = cur.L0Sublevels.FlushSplitKeys()
}
smallestSet, largestSet := false, false
updatePointBounds := func(iter internalIterator) {
if key, _ := iter.First(); key != nil {
if !smallestSet ||
base.InternalCompare(c.cmp, c.smallest, *key) > 0 {
smallestSet = true
c.smallest = key.Clone()
}
}
if key, _ := iter.Last(); key != nil {
if !largestSet ||
base.InternalCompare(c.cmp, c.largest, *key) < 0 {
largestSet = true
c.largest = key.Clone()
}
}
}
updateRangeBounds := func(iter internalIterator) {
if key, _ := iter.First(); key != nil {
if !smallestSet ||
base.InternalCompare(c.cmp, c.smallest, *key) > 0 {
smallestSet = true
c.smallest = key.Clone()
}
}
if key, value := iter.Last(); key != nil {
tmp := base.InternalKey{
UserKey: value,
Trailer: key.Trailer,
}
if !largestSet ||
base.InternalCompare(c.cmp, c.largest, tmp) < 0 {
largestSet = true
c.largest = tmp.Clone()
}
}
}
var flushingBytes uint64
for i := range flushing {
f := flushing[i]
updatePointBounds(f.newIter(nil))
if rangeDelIter := f.newRangeDelIter(nil); rangeDelIter != nil {
updateRangeBounds(rangeDelIter)
}
flushingBytes += f.inuseBytes()
}
if opts.FlushSplitBytes > 0 {
c.maxOutputFileSize = uint64(opts.Level(0).TargetFileSize)
c.maxOverlapBytes = maxGrandparentOverlapBytes(opts, 0)
c.grandparents = c.version.Overlaps(baseLevel, c.cmp,
c.smallest.UserKey, c.largest.UserKey)
adjustGrandparentOverlapBytesForFlush(c, flushingBytes)
}
c.setupInuseKeyRanges()
return c
}
func (c *compaction) setupInuseKeyRanges() {
level := c.outputLevel.level + 1
if c.outputLevel.level == 0 {
level = 0
}
c.inuseKeyRanges = calculateInuseKeyRanges(c.version, c.cmp, level,
c.smallest.UserKey, c.largest.UserKey)
}
func calculateInuseKeyRanges(
v *version, cmp base.Compare, level int, smallest, largest []byte,
) []manifest.UserKeyRange {
// Use two slices, alternating which one is input and which one is output
// as we descend the LSM.
var input, output []manifest.UserKeyRange
// L0 requires special treatment, since sstables within L0 may overlap.
// We use the L0 Sublevels structure to efficiently calculate the merged
// in-use key ranges.
if level == 0 {
output = v.L0Sublevels.InUseKeyRanges(smallest, largest)
level++
}
for ; level < numLevels; level++ {
overlaps := v.Overlaps(level, cmp, smallest, largest)
iter := overlaps.Iter()
// We may already have in-use key ranges from higher levels. Iterate
// through both our accumulated in-use key ranges and this level's
// files, merging the two.
//
// Tables higher within the LSM have broader key spaces. We use this
// when possible to seek past a level's files that are contained by
// our current accumulated in-use key ranges. This helps avoid
// per-sstable work during flushes or compactions in high levels which
// overlap the majority of the LSM's sstables.
input, output = output, input
output = output[:0]
var currFile *fileMetadata
var currAccum *manifest.UserKeyRange
if len(input) > 0 {
currAccum, input = &input[0], input[1:]
}
// If we have an accumulated key range and its start is ≤ smallest,
// we can seek to the accumulated range's end. Otherwise, we need to
// start at the first overlapping file within the level.
if currAccum != nil && cmp(currAccum.Start, smallest) <= 0 {
currFile = seekGT(&iter, cmp, currAccum.End)
} else {
currFile = iter.First()
}
for currFile != nil || currAccum != nil {
// If we've exhausted either the files in the level or the
// accumulated key ranges, we just need to append the one we have.
// If we have both a currFile and a currAccum, they either overlap
// or they're disjoint. If they're disjoint, we append whichever
// one sorts first and move on to the next file or range. If they
// overlap, we merge them into currAccum and proceed to the next
// file.
switch {
case currAccum == nil || (currFile != nil && cmp(currFile.Largest.UserKey, currAccum.Start) < 0):
// This file is strictly before the current accumulated range,
// or there are no more accumulated ranges.
output = append(output, manifest.UserKeyRange{
Start: currFile.Smallest.UserKey,
End: currFile.Largest.UserKey,
})
currFile = iter.Next()
case currFile == nil || (currAccum != nil && cmp(currAccum.End, currFile.Smallest.UserKey) < 0):
// The current accumulated key range is strictly before the
// current file, or there are no more files.
output = append(output, *currAccum)
currAccum = nil
if len(input) > 0 {
currAccum, input = &input[0], input[1:]
}
default:
// The current accumulated range and the current file overlap.
// Adjust the accumulated range to be the union.
if cmp(currFile.Smallest.UserKey, currAccum.Start) < 0 {
currAccum.Start = currFile.Smallest.UserKey
}
if cmp(currFile.Largest.UserKey, currAccum.End) > 0 {
currAccum.End = currFile.Largest.UserKey
}
// Extending `currAccum`'s end boundary may have caused it to
// overlap with `input` key ranges that we haven't processed
// yet. Merge any such key ranges.
for len(input) > 0 && cmp(input[0].Start, currAccum.End) <= 0 {
if cmp(input[0].End, currAccum.End) > 0 {
currAccum.End = input[0].End
}
input = input[1:]
}
// Seek the level iterator past our current accumulated end.
currFile = seekGT(&iter, cmp, currAccum.End)
}
}
}
return output
}
func seekGT(iter *manifest.LevelIterator, cmp base.Compare, key []byte) *manifest.FileMetadata {
f := iter.SeekGE(cmp, key)
for f != nil && cmp(f.Largest.UserKey, key) == 0 {
f = iter.Next()
}
return f
}
// findGrandparentLimit takes the start user key for a table and returns the
// user key to which that table can extend without excessively overlapping
// the grandparent level. If no limit is needed considering the grandparent
// files, this function returns nil. This is done in order to prevent a table
// at level N from overlapping too much data at level N+1. We want to avoid
// such large overlaps because they translate into large compactions. The
// current heuristic stops output of a table if the addition of another key
// would cause the table to overlap more than 10x the target file size at
// level N. See maxGrandparentOverlapBytes.
//
// TODO(peter): Stopping compaction output in the middle of a user-key creates
// 2 sstables that need to be compacted together as an "atomic compaction
// unit". This is unfortunate as it removes the benefit of stopping output to
// an sstable in order to prevent a large compaction with the next level. Seems
// better to adjust findGrandparentLimit to not stop output in the middle of a
// user-key. Perhaps this isn't a problem if the compaction picking heuristics
// always pick the right (older) sibling for compaction first.
func (c *compaction) findGrandparentLimit(start []byte) []byte {
iter := c.grandparents.Iter()
var overlappedBytes uint64
for f := iter.SeekGE(c.cmp, start); f != nil; f = iter.Next() {
overlappedBytes += f.Size
// To ensure forward progress we always return a larger user
// key than where we started. See comments above clients of
// this function for how this is used.
if overlappedBytes > c.maxOverlapBytes && c.cmp(start, f.Largest.UserKey) < 0 {
return f.Largest.UserKey
}
}
return nil
}
// findL0Limit takes the start key for a table and returns the user key to which
// that table can be extended without hitting the next l0Limit. Having flushed
// sstables "bridging across" an l0Limit could lead to increased L0 -> LBase
// compaction sizes as well as elevated read amplification.
func (c *compaction) findL0Limit(start []byte) []byte {
if c.startLevel.level > -1 || c.outputLevel.level != 0 || len(c.l0Limits) == 0 {
return nil
}
index := sort.Search(len(c.l0Limits), func(i int) bool {
return c.cmp(c.l0Limits[i], start) > 0
})
if index < len(c.l0Limits) {
return c.l0Limits[index]
}
return nil
}
// errorOnUserKeyOverlap returns an error if the last two written sstables in
// this compaction have revisions of the same user key present in both sstables,
// when it shouldn't (eg. when splitting flushes).
func (c *compaction) errorOnUserKeyOverlap(ve *versionEdit) error {
if n := len(ve.NewFiles); n > 1 {
meta := ve.NewFiles[n-1].Meta
prevMeta := ve.NewFiles[n-2].Meta
if prevMeta.Largest.Trailer != InternalKeyRangeDeleteSentinel &&
c.cmp(prevMeta.Largest.UserKey, meta.Smallest.UserKey) >= 0 {
return errors.Errorf("pebble: compaction split user key across two sstables: %s in %s and %s",
prevMeta.Largest.Pretty(c.formatKey),
prevMeta.FileNum,
meta.FileNum)
}
}
return nil
}
// allowZeroSeqNum returns true if seqnum's can be zeroed if there are no
// snapshots requiring them to be kept. It performs this determination by
// looking for an sstable which overlaps the bounds of the compaction at a
// lower level in the LSM.
func (c *compaction) allowZeroSeqNum() bool {
return c.elideRangeTombstone(c.smallest.UserKey, c.largest.UserKey)
}
// elideTombstone returns true if it is ok to elide a tombstone for the
// specified key. A return value of true guarantees that there are no key/value
// pairs at c.level+2 or higher that possibly contain the specified user
// key. The keys in multiple invocations to elideTombstone must be supplied in
// order.
func (c *compaction) elideTombstone(key []byte) bool {
if len(c.flushing) != 0 {
return false
}
for ; c.elideTombstoneIndex < len(c.inuseKeyRanges); c.elideTombstoneIndex++ {
r := &c.inuseKeyRanges[c.elideTombstoneIndex]
if c.cmp(key, r.End) <= 0 {
if c.cmp(key, r.Start) >= 0 {
return false
}
break
}
}
return true
}
// elideRangeTombstone returns true if it is ok to elide the specified range
// tombstone. A return value of true guarantees that there are no key/value
// pairs at c.outputLevel.level+1 or higher that possibly overlap the specified
// tombstone.
func (c *compaction) elideRangeTombstone(start, end []byte) bool {
// Disable range tombstone elision if the testing knob for that is enabled,
// or if we are flushing memtables. The latter requirement is due to
// inuseKeyRanges not accounting for key ranges in other memtables that are
// being flushed in the same compaction. It's possible for a range tombstone
// in one memtable to overlap keys in a preceding memtable in c.flushing.
//
// This function is also used in setting allowZeroSeqNum, so disabling
// elision of range tombstones also disables zeroing of SeqNums.
//
// TODO(peter): we disable zeroing of seqnums during flushing to match
// RocksDB behavior and to avoid generating overlapping sstables during
// DB.replayWAL. When replaying WAL files at startup, we flush after each
// WAL is replayed building up a single version edit that is
// applied. Because we don't apply the version edit after each flush, this
// code doesn't know that L0 contains files and zeroing of seqnums should
// be disabled. That is fixable, but it seems safer to just match the
// RocksDB behavior for now.
if c.disableRangeTombstoneElision || len(c.flushing) != 0 {
return false
}
lower := sort.Search(len(c.inuseKeyRanges), func(i int) bool {
return c.cmp(c.inuseKeyRanges[i].End, start) >= 0
})
upper := sort.Search(len(c.inuseKeyRanges), func(i int) bool {
return c.cmp(c.inuseKeyRanges[i].Start, end) > 0
})
return lower >= upper
}
// newInputIter returns an iterator over all the input tables in a compaction.
func (c *compaction) newInputIter(newIters tableNewIters) (_ internalIterator, retErr error) {
if len(c.flushing) != 0 {
if len(c.flushing) == 1 {
f := c.flushing[0]
iter := f.newFlushIter(nil, &c.bytesIterated)
if rangeDelIter := f.newRangeDelIter(nil); rangeDelIter != nil {
return newMergingIter(c.logger, c.cmp, nil, iter, rangeDelIter), nil
}
return iter, nil
}
iters := make([]internalIterator, 0, 2*len(c.flushing))
for i := range c.flushing {
f := c.flushing[i]
iters = append(iters, f.newFlushIter(nil, &c.bytesIterated))
rangeDelIter := f.newRangeDelIter(nil)
if rangeDelIter != nil {
iters = append(iters, rangeDelIter)
}
}
return newMergingIter(c.logger, c.cmp, nil, iters...), nil
}
// Check that the LSM ordering invariants are ok in order to prevent
// generating corrupted sstables due to a violation of those invariants.
if c.startLevel.level >= 0 {
err := manifest.CheckOrdering(c.cmp, c.formatKey,
manifest.Level(c.startLevel.level), c.startLevel.files.Iter())
if err != nil {
return nil, err
}
}
err := manifest.CheckOrdering(c.cmp, c.formatKey,
manifest.Level(c.outputLevel.level), c.outputLevel.files.Iter())
if err != nil {
return nil, err
}
iters := make([]internalIterator, 0, 2*c.startLevel.files.Len()+1)
defer func() {