Skip to content

Commit 8ab4b15

Browse files
committed
db: fix obsolete file metric underflows
Previously a race existed in the calculation of obsolete file metrics resulting in underflow. The values within versionSet.metrics were reset and recalculated to reflect the set of files in versionSet.obsolete{Tables,Blobs} whenever new files were added to obsoleteFiles. Additionally, the cleanup manager invoked a callback to decrease versionSet.metrics whenever a table was deleted. The recalculation of versionSet.metrics could reset the metrics to less than the sum of outstanding pending deletes. When the cleanup manager eventually deleted the pending tables, these metrics would underflow. This commit fixes the bug by maintaining separate stats for all files that have been enqueued for the cleanup manager and all files that have been successfully deleted. The volume of outstanding, pending deletions is the difference between the two. For now, there's an additional wart that the set of files that are sitting in versionSet.obsolete{Table,Blobs} are still separately tracked. Fix #4811.
1 parent 3825430 commit 8ab4b15

File tree

4 files changed

+126
-36
lines changed

4 files changed

+126
-36
lines changed

db.go

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -453,10 +453,16 @@ type DB struct {
453453
noOngoingFlushStartTime crtime.Mono
454454
}
455455

456-
// Non-zero when file cleaning is disabled. The disabled count acts as a
457-
// reference count to prohibit file cleaning. See
458-
// DB.{disable,Enable}FileDeletions().
459-
disableFileDeletions int
456+
fileDeletions struct {
457+
// Non-zero when file cleaning is disableCount. The disableCount
458+
// count acts as a reference count to prohibit file cleaning. See
459+
// DB.{disable,enable}FileDeletions().
460+
disableCount int
461+
// queuedStats holds cumulative stats for files that have been
462+
// queued for deletion by the cleanup manager. These stats are
463+
// monotonically increasing for the *DB's lifetime.
464+
queuedStats obsoleteObjectStats
465+
}
460466

