-
Notifications
You must be signed in to change notification settings - Fork 465
/
compaction.go
3887 lines (3599 loc) · 140 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"
"cmp"
"context"
"fmt"
"io"
"math"
"runtime/pprof"
"slices"
"sort"
"sync/atomic"
"time"
"github.com/cockroachdb/errors"
"github.com/cockroachdb/pebble/internal/base"
"github.com/cockroachdb/pebble/internal/invalidating"
"github.com/cockroachdb/pebble/internal/invariants"
"github.com/cockroachdb/pebble/internal/keyspan"
"github.com/cockroachdb/pebble/internal/manifest"
"github.com/cockroachdb/pebble/internal/private"
"github.com/cockroachdb/pebble/internal/rangedel"
"github.com/cockroachdb/pebble/internal/rangekey"
"github.com/cockroachdb/pebble/objstorage"
"github.com/cockroachdb/pebble/objstorage/objstorageprovider/objiotracing"
"github.com/cockroachdb/pebble/objstorage/remote"
"github.com/cockroachdb/pebble/sstable"
"github.com/cockroachdb/pebble/vfs"
)
var errEmptyTable = errors.New("pebble: empty table")
// ErrCancelledCompaction is returned if a compaction is cancelled by a
// concurrent excise or ingest-split operation.
var ErrCancelledCompaction = errors.New("pebble: compaction cancelled by a concurrent operation, will retry compaction")
var compactLabels = pprof.Labels("pebble", "compact")
var flushLabels = pprof.Labels("pebble", "flush")
var gcLabels = pprof.Labels("pebble", "gc")
// getInternalWriterProperties accesses a private variable (in the
// internal/private package) initialized by the sstable Writer. This indirection
// is necessary to ensure non-Pebble users constructing sstables for ingestion
// are unable to set internal-only properties.
var getInternalWriterProperties = private.SSTableInternalProperties.(func(*sstable.Writer) *sstable.Properties)
// 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, availBytes uint64) uint64 {
v := uint64(25 * opts.Level(level).TargetFileSize)
// Never expand a compaction beyond half the available capacity, divided
// by the maximum number of concurrent compactions. Each of the concurrent
// compactions may expand up to this limit, so this attempts to limit
// compactions to half of available disk space. Note that this will not
// prevent compaction picking from pursuing compactions that are larger
// than this threshold before expansion.
diskMax := (availBytes / 2) / uint64(opts.MaxConcurrentCompactions())
if v > diskMax {
v = diskMax
}
return v
}
// 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)
}
// maxReadCompactionBytes is used to prevent read compactions which
// are too wide.
func maxReadCompactionBytes(opts *Options, level int) uint64 {
return uint64(10 * opts.Level(level).TargetFileSize)
}
// noCloseIter wraps around a FragmentIterator, intercepting and eliding
// calls to Close. It is used during compaction to ensure that rangeDelIters
// are not closed prematurely.
type noCloseIter struct {
keyspan.FragmentIterator
}
func (i noCloseIter) Close() error {
return nil
}
type compactionLevel struct {
level int
files manifest.LevelSlice
// l0SublevelInfo contains information about L0 sublevels being compacted.
// It's only set for the start level of a compaction starting out of L0 and
// is nil for all other compactions.
l0SublevelInfo []sublevelInfo
}
func (cl compactionLevel) Clone() compactionLevel {
newCL := compactionLevel{
level: cl.level,
files: cl.files.Reslice(func(start, end *manifest.LevelIterator) {}),
}
return newCL
}
func (cl compactionLevel) String() string {
return fmt.Sprintf(`Level %d, Files %s`, cl.level, cl.files)
}
// Return output from compactionOutputSplitters. See comment on
// compactionOutputSplitter.shouldSplitBefore() on how this value is used.
type maybeSplit int
const (
noSplit maybeSplit = iota
splitNow
)
// String implements the Stringer interface.
func (c maybeSplit) 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. If shouldSplitBefore(a) advises a split then
// shouldSplitBefore(b) should also advise a split given b >= a, until
// onNewOutput is called.
shouldSplitBefore(key *InternalKey, tw *sstable.Writer) maybeSplit
// 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 []byte) []byte
}
// fileSizeSplitter is a compactionOutputSplitter that enforces target file
// sizes. This splitter splits to a new output file when the estimated file size
// is 0.5x-2x the target file size. If there are overlapping grandparent files,
// this splitter will attempt to split at a grandparent boundary. For example,
// consider the example where a compaction wrote 'd' to the current output file,
// and the next key has a user key 'g':
//
// previous key next key
// | |
// | |
// +---------------|----+ +--|----------+
// grandparents: | 000006 | | | | 000007 |
// +---------------|----+ +--|----------+
// a b d e f g i
//
// Splitting the output file F before 'g' will ensure that the current output
// file F does not overlap the grandparent file 000007. Aligning sstable
// boundaries like this can significantly reduce write amplification, since a
// subsequent compaction of F into the grandparent level will avoid needlessly
// rewriting any keys within 000007 that do not overlap F's bounds. Consider the
// following compaction:
//
// +----------------------+
// input | |
// level +----------------------+
// \/
// +---------------+ +---------------+
// output |XXXXXXX| | | |XXXXXXXX|
// level +---------------+ +---------------+
//
// The input-level file overlaps two files in the output level, but only
// partially. The beginning of the first output-level file and the end of the
// second output-level file will be rewritten verbatim. This write I/O is
// "wasted" in the sense that no merging is being performed.
//
// To prevent the above waste, this splitter attempts to split output files
// before the start key of grandparent files. It still strives to write output
// files of approximately the target file size, by constraining this splitting
// at grandparent points to apply only if the current output's file size is
// about the right order of magnitude.
//
// 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 {
frontier frontier
targetFileSize uint64
atGrandparentBoundary bool
boundariesObserved uint64
nextGrandparent *fileMetadata
grandparents manifest.LevelIterator
}
func newFileSizeSplitter(
f *frontiers, targetFileSize uint64, grandparents manifest.LevelIterator,
) *fileSizeSplitter {
s := &fileSizeSplitter{targetFileSize: targetFileSize}
s.nextGrandparent = grandparents.First()
s.grandparents = grandparents
if s.nextGrandparent != nil {
s.frontier.Init(f, s.nextGrandparent.Smallest.UserKey, s.reached)
}
return s
}
func (f *fileSizeSplitter) reached(nextKey []byte) []byte {
f.atGrandparentBoundary = true
f.boundariesObserved++
// NB: f.grandparents is a bounded iterator, constrained to the compaction
// key range.
f.nextGrandparent = f.grandparents.Next()
if f.nextGrandparent == nil {
return nil
}
// TODO(jackson): Should we also split before or immediately after
// grandparents' largest keys? Splitting before the start boundary prevents
// overlap with the grandparent. Also splitting after the end boundary may
// increase the probability of move compactions.
return f.nextGrandparent.Smallest.UserKey
}
func (f *fileSizeSplitter) shouldSplitBefore(key *InternalKey, tw *sstable.Writer) maybeSplit {
atGrandparentBoundary := f.atGrandparentBoundary
// Clear f.atGrandparentBoundary unconditionally.
//
// This is a bit subtle. Even if do decide to split, it's possible that a
// higher-level splitter will ignore our request (eg, because we're between
// two internal keys with the same user key). In this case, the next call to
// shouldSplitBefore will find atGrandparentBoundary=false. This is
// desirable, because in this case we would've already written the earlier
// key with the same user key to the output file. The current output file is
// already doomed to overlap the grandparent whose bound triggered
// atGrandparentBoundary=true. We should continue on, waiting for the next
// grandparent boundary.
f.atGrandparentBoundary = false
// If the key is a range tombstone, the EstimatedSize may not grow right
// away when a range tombstone is added to the fragmenter: It's dependent on
// whether or not the this new range deletion will start a new fragment.
// Range deletions are rare, so we choose to simply not split yet.
// TODO(jackson): Reconsider this, and consider range keys too as a part of
// #2321.
if key.Kind() == InternalKeyKindRangeDelete || tw == nil {
return noSplit
}
estSize := tw.EstimatedSize()
switch {
case estSize < f.targetFileSize/2:
// The estimated file size is less than half the target file size. Don't
// split it, even if currently aligned with a grandparent file because
// it's too small.
return noSplit
case estSize >= 2*f.targetFileSize:
// The estimated file size is double the target file size. Split it even
// if we were not aligned with a grandparent file boundary to avoid
// excessively exceeding the target file size.
return splitNow
case !atGrandparentBoundary:
// Don't split if we're not at a grandparent, except if we've exhausted
// all the grandparents overlapping this compaction's key range. Then we
// may want to split purely based on file size.
if f.nextGrandparent == nil {
// There are no more grandparents. Optimize for the target file size
// and split as soon as we hit the target file size.
if estSize >= f.targetFileSize {
return splitNow
}
}
return noSplit
default:
// INVARIANT: atGrandparentBoundary
// INVARIANT: targetSize/2 < estSize < 2*targetSize
//
// The estimated file size is close enough to the target file size that
// we should consider splitting.
//
// Determine whether to split now based on how many grandparent
// boundaries we have already observed while building this output file.
// The intuition here is that if the grandparent level is dense in this
// part of the keyspace, we're likely to continue to have more
// opportunities to split this file aligned with a grandparent. If this
// is the first grandparent boundary observed, we split immediately
// (we're already at ≥50% the target file size). Otherwise, each
// overlapping grandparent we've observed increases the minimum file
// size by 5% of the target file size, up to at most 90% of the target
// file size.
//
// TODO(jackson): The particular thresholds are somewhat unprincipled.
// This is the same heuristic as RocksDB implements. Is there are more
// principled formulation that can, further reduce w-amp, produce files
// closer to the target file size, or is more understandable?
// NB: Subtract 1 from `boundariesObserved` to account for the current
// boundary we're considering splitting at. `reached` will have
// incremented it at the same time it set `atGrandparentBoundary`.
minimumPctOfTargetSize := 50 + 5*min(f.boundariesObserved-1, 8)
if estSize < (minimumPctOfTargetSize*f.targetFileSize)/100 {
return noSplit
}
return splitNow
}
}
func (f *fileSizeSplitter) onNewOutput(key []byte) []byte {
f.boundariesObserved = 0
return nil
}
func newLimitFuncSplitter(f *frontiers, limitFunc func(userKey []byte) []byte) *limitFuncSplitter {
s := &limitFuncSplitter{limitFunc: limitFunc}
s.frontier.Init(f, nil, s.reached)
return s
}
type limitFuncSplitter struct {
frontier frontier
limitFunc func(userKey []byte) []byte
split maybeSplit
}
func (lf *limitFuncSplitter) shouldSplitBefore(key *InternalKey, tw *sstable.Writer) maybeSplit {
return lf.split
}
func (lf *limitFuncSplitter) reached(nextKey []byte) []byte {
lf.split = splitNow
return nil
}
func (lf *limitFuncSplitter) onNewOutput(key []byte) []byte {
lf.split = noSplit
if key != nil {
// TODO(jackson): For some users, like L0 flush splits, there's no need
// to binary search over all the flush splits every time. The next split
// point must be ahead of the previous flush split point.
limit := lf.limitFunc(key)
lf.frontier.Update(limit)
return limit
}
lf.frontier.Update(nil)
return nil
}
// 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 maybeSplit) {
for _, splitter := range a.splitters {
if splitter.shouldSplitBefore(key, tw) == splitNow {
return splitNow
}
}
return noSplit
}
func (a *splitterGroup) onNewOutput(key []byte) []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.) If a wrapped splitter advises a split, it must continue
// to advise a split until a new output.
type userKeyChangeSplitter struct {
cmp Compare
splitter compactionOutputSplitter
unsafePrevUserKey func() []byte
}
func (u *userKeyChangeSplitter) shouldSplitBefore(key *InternalKey, tw *sstable.Writer) maybeSplit {
// NB: The userKeyChangeSplitter only needs to suffer a key comparison if
// the wrapped splitter requests a split.
//
// We could implement this splitter using frontiers: When the inner splitter
// requests a split before key `k`, we'd update a frontier to be
// ImmediateSuccessor(k). Then on the next key greater than >k, the
// frontier's `reached` func would be called and we'd return splitNow.
// This doesn't really save work since duplicate user keys are rare, and it
// requires us to materialize the ImmediateSuccessor key. It also prevents
// us from splitting on the same key that the inner splitter requested a
// split for—instead we need to wait until the next key. The current
// implementation uses `unsafePrevUserKey` to gain access to the previous
// key which allows it to immediately respect the inner splitter if
// possible.
if split := u.splitter.shouldSplitBefore(key, tw); split != splitNow {
return split
}
if u.cmp(key.UserKey, u.unsafePrevUserKey()) > 0 {
return splitNow
}
return noSplit
}
func (u *userKeyChangeSplitter) onNewOutput(key []byte) []byte {
return u.splitter.onNewOutput(key)
}
// compactionWritable is a objstorage.Writable 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 compactionWritable struct {
objstorage.Writable
versions *versionSet
written *int64
}
// Write is part of the objstorage.Writable interface.
func (c *compactionWritable) Write(p []byte) error {
if err := c.Writable.Write(p); err != nil {
return err
}
*c.written += int64(len(p))
c.versions.incrementCompactionBytes(int64(len(p)))
return nil
}
type compactionKind int
const (
compactionKindDefault compactionKind = iota
compactionKindFlush
// compactionKindMove denotes a move compaction where the input file is
// retained and linked in a new level without being obsoleted.
compactionKindMove
// compactionKindCopy denotes a copy compaction where the input file is
// copied byte-by-byte into a new file with a new FileNum in the output level.
compactionKindCopy
compactionKindDeleteOnly
compactionKindElisionOnly
compactionKindRead
compactionKindRewrite
compactionKindIngestedFlushable
)
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"
case compactionKindRewrite:
return "rewrite"
case compactionKindIngestedFlushable:
return "ingested-flushable"
case compactionKindCopy:
return "copy"
}
return "?"
}
// rangeKeyCompactionTransform is used to transform range key spans as part of the
// keyspan.MergingIter. As part of this transformation step, we can elide range
// keys in the last snapshot stripe, as well as coalesce range keys within
// snapshot stripes.
func rangeKeyCompactionTransform(
eq base.Equal, snapshots []uint64, elideRangeKey func(start, end []byte) bool,
) keyspan.Transformer {
return keyspan.TransformerFunc(func(cmp base.Compare, s keyspan.Span, dst *keyspan.Span) error {
elideInLastStripe := func(keys []keyspan.Key) []keyspan.Key {
// Unsets and deletes in the last snapshot stripe can be elided.
k := 0
for j := range keys {
if elideRangeKey(s.Start, s.End) &&
(keys[j].Kind() == InternalKeyKindRangeKeyUnset || keys[j].Kind() == InternalKeyKindRangeKeyDelete) {
continue
}
keys[k] = keys[j]
k++
}
keys = keys[:k]
return keys
}
// snapshots are in ascending order, while s.keys are in descending seqnum
// order. Partition s.keys by snapshot stripes, and call rangekey.Coalesce
// on each partition.
dst.Start = s.Start
dst.End = s.End
dst.Keys = dst.Keys[:0]
i, j := len(snapshots)-1, 0
usedLen := 0
for i >= 0 {
start := j
for j < len(s.Keys) && !base.Visible(s.Keys[j].SeqNum(), snapshots[i], base.InternalKeySeqNumMax) {
// Include j in current partition.
j++
}
if j > start {
keysDst := dst.Keys[usedLen:cap(dst.Keys)]
if err := rangekey.Coalesce(cmp, eq, s.Keys[start:j], &keysDst); err != nil {
return err
}
if j == len(s.Keys) {
// This is the last snapshot stripe. Unsets and deletes can be elided.
keysDst = elideInLastStripe(keysDst)
}
usedLen += len(keysDst)
dst.Keys = append(dst.Keys, keysDst...)
}
i--
}
if j < len(s.Keys) {
keysDst := dst.Keys[usedLen:cap(dst.Keys)]
if err := rangekey.Coalesce(cmp, eq, s.Keys[j:], &keysDst); err != nil {
return err
}
keysDst = elideInLastStripe(keysDst)
usedLen += len(keysDst)
dst.Keys = append(dst.Keys, keysDst...)
}
return nil
})
}
// compaction is a table compaction from one level to the next, starting from a
// given version.
type compaction struct {
// cancel is a bool that can be used by other goroutines to signal a compaction
// to cancel, such as if a conflicting excise operation raced it to manifest
// application. Only holders of the manifest lock will write to this atomic.
cancel atomic.Bool
kind compactionKind
cmp Compare
equal Equal
comparer *base.Comparer
formatKey base.FormatKey
logger Logger
version *version
stats base.InternalIteratorStats
beganAt time.Time
// versionEditApplied is set to true when a compaction has completed and the
// resulting version has been installed (if successful), but the compaction
// goroutine is still cleaning up (eg, deleting obsolete files).
versionEditApplied bool
bufferPool sstable.BufferPool
// 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:
// - if startLevel is 0, the output level equals compactionPicker.baseLevel().
// - in multilevel compaction, the output level is the lowest level involved in
// the compaction
// A compaction's outputLevel is nil for delete-only compactions.
outputLevel *compactionLevel
// extraLevels point to additional levels in between the input and output
// levels that get compacted in multilevel compactions
extraLevels []*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
// disableSpanElision disables elision of range tombstones and range keys. Used
// by tests to allow range tombstones or range keys to be added to tables where
// they would otherwise be elided.
disableSpanElision 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
// 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 keyspan.Fragmenter
// The range key fragmenter. Similar to rangeDelFrag in that it gets range
// keys from the compaction iter and fragments them for output to files.
rangeKeyFrag keyspan.Fragmenter
// The range deletion tombstone iterator, that merges and fragments
// tombstones across levels. This iterator is included within the compaction
// input iterator as a single level.
// TODO(jackson): Remove this when the refactor of FragmentIterator,
// InterleavingIterator, etc is complete.
rangeDelIter keyspan.InternalIteratorShim
// rangeKeyInterleaving is the interleaving iter for range keys.
rangeKeyInterleaving keyspan.InterleavingIter
// 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
// inuseEntireRange is set if the above inuse key ranges wholly contain the
// compaction's key range. This allows compactions in higher levels to often
// elide key comparisons.
inuseEntireRange bool
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
pickerMetrics compactionPickerMetrics
}
func (c *compaction) makeInfo(jobID int) CompactionInfo {
info := CompactionInfo{
JobID: jobID,
Reason: c.kind.String(),
Input: make([]LevelInfo, 0, len(c.inputs)),
Annotations: []string{},
}
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
}
for i, score := range c.pickerMetrics.scores {
info.Input[i].Score = score
}
info.SingleLevelOverlappingRatio = c.pickerMetrics.singleLevelOverlappingRatio
info.MultiLevelOverlappingRatio = c.pickerMetrics.multiLevelOverlappingRatio
if len(info.Input) > 2 {
info.Annotations = append(info.Annotations, "multilevel")
}
return info
}
func newCompaction(
pc *pickedCompaction, opts *Options, beganAt time.Time, provider objstorage.Provider,
) *compaction {
c := &compaction{
kind: compactionKindDefault,
cmp: pc.cmp,
equal: opts.equal(),
comparer: opts.Comparer,
formatKey: opts.Comparer.FormatKey,
inputs: pc.inputs,
smallest: pc.smallest,
largest: pc.largest,
logger: opts.Logger,
version: pc.version,
beganAt: beganAt,
maxOutputFileSize: pc.maxOutputFileSize,
maxOverlapBytes: pc.maxOverlapBytes,
pickerMetrics: pc.pickerMetrics,
}
c.startLevel = &c.inputs[0]
if pc.startLevel.l0SublevelInfo != nil {
c.startLevel.l0SublevelInfo = pc.startLevel.l0SublevelInfo
}
c.outputLevel = &c.inputs[1]
if len(pc.extraLevels) > 0 {
c.extraLevels = pc.extraLevels
c.outputLevel = &c.inputs[len(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.largest.IsExclusiveSentinel())
}
c.setupInuseKeyRanges()
c.kind = pc.kind
if c.kind == compactionKindDefault && c.outputLevel.files.Empty() && !c.hasExtraLevelData() &&
c.startLevel.files.Len() == 1 && c.grandparents.SizeSum() <= c.maxOverlapBytes {
// This compaction can be converted into a move or copy 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.
iter := c.startLevel.files.Iter()
meta := iter.First()
isRemote := false
// We should always be passed a provider, except in some unit tests.
if provider != nil {
objMeta, err := provider.Lookup(fileTypeTable, meta.FileBacking.DiskFileNum)
if err != nil {
panic(errors.Wrapf(err, "cannot lookup table %s in provider", meta.FileBacking.DiskFileNum))
}
isRemote = objMeta.IsRemote()
}
// Avoid a trivial move or copy if all of these are true, as rewriting a
// new file is better:
//
// 1) The source file is a virtual sstable
// 2) The existing file `meta` is on non-remote storage
// 3) The output level prefers shared storage
mustCopy := !isRemote && remote.ShouldCreateShared(opts.Experimental.CreateOnShared, c.outputLevel.level)
if mustCopy {
// If the source is virtual, it's best to just rewrite the file as all
// conditions in the above comment are met.
if !meta.Virtual {
c.kind = compactionKindCopy
}
} else {
c.kind = compactionKindMove
}
}
return c
}
func newDeleteOnlyCompaction(
opts *Options, cur *version, inputs []compactionLevel, beganAt time.Time,
) *compaction {
c := &compaction{
kind: compactionKindDeleteOnly,
cmp: opts.Comparer.Compare,
equal: opts.equal(),
comparer: opts.Comparer,
formatKey: opts.Comparer.FormatKey,
logger: opts.Logger,
version: cur,
beganAt: beganAt,
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, beganAt time.Time,
) *compaction {
c := &compaction{
kind: compactionKindFlush,
cmp: opts.Comparer.Compare,
equal: opts.equal(),
comparer: opts.Comparer,
formatKey: opts.Comparer.FormatKey,
logger: opts.Logger,
version: cur,
beganAt: beganAt,
inputs: []compactionLevel{{level: -1}, {level: 0}},
maxOutputFileSize: math.MaxUint64,
maxOverlapBytes: math.MaxUint64,
flushing: flushing,
}
c.startLevel = &c.inputs[0]
c.outputLevel = &c.inputs[1]
if len(flushing) > 0 {
if _, ok := flushing[0].flushable.(*ingestedFlushable); ok {
if len(flushing) != 1 {
panic("pebble: ingestedFlushable must be flushed one at a time.")
}
c.kind = compactionKindIngestedFlushable
return c
}
}
// Make sure there's no ingestedFlushable after the first flushable in the
// list.
for _, f := range flushing {
if _, ok := f.flushable.(*ingestedFlushable); ok {
panic("pebble: flushing shouldn't contain ingestedFlushable flushable")
}
}
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 keyspan.FragmentIterator) {
// File bounds require s != nil && !s.Empty(). We only need to check for
// s != nil here, as the memtable's FragmentIterator would never surface
// empty spans.
if s := iter.First(); s != nil {
if key := s.SmallestKey(); !smallestSet ||
base.InternalCompare(c.cmp, c.smallest, key) > 0 {
smallestSet = true
c.smallest = key.Clone()
}
}
if s := iter.Last(); s != nil {
if key := s.LargestKey(); !largestSet ||
base.InternalCompare(c.cmp, c.largest, key) < 0 {
largestSet = true
c.largest = key.Clone()
}
}
}
var flushingBytes uint64
for i := range flushing {
f := flushing[i]
updatePointBounds(f.newIter(nil))
if rangeDelIter := f.newRangeDelIter(nil); rangeDelIter != nil {
updateRangeBounds(rangeDelIter)
}
if rangeKeyIter := f.newRangeKeyIter(nil); rangeKeyIter != nil {
updateRangeBounds(rangeKeyIter)
}
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, c.largest.IsExclusiveSentinel())
adjustGrandparentOverlapBytesForFlush(c, flushingBytes)
}
c.setupInuseKeyRanges()
return c
}
func (c *compaction) hasExtraLevelData() bool {
if len(c.extraLevels) == 0 {
// not a multi level compaction
return false
} else if c.extraLevels[0].files.Empty() {
// a multi level compaction without data in the intermediate input level;
// e.g. for a multi level compaction with levels 4,5, and 6, this could
// occur if there is no files to compact in 5, or in 5 and 6 (i.e. a move).
return false
}
return true
}
func (c *compaction) setupInuseKeyRanges() {
level := c.outputLevel.level + 1