Skip to content

Commit 542915b

Browse files
committed
ingest: perform LSM overlap checks outside of DB.mu
This change separates the LSM overlap checking from `ingestTargetLevel`, with the former now happening without holding `DB.mu`. We will make more improvements in this area soon, but this change should be a significant improvement on its own.
1 parent 8b278ee commit 542915b

File tree

4 files changed

+131
-49
lines changed

4 files changed

+131
-49
lines changed

compaction.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1244,9 +1244,14 @@ func (d *DB) runIngestFlush(c *compaction) (*manifest.VersionEdit, error) {
12441244
ingestFlushable.exciseSpan.Contains(d.cmp, file.FileMetadata.Largest) {
12451245
level = 6
12461246
} else {
1247-
var err error
1248-
level, fileToSplit, err = ingestTargetLevel(ctx, overlapChecker,
1249-
baseLevel, d.mu.compact.inProgress, file.FileMetadata, suggestSplit)
1247+
// TODO(radu): this can perform I/O; we should not do this while holding DB.mu.
1248+
lsmOverlap, err := overlapChecker.DetermineLSMOverlap(ctx, file.UserKeyBounds())
1249+
if err != nil {
1250+
return nil, err
1251+
}
1252+
level, fileToSplit, err = ingestTargetLevel(
1253+
ctx, d.cmp, lsmOverlap, baseLevel, d.mu.compact.inProgress, file.FileMetadata, suggestSplit,
1254+
)
12501255
if err != nil {
12511256
return nil, err
12521257
}

ingest.go

Lines changed: 37 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -831,7 +831,8 @@ func ingestUpdateSeqNum(
831831
// is returned as the splitFile.
832832
func ingestTargetLevel(
833833
ctx context.Context,
834-
overlapChecker *overlapChecker,
834+
cmp base.Compare,
835+
lsmOverlap lsmOverlap,
835836
baseLevel int,
836837
compactions map[*compaction]struct{},
837838
meta *fileMetadata,
@@ -903,46 +904,32 @@ func ingestTargetLevel(
903904
// existing point that falls within the ingested table bounds as being "data
904905
// overlap".
905906

906-
// This assertion implicitly checks that we have the current version of
907-
// the metadata.
908-
if overlapChecker.v.L0Sublevels == nil {
909-
return 0, nil, base.AssertionFailedf("could not read L0 sublevels")
910-
}
911-
bounds := meta.UserKeyBounds()
912-
913-
// Check for overlap over the keys of L0.
914-
if overlap, err := overlapChecker.DetermineAnyDataOverlapInLevel(ctx, bounds, 0); err != nil {
915-
return 0, nil, err
916-
} else if overlap {
907+
if lsmOverlap[0].result == dataOverlap {
917908
return 0, nil, nil
918909
}
919-
910+
targetLevel = 0
911+
splitFile = nil
920912
for level := baseLevel; level < numLevels; level++ {
921-
dataOverlap, err := overlapChecker.DetermineAnyDataOverlapInLevel(ctx, bounds, level)
922-
if err != nil {
923-
return 0, nil, err
924-
} else if dataOverlap {
913+
var candidateSplitFile *fileMetadata
914+
switch lsmOverlap[level].result {
915+
case dataOverlap:
916+
// We cannot ingest into or under this level; return the best target level
917+
// so far.
925918
return targetLevel, splitFile, nil
926-
}
927919

928-
// Check boundary overlap.
929-
var candidateSplitFile *fileMetadata
930-
boundaryOverlaps := overlapChecker.v.Overlaps(level, bounds)
931-
if !boundaryOverlaps.Empty() {
932-
// We are already guaranteed to not have any data overlaps with files
933-
// in boundaryOverlaps, otherwise we'd have returned in the above if
934-
// statements. Use this, plus boundaryOverlaps.Len() == 1 to detect for
935-
// the case where we can slot this file into the current level despite
936-
// a boundary overlap, by splitting one existing file into two virtual
937-
// sstables.
938-
if suggestSplit && boundaryOverlaps.Len() == 1 {
939-
iter := boundaryOverlaps.Iter()
940-
candidateSplitFile = iter.First()
941-
} else {
942-
// We either don't want to suggest ingest-time splits (i.e.
943-
// !suggestSplit), or we boundary-overlapped with more than one file.
920+
case noDataOverlap:
921+
if !suggestSplit || lsmOverlap[level].splitFile == nil {
922+
// We can ingest under this level, but not into this level.
944923
continue
945924
}
925+
// We can ingest into this level if we split this file.
926+
candidateSplitFile = lsmOverlap[level].splitFile
927+
928+
case noBoundaryOverlap:
929+
// We can ingest into this level.
930+
931+
default:
932+
return 0, nil, base.AssertionFailedf("unexpected lsmOverlap result: %v", lsmOverlap[level].result)
946933
}
947934

948935
// Check boundary overlap with any ongoing compactions. We consider an
@@ -964,8 +951,8 @@ func ingestTargetLevel(
964951
if c.outputLevel == nil || level != c.outputLevel.level {
965952
continue
966953
}
967-
if overlapChecker.comparer.Compare(meta.Smallest.UserKey, c.largest.UserKey) <= 0 &&
968-
overlapChecker.comparer.Compare(meta.Largest.UserKey, c.smallest.UserKey) >= 0 {
954+
if cmp(meta.Smallest.UserKey, c.largest.UserKey) <= 0 &&
955+
cmp(meta.Largest.UserKey, c.smallest.UserKey) >= 0 {
969956
overlaps = true
970957
break
971958
}
@@ -2202,14 +2189,20 @@ func (d *DB) ingestApply(
22022189
f.Level = 6
22032190
}
22042191
} else {
2205-
// TODO(bilal): ingestTargetLevel does disk IO (reading files for data
2206-
// overlap) even though we're holding onto d.mu. Consider unlocking
2207-
// d.mu while we do this. We already hold versions.logLock so we should
2208-
// not see any version applications while we're at this. The one
2209-
// complication here would be pulling out the mu.compact.inProgress
2210-
// check from ingestTargetLevel, as that requires d.mu to be held.
2211-
f.Level, splitFile, err = ingestTargetLevel(ctx, overlapChecker,
2212-
baseLevel, d.mu.compact.inProgress, m, shouldIngestSplit)
2192+
// We check overlap against the LSM without holding DB.mu. Note that we
2193+
// are still holding the log lock, so the version cannot change.
2194+
// TODO(radu): perform this check optimistically outside of the log lock.
2195+
var overlap lsmOverlap
2196+
overlap, err = func() (lsmOverlap, error) {
2197+
d.mu.Unlock()
2198+
defer d.mu.Lock()
2199+
return overlapChecker.DetermineLSMOverlap(ctx, m.UserKeyBounds())
2200+
}()
2201+
if err == nil {
2202+
f.Level, splitFile, err = ingestTargetLevel(
2203+
ctx, d.cmp, overlap, baseLevel, d.mu.compact.inProgress, m, shouldIngestSplit,
2204+
)
2205+
}
22132206
}
22142207

22152208
if splitFile != nil {

ingest_test.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2290,8 +2290,12 @@ func TestIngestTargetLevel(t *testing.T) {
22902290
},
22912291
v: d.mu.versions.currentVersion(),
22922292
}
2293-
level, overlapFile, err := ingestTargetLevel(context.Background(), overlapChecker,
2294-
1, d.mu.compact.inProgress, meta, suggestSplit)
2293+
lsmOverlap, err := overlapChecker.DetermineLSMOverlap(context.Background(), meta.UserKeyBounds())
2294+
if err != nil {
2295+
return err.Error()
2296+
}
2297+
level, overlapFile, err := ingestTargetLevel(
2298+
context.Background(), d.cmp, lsmOverlap, 1, d.mu.compact.inProgress, meta, suggestSplit)
22952299
if err != nil {
22962300
return err.Error()
22972301
}

overlap.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,83 @@ type overlapChecker struct {
2828
keyspanLevelIter keyspanimpl.LevelIter
2929
}
3030

31+
// levelOverlapResult indicates the result of checking data overlap between a
32+
// key range and a level. We check two types of overlap:
33+
//
34+
// - file boundary overlap: whether the key range overlaps any of the level's
35+
// user key boundaries;
36+
//
37+
// - data overlap: whether the key range overlaps any keys or ranges in the
38+
// level. Data overlap implies file boundary overlap.
39+
type levelOverlapResult uint8
40+
41+
const (
42+
// The key range of interest doesn't overlap any tables on the level.
43+
noBoundaryOverlap levelOverlapResult = iota + 1
44+
// The key range of interest overlaps some tables on the level in terms of
45+
// boundary, but the tables contain no data within that range.
46+
noDataOverlap
47+
// At least a key or range in the level overlaps with the key range of
48+
// interest. Note that the data overlap check is best-effort and there could
49+
// be false positives.
50+
dataOverlap
51+
)
52+
53+
// levelOverlap is the result of checking overlap against an LSM level.
54+
type levelOverlap struct {
55+
result levelOverlapResult
56+
// splitFile can be set only when result is noDataOverlap. If it is set, this
57+
// file can be split to free up the range of interest.
58+
splitFile *fileMetadata
59+
}
60+
61+
// lsmOverlap stores the result of checking for data overlap for the LSM levels,
62+
// starting from the top (L0). The overlap checks are only populated up to the
63+
// first level where we find data overlap.
64+
//
65+
// This is akin to a tetris game where a horizontal bar is falling and we want
66+
// to determine where it lands.
67+
type lsmOverlap [numLevels]levelOverlap
68+
69+
// DetermineLSMOverlap calculates the lsmOverlap for the given bounds.
70+
func (c *overlapChecker) DetermineLSMOverlap(
71+
ctx context.Context, bounds base.UserKeyBounds,
72+
) (lsmOverlap, error) {
73+
var result lsmOverlap
74+
for level := 0; level < numLevels; level++ {
75+
var err error
76+
result[level], err = c.DetermineLevelOverlap(ctx, bounds, level)
77+
if err != nil || result[level].result == dataOverlap {
78+
return result, err
79+
}
80+
}
81+
return result, nil
82+
}
83+
84+
func (c *overlapChecker) DetermineLevelOverlap(
85+
ctx context.Context, bounds base.UserKeyBounds, level int,
86+
) (levelOverlap, error) {
87+
// First, check boundary overlap.
88+
boundaryOverlaps := c.v.Overlaps(level, bounds)
89+
if boundaryOverlaps.Empty() {
90+
return levelOverlap{result: noBoundaryOverlap}, nil
91+
}
92+
93+
overlap, err := c.DetermineAnyDataOverlapInLevel(ctx, bounds, level)
94+
if err != nil || overlap {
95+
return levelOverlap{result: dataOverlap}, err
96+
}
97+
var splitFile *fileMetadata
98+
if boundaryOverlaps.Len() == 1 {
99+
iter := boundaryOverlaps.Iter()
100+
splitFile = iter.First()
101+
}
102+
return levelOverlap{
103+
result: noDataOverlap,
104+
splitFile: splitFile,
105+
}, nil
106+
}
107+
31108
// DetermineAnyDataOverlapInLevel checks whether any keys within the provided
32109
// bounds and level exist within the checker's associated LSM version. This
33110
// function checks for actual keys within the bounds, performing I/O if
@@ -52,6 +129,9 @@ func (c *overlapChecker) DetermineAnyDataOverlapInLevel(
52129
// NB: sublevel 0 contains the newest keys, whereas sublevel n contains the
53130
// oldest keys.
54131
if level == 0 {
132+
if c.v.L0Sublevels == nil {
133+
return true, base.AssertionFailedf("could not read L0 sublevels")
134+
}
55135
for subLevel := 0; subLevel < len(c.v.L0SublevelFiles); subLevel++ {
56136
manifestIter := c.v.L0Sublevels.Levels[subLevel].Iter()
57137
pointOverlap, err := c.determinePointKeyOverlapInLevel(

0 commit comments

Comments
 (0)