461467
snapshots struct {
462468
// The list of active snapshots.
@@ -2055,6 +2061,7 @@ func (d *DB) AsyncFlush() (<-chan struct{}, error) {
20552061
func (d *DB) Metrics() *Metrics {
20562062
metrics := &Metrics{}
20572063
walStats := d.mu.log.manager.Stats()
2064+
completedObsoleteFileStats := d.cleanupManager.CompletedStats()
20582065

20592066
d.mu.Lock()
20602067
vers := d.mu.versions.currentVersion()
@@ -2114,6 +2121,30 @@ func (d *DB) Metrics() *Metrics {
21142121
metrics.Table.ZombieCount = int64(d.mu.versions.zombieTables.Count())
21152122
metrics.Table.ZombieSize = d.mu.versions.zombieTables.TotalSize()
21162123
metrics.Table.Local.ZombieCount, metrics.Table.Local.ZombieSize = d.mu.versions.zombieTables.LocalStats()
2124+
2125+
// The obsolete blob/table metrics have a subtle calculation:
2126+
//
2127+
// (A) The vs.metrics.{Table,BlobFiles}.[Local.]{ObsoleteCount,ObsoleteSize}
2128+
// fields reflect the set of files currently sitting in
2129+
// vs.obsolete{Tables,Blobs} but not yet enqueued to the cleanup manager.
2130+
//
2131+
// (B) The d.mu.fileDeletions.queuedStats field holds the set of files that have
2132+
// been queued for deletion by the cleanup manager.
2133+
//
2134+
// (C) The cleanup manager also maintains cumulative stats for the set of
2135+
// files that have been deleted.
2136+
//
2137+
// The value of currently pending obsolete files is (A) + (B) - (C).
2138+
pendingObsoleteFileStats := d.mu.fileDeletions.queuedStats
2139+
pendingObsoleteFileStats.Sub(completedObsoleteFileStats)
2140+
metrics.Table.Local.ObsoleteCount += pendingObsoleteFileStats.tablesLocal.count
2141+
metrics.Table.Local.ObsoleteSize += pendingObsoleteFileStats.tablesLocal.size
2142+
metrics.Table.ObsoleteCount += int64(pendingObsoleteFileStats.tablesAll.count)
2143+
metrics.Table.ObsoleteSize += pendingObsoleteFileStats.tablesAll.size
2144+
metrics.BlobFiles.Local.ObsoleteCount += pendingObsoleteFileStats.blobFilesLocal.count
2145+
metrics.BlobFiles.Local.ObsoleteSize += pendingObsoleteFileStats.blobFilesLocal.size
2146+
metrics.BlobFiles.ObsoleteCount += pendingObsoleteFileStats.blobFilesAll.count
2147+
metrics.BlobFiles.ObsoleteSize += pendingObsoleteFileStats.blobFilesAll.size
21172148
metrics.private.optionsFileSize = d.optionsFileSize
21182149

21192150
// TODO(jackson): Consider making these metrics optional.

obsolete_files.go

Lines changed: 85 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,9 @@ type DeleteCleaner = base.DeleteCleaner
3333
type ArchiveCleaner = base.ArchiveCleaner
3434

3535
type cleanupManager struct {
36-
opts *Options
37-
objProvider objstorage.Provider
38-
onTableDeleteFn func(fileSize uint64, isLocal bool)
39-
deletePacer *deletionPacer
36+
opts *Options
37+
objProvider objstorage.Provider
38+
deletePacer *deletionPacer
4039

4140
// jobsCh is used as the cleanup job queue.
4241
jobsCh chan *cleanupJob
@@ -47,12 +46,21 @@ type cleanupManager struct {
4746
sync.Mutex
4847
// totalJobs is the total number of enqueued jobs (completed or in progress).
4948
totalJobs int
49+
completedStats obsoleteObjectStats
5050
completedJobs int
5151
completedJobsCond sync.Cond
5252
jobsQueueWarningIssued bool
5353
}
5454
}
5555

56+
// CompletedStats returns the stats summarizing objects deleted. The returned
57+
// stats increase monotonically over the lifetime of the DB.
58+
func (m *cleanupManager) CompletedStats() obsoleteObjectStats {
59+
m.mu.Lock()
60+
defer m.mu.Unlock()
61+
return m.mu.completedStats
62+
}
63+
5664
// We can queue this many jobs before we have to block EnqueueJob.
5765
const jobsQueueDepth = 1000
5866

@@ -74,20 +82,17 @@ func (of *obsoleteFile) needsPacing() bool {
7482
type cleanupJob struct {
7583
jobID JobID
7684
obsoleteFiles []obsoleteFile
85+
stats obsoleteObjectStats
7786
}
7887

7988
// openCleanupManager creates a cleanupManager and starts its background goroutine.
8089
// The cleanupManager must be Close()d.
8190
func openCleanupManager(
82-
opts *Options,
83-
objProvider objstorage.Provider,
84-
onTableDeleteFn func(fileSize uint64, isLocal bool),
85-
getDeletePacerInfo func() deletionPacerInfo,
91+
opts *Options, objProvider objstorage.Provider, getDeletePacerInfo func() deletionPacerInfo,
8692
) *cleanupManager {
8793
cm := &cleanupManager{
88-
opts: opts,
89-
objProvider: objProvider,
90-
onTableDeleteFn: onTableDeleteFn,
94+
opts: opts,
95+
objProvider: objProvider,
9196
deletePacer: newDeletionPacer(
9297
crtime.NowMono(),
9398
opts.FreeSpaceThresholdBytes,
@@ -119,10 +124,13 @@ func (cm *cleanupManager) Close() {
119124
}
120125

121126
// EnqueueJob adds a cleanup job to the manager's queue.
122-
func (cm *cleanupManager) EnqueueJob(jobID JobID, obsoleteFiles []obsoleteFile) {
127+
func (cm *cleanupManager) EnqueueJob(
128+
jobID JobID, obsoleteFiles []obsoleteFile, stats obsoleteObjectStats,
129+
) {
123130
job := &cleanupJob{
124131
jobID: jobID,
125132
obsoleteFiles: obsoleteFiles,
133+
stats: stats,
126134
}
127135

128136
// Report deleted bytes to the pacer, which can use this data to potentially
@@ -174,7 +182,6 @@ func (cm *cleanupManager) mainLoop() {
174182
switch of.fileType {
175183
case base.FileTypeTable:
176184
cm.maybePace(&tb, &of)
177-
cm.onTableDeleteFn(of.fileSize, of.isLocal)
178185
cm.deleteObsoleteObject(of.fileType, job.jobID, of.fileNum)
179186
case base.FileTypeBlob:
180187
cm.maybePace(&tb, &of)
@@ -185,6 +192,7 @@ func (cm *cleanupManager) mainLoop() {
185192
}
186193
cm.mu.Lock()
187194
cm.mu.completedJobs++
195+
cm.mu.completedStats.Add(job.stats)
188196
cm.mu.completedJobsCond.Broadcast()
189197
cm.maybeLogLocked()
190198
cm.mu.Unlock()
@@ -320,18 +328,6 @@ func (d *DB) getDeletionPacerInfo() deletionPacerInfo {
320328
return pacerInfo
321329
}
322330

323-
// onObsoleteTableDelete is called to update metrics when an sstable is deleted.
324-
func (d *DB) onObsoleteTableDelete(fileSize uint64, isLocal bool) {
325-
d.mu.Lock()
326-
d.mu.versions.metrics.Table.ObsoleteCount--
327-
d.mu.versions.metrics.Table.ObsoleteSize -= fileSize
328-
if isLocal {
329-
d.mu.versions.metrics.Table.Local.ObsoleteCount--
330-
d.mu.versions.metrics.Table.Local.ObsoleteSize -= fileSize
331-
}
332-
d.mu.Unlock()
333-
}
334-
335331
// scanObsoleteFiles scans the filesystem for files that are no longer needed
336332
// and adds those to the internal lists of obsolete files. Note that the files
337333
// are not actually deleted by this method. A subsequent call to
@@ -448,7 +444,7 @@ func (d *DB) scanObsoleteFiles(list []string, flushableIngests []*ingestedFlusha
448444
//
449445
// d.mu must be held when calling this method.
450446
func (d *DB) disableFileDeletions() {
451-
d.mu.disableFileDeletions++
447+
d.mu.fileDeletions.disableCount++
452448
d.mu.Unlock()
453449
defer d.mu.Lock()
454450
d.cleanupManager.Wait()
@@ -459,11 +455,11 @@ func (d *DB) disableFileDeletions() {
459455
//
460456
// d.mu must be held when calling this method.
461457
func (d *DB) enableFileDeletions() {
462-
if d.mu.disableFileDeletions <= 0 {
458+
if d.mu.fileDeletions.disableCount <= 0 {
463459
panic("pebble: file deletion disablement invariant violated")
464460
}
465-
d.mu.disableFileDeletions--
466-
if d.mu.disableFileDeletions > 0 {
461+
d.mu.fileDeletions.disableCount--
462+
if d.mu.fileDeletions.disableCount > 0 {
467463
return
468464
}
469465
d.deleteObsoleteFiles(d.newJobIDLocked())
@@ -478,7 +474,7 @@ type fileInfo = base.FileInfo
478474
// Does nothing if file deletions are disabled (see disableFileDeletions). A
479475
// cleanup job will be scheduled when file deletions are re-enabled.
480476
func (d *DB) deleteObsoleteFiles(jobID JobID) {
481-
if d.mu.disableFileDeletions > 0 {
477+
if d.mu.fileDeletions.disableCount > 0 {
482478
return
483479
}
484480
_, noRecycle := d.opts.Cleaner.(base.NeedsFileContents)
@@ -524,6 +520,16 @@ func (d *DB) deleteObsoleteFiles(jobID JobID) {
524520
obsoleteOptions := d.mu.versions.obsoleteOptions
525521
d.mu.versions.obsoleteOptions = nil
526522

523+
// Compute the stats for the files being queued for deletion and add them to
524+
// the running total. These stats will be used during DB.Metrics() to
525+
// calculate the count and size of pending obsolete files by diffing these
526+
// stats and the stats reported by the cleanup manager.
527+
var objectStats obsoleteObjectStats
528+
objectStats.tablesAll, objectStats.tablesLocal = calculateObsoleteObjectStats(obsoleteTables)
529+
objectStats.blobFilesAll, objectStats.blobFilesLocal = calculateObsoleteObjectStats(obsoleteBlobs)
530+
d.mu.fileDeletions.queuedStats.Add(objectStats)
531+
d.mu.versions.updateObsoleteObjectMetricsLocked()
532+
527533
// Release d.mu while preparing the cleanup job and possibly waiting.
528534
// Note the unusual order: Unlock and then Lock.
529535
d.mu.Unlock()
@@ -552,7 +558,7 @@ func (d *DB) deleteObsoleteFiles(jobID JobID) {
552558
d.fileCache.Evict(f.fileNum, base.FileTypeBlob)
553559
}
554560
if len(filesToDelete) > 0 {
555-
d.cleanupManager.EnqueueJob(jobID, filesToDelete)
561+
d.cleanupManager.EnqueueJob(jobID, filesToDelete, objectStats)
556562
}
557563
if d.opts.private.testingAlwaysWaitForCleanup {
558564
d.cleanupManager.Wait()
@@ -601,6 +607,54 @@ func (o objectInfo) asObsoleteFile(fs vfs.FS, fileType base.FileType, dirname st
601607
}
602608
}
603609

610+
func calculateObsoleteObjectStats(files []obsoleteFile) (total, local countAndSize) {
611+
for _, of := range files {
612+
if of.isLocal {
613+
local.count++
614+
local.size += of.fileSize
615+
}
616+
total.count++
617+
total.size += of.fileSize
618+
}
619+
return total, local
620+
}
621+
622+
type obsoleteObjectStats struct {
623+
tablesLocal countAndSize
624+
tablesAll countAndSize
625+
blobFilesLocal countAndSize
626+
blobFilesAll countAndSize
627+
}
628+
629+
func (s *obsoleteObjectStats) Add(other obsoleteObjectStats) {
630+
s.tablesLocal.Add(other.tablesLocal)
631+
s.tablesAll.Add(other.tablesAll)
632+
s.blobFilesLocal.Add(other.blobFilesLocal)
633+
s.blobFilesAll.Add(other.blobFilesAll)
634+
}
635+
636+
func (s *obsoleteObjectStats) Sub(other obsoleteObjectStats) {
637+
s.tablesLocal.Sub(other.tablesLocal)
638+
s.tablesAll.Sub(other.tablesAll)
639+
s.blobFilesLocal.Sub(other.blobFilesLocal)
640+
s.blobFilesAll.Sub(other.blobFilesAll)
641+
}
642+
643+
type countAndSize struct {
644+
count uint64
645+
size uint64
646+
}
647+
648+
func (c *countAndSize) Add(other countAndSize) {
649+
c.count += other.count
650+
c.size += other.size
651+
}
652+
653+
func (c *countAndSize) Sub(other countAndSize) {
654+
c.count = invariants.SafeSub(c.count, other.count)
655+
c.size = invariants.SafeSub(c.size, other.size)
656+
}
657+
604658
func makeZombieObjects() zombieObjects {
605659
return zombieObjects{
606660
objs: make(map[base.DiskFileNum]objectInfo),

open.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,7 @@ func Open(dirname string, opts *Options) (db *DB, err error) {
391391

392392
d.mu.log.manager = walManager
393393

394-
d.cleanupManager = openCleanupManager(opts, d.objProvider, d.onObsoleteTableDelete, d.getDeletionPacerInfo)
394+
d.cleanupManager = openCleanupManager(opts, d.objProvider, d.getDeletionPacerInfo)
395395

396396
if manifestExists && !opts.DisableConsistencyCheck {
397397
curVersion := d.mu.versions.currentVersion()

version_set.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1197,6 +1197,11 @@ func (vs *versionSet) addObsolete(obsolete manifest.ObsoleteFiles) {
11971197
}
11981198

11991199
func (vs *versionSet) updateObsoleteObjectMetricsLocked() {
1200+
// TODO(jackson): Ideally we would update vs.fileDeletions.queuedStats to
1201+
// include the files on vs.obsolete{Tables,Blobs}, but there's subtlety in
1202+
// deduplicating the files before computing the stats. It might also be
1203+
// possible to refactor to remove the vs.obsolete{Tables,Blobs} intermediary
1204+
// step. Revisit this.
12001205
vs.metrics.Table.ObsoleteCount = int64(len(vs.obsoleteTables))
12011206
vs.metrics.Table.ObsoleteSize = 0
12021207
vs.metrics.Table.Local.ObsoleteSize = 0

0 commit comments

Comments
 (0)