github.com/cockroachdb/pebble@v1.1.1-0.20240513155919-3622ade60459/pacer.go (about)

     1  // Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use
     2  // of this source code is governed by a BSD-style license that can be found in
     3  // the LICENSE file.
     4  
     5  package pebble
     6  
     7  import (
     8  	"sync"
     9  	"time"
    10  )
    11  
    12  // deletionPacerInfo contains any info from the db necessary to make deletion
    13  // pacing decisions (to limit background IO usage so that it does not contend
    14  // with foreground traffic).
    15  type deletionPacerInfo struct {
    16  	freeBytes     uint64
    17  	obsoleteBytes uint64
    18  	liveBytes     uint64
    19  }
    20  
    21  // deletionPacer rate limits deletions of obsolete files. This is necessary to
    22  // prevent overloading the disk with too many deletions too quickly after a
    23  // large compaction, or an iterator close. On some SSDs, disk performance can be
    24  // negatively impacted if too many blocks are deleted very quickly, so this
    25  // mechanism helps mitigate that.
    26  type deletionPacer struct {
    27  	// If there are less than freeSpaceThreshold bytes of free space on
    28  	// disk, increase the pace of deletions such that we delete enough bytes to
    29  	// get back to the threshold within the freeSpaceTimeframe.
    30  	freeSpaceThreshold uint64
    31  	freeSpaceTimeframe time.Duration
    32  
    33  	// If the ratio of obsolete bytes to live bytes is greater than
    34  	// obsoleteBytesMaxRatio, increase the pace of deletions such that we delete
    35  	// enough bytes to get back to the threshold within the obsoleteBytesTimeframe.
    36  	obsoleteBytesMaxRatio  float64
    37  	obsoleteBytesTimeframe time.Duration
    38  
    39  	mu struct {
    40  		sync.Mutex
    41  
    42  		// history keeps rack of recent deletion history; it used to increase the
    43  		// deletion rate to match the pace of deletions.
    44  		history history
    45  	}
    46  
    47  	targetByteDeletionRate int64
    48  
    49  	getInfo func() deletionPacerInfo
    50  }
    51  
    52  const deletePacerHistory = 5 * time.Minute
    53  
    54  // newDeletionPacer instantiates a new deletionPacer for use when deleting
    55  // obsolete files.
    56  //
    57  // targetByteDeletionRate is the rate (in bytes/sec) at which we want to
    58  // normally limit deletes (when we are not falling behind or running out of
    59  // space). A value of 0.0 disables pacing.
    60  func newDeletionPacer(
    61  	now time.Time, targetByteDeletionRate int64, getInfo func() deletionPacerInfo,
    62  ) *deletionPacer {
    63  	d := &deletionPacer{
    64  		freeSpaceThreshold: 16 << 30, // 16 GB
    65  		freeSpaceTimeframe: 10 * time.Second,
    66  
    67  		obsoleteBytesMaxRatio:  0.20,
    68  		obsoleteBytesTimeframe: 5 * time.Minute,
    69  
    70  		targetByteDeletionRate: targetByteDeletionRate,
    71  		getInfo:                getInfo,
    72  	}
    73  	d.mu.history.Init(now, deletePacerHistory)
    74  	return d
    75  }
    76  
    77  // ReportDeletion is used to report a deletion to the pacer. The pacer uses it
    78  // to keep track of the recent rate of deletions and potentially increase the
    79  // deletion rate accordingly.
    80  //
    81  // ReportDeletion is thread-safe.
    82  func (p *deletionPacer) ReportDeletion(now time.Time, bytesToDelete uint64) {
    83  	p.mu.Lock()
    84  	defer p.mu.Unlock()
    85  	p.mu.history.Add(now, int64(bytesToDelete))
    86  }
    87  
    88  // PacingDelay returns the recommended pacing wait time (in seconds) for
    89  // deleting the given number of bytes.
    90  //
    91  // PacingDelay is thread-safe.
    92  func (p *deletionPacer) PacingDelay(now time.Time, bytesToDelete uint64) (waitSeconds float64) {
    93  	if p.targetByteDeletionRate == 0 {
    94  		// Pacing disabled.
    95  		return 0.0
    96  	}
    97  
    98  	baseRate := float64(p.targetByteDeletionRate)
    99  	// If recent deletion rate is more than our target, use that so that we don't
   100  	// fall behind.
   101  	historicRate := func() float64 {
   102  		p.mu.Lock()
   103  		defer p.mu.Unlock()
   104  		return float64(p.mu.history.Sum(now)) / deletePacerHistory.Seconds()
   105  	}()
   106  	if historicRate > baseRate {
   107  		baseRate = historicRate
   108  	}
   109  
   110  	// Apply heuristics to increase the deletion rate.
   111  	var extraRate float64
   112  	info := p.getInfo()
   113  	if info.freeBytes <= p.freeSpaceThreshold {
   114  		// Increase the rate so that we can free up enough bytes within the timeframe.
   115  		extraRate = float64(p.freeSpaceThreshold-info.freeBytes) / p.freeSpaceTimeframe.Seconds()
   116  	}
   117  	if info.liveBytes == 0 {
   118  		// We don't know the obsolete bytes ratio. Disable pacing altogether.
   119  		return 0.0
   120  	}
   121  	obsoleteBytesRatio := float64(info.obsoleteBytes) / float64(info.liveBytes)
   122  	if obsoleteBytesRatio >= p.obsoleteBytesMaxRatio {
   123  		// Increase the rate so that we can free up enough bytes within the timeframe.
   124  		r := (obsoleteBytesRatio - p.obsoleteBytesMaxRatio) * float64(info.liveBytes) / p.obsoleteBytesTimeframe.Seconds()
   125  		if extraRate < r {
   126  			extraRate = r
   127  		}
   128  	}
   129  
   130  	return float64(bytesToDelete) / (baseRate + extraRate)
   131  }
   132  
   133  // history is a helper used to keep track of the recent history of a set of
   134  // data points (in our case deleted bytes), at limited granularity.
   135  // Specifically, we split the desired timeframe into 100 "epochs" and all times
   136  // are effectively rounded down to the nearest epoch boundary.
   137  type history struct {
   138  	epochDuration time.Duration
   139  	startTime     time.Time
   140  	// currEpoch is the epoch of the most recent operation.
   141  	currEpoch int64
   142  	// val contains the recent epoch values.
   143  	// val[currEpoch % historyEpochs] is the current epoch.
   144  	// val[(currEpoch + 1) % historyEpochs] is the oldest epoch.
   145  	val [historyEpochs]int64
   146  	// sum is always equal to the sum of values in val.
   147  	sum int64
   148  }
   149  
   150  const historyEpochs = 100
   151  
   152  // Init the history helper to keep track of data over the given number of
   153  // seconds.
   154  func (h *history) Init(now time.Time, timeframe time.Duration) {
   155  	*h = history{
   156  		epochDuration: timeframe / time.Duration(historyEpochs),
   157  		startTime:     now,
   158  		currEpoch:     0,
   159  		sum:           0,
   160  	}
   161  }
   162  
   163  // Add adds a value for the current time.
   164  func (h *history) Add(now time.Time, val int64) {
   165  	h.advance(now)
   166  	h.val[h.currEpoch%historyEpochs] += val
   167  	h.sum += val
   168  }
   169  
   170  // Sum returns the sum of recent values. The result is approximate in that the
   171  // cut-off time is within 1% of the exact one.
   172  func (h *history) Sum(now time.Time) int64 {
   173  	h.advance(now)
   174  	return h.sum
   175  }
   176  
   177  func (h *history) epoch(t time.Time) int64 {
   178  	return int64(t.Sub(h.startTime) / h.epochDuration)
   179  }
   180  
   181  // advance advances the time to the given time.
   182  func (h *history) advance(now time.Time) {
   183  	epoch := h.epoch(now)
   184  	for h.currEpoch < epoch {
   185  		h.currEpoch++
   186  		// Forget the data for the oldest epoch.
   187  		h.sum -= h.val[h.currEpoch%historyEpochs]
   188  		h.val[h.currEpoch%historyEpochs] = 0
   189  	}
   190  }