222 lines
6.8 KiB
Go
222 lines
6.8 KiB
Go
// Copyright 2019 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 (
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/cockroachdb/errors"
|
|
"github.com/cockroachdb/pebble/internal/rate"
|
|
)
|
|
|
|
var nilPacer = &noopPacer{}
|
|
|
|
type limiter interface {
|
|
DelayN(now time.Time, n int) time.Duration
|
|
AllowN(now time.Time, n int) bool
|
|
Burst() int
|
|
}
|
|
|
|
// pacer is the interface for flush and compaction rate limiters. The rate limiter
|
|
// is possible applied on each iteration step of a flush or compaction. This is to
|
|
// limit background IO usage so that it does not contend with foreground traffic.
|
|
type pacer interface {
|
|
maybeThrottle(bytesIterated uint64) error
|
|
|
|
reportDeletion(bytesToDelete uint64)
|
|
}
|
|
|
|
// deletionPacerInfo contains any info from the db necessary to make deletion
|
|
// pacing decisions.
|
|
type deletionPacerInfo struct {
|
|
freeBytes uint64
|
|
obsoleteBytes uint64
|
|
liveBytes uint64
|
|
}
|
|
|
|
// deletionPacer rate limits deletions of obsolete files. This is necessary to
|
|
// prevent overloading the disk with too many deletions too quickly after a
|
|
// large compaction, or an iterator close. On some SSDs, disk performance can be
|
|
// negatively impacted if too many blocks are deleted very quickly, so this
|
|
// mechanism helps mitigate that.
|
|
type deletionPacer struct {
|
|
limiter limiter
|
|
freeSpaceThreshold uint64
|
|
obsoleteBytesMaxRatio float64
|
|
targetByteDeletionRate int64
|
|
|
|
getInfo func() deletionPacerInfo
|
|
|
|
mu struct {
|
|
sync.Mutex
|
|
|
|
// history keeps rack of recent deletion history; it used to increase the
|
|
// deletion rate to match the pace of deletions.
|
|
history history
|
|
}
|
|
}
|
|
|
|
const deletePacerHistory = 5 * time.Minute
|
|
|
|
// newDeletionPacer instantiates a new deletionPacer for use when deleting
|
|
// obsolete files. The limiter passed in must be a singleton shared across this
|
|
// pebble instance.
|
|
func newDeletionPacer(
|
|
targetByteDeletionRate int64, getInfo func() deletionPacerInfo,
|
|
) *deletionPacer {
|
|
d := &deletionPacer{
|
|
limiter: rate.NewLimiter(rate.Limit(targetByteDeletionRate), int(targetByteDeletionRate)),
|
|
|
|
// If there are less than freeSpaceThreshold bytes of free space on
|
|
// disk, do not pace deletions at all.
|
|
freeSpaceThreshold: 16 << 30, // 16 GB
|
|
// If the ratio of obsolete bytes to live bytes is greater than
|
|
// obsoleteBytesMaxRatio, do not pace deletions at all.
|
|
obsoleteBytesMaxRatio: 0.20,
|
|
|
|
targetByteDeletionRate: targetByteDeletionRate,
|
|
|
|
getInfo: getInfo,
|
|
}
|
|
d.mu.history.Init(time.Now(), deletePacerHistory)
|
|
return d
|
|
}
|
|
|
|
// limit applies rate limiting if the current free disk space is more than
|
|
// freeSpaceThreshold, and the ratio of obsolete to live bytes is less than
|
|
// obsoleteBytesMaxRatio.
|
|
func (p *deletionPacer) limit(amount uint64, info deletionPacerInfo) error {
|
|
obsoleteBytesRatio := float64(1.0)
|
|
if info.liveBytes > 0 {
|
|
obsoleteBytesRatio = float64(info.obsoleteBytes) / float64(info.liveBytes)
|
|
}
|
|
paceDeletions := info.freeBytes > p.freeSpaceThreshold &&
|
|
obsoleteBytesRatio < p.obsoleteBytesMaxRatio
|
|
|
|
p.mu.Lock()
|
|
if historyRate := p.mu.history.Sum(time.Now()) / int64(deletePacerHistory/time.Second); historyRate > p.targetByteDeletionRate {
|
|
// Deletions have been happening at a rate higher than the target rate; we
|
|
// want to speed up deletions so they match the historic rate. We do this by
|
|
// decreasing the amount accordingly.
|
|
amount = uint64(float64(p.targetByteDeletionRate) / float64(historyRate) * float64(amount))
|
|
}
|
|
p.mu.Unlock()
|
|
|
|
if paceDeletions {
|
|
burst := p.limiter.Burst()
|
|
for amount > uint64(burst) {
|
|
d := p.limiter.DelayN(time.Now(), burst)
|
|
if d == rate.InfDuration {
|
|
return errors.Errorf("pacing failed")
|
|
}
|
|
time.Sleep(d)
|
|
amount -= uint64(burst)
|
|
}
|
|
d := p.limiter.DelayN(time.Now(), int(amount))
|
|
if d == rate.InfDuration {
|
|
return errors.Errorf("pacing failed")
|
|
}
|
|
time.Sleep(d)
|
|
} else {
|
|
burst := p.limiter.Burst()
|
|
for amount > uint64(burst) {
|
|
// AllowN will subtract burst if there are enough tokens available,
|
|
// else leave the tokens untouched. That is, we are making a
|
|
// best-effort to account for this activity in the limiter, but by
|
|
// ignoring the return value, we do the activity instantaneously
|
|
// anyway.
|
|
p.limiter.AllowN(time.Now(), burst)
|
|
amount -= uint64(burst)
|
|
}
|
|
p.limiter.AllowN(time.Now(), int(amount))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// maybeThrottle slows down a deletion of this file if it's faster than
|
|
// opts.Experimental.MinDeletionRate.
|
|
func (p *deletionPacer) maybeThrottle(bytesToDelete uint64) error {
|
|
return p.limit(bytesToDelete, p.getInfo())
|
|
}
|
|
|
|
// reportDeletion is used to report a deletion to the pacer. The pacer uses it
|
|
// to keep track of the recent rate of deletions and potentially increase the
|
|
// deletion rate accordingly.
|
|
//
|
|
// reportDeletion is thread-safe.
|
|
func (p *deletionPacer) reportDeletion(bytesToDelete uint64) {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
p.mu.history.Add(time.Now(), int64(bytesToDelete))
|
|
}
|
|
|
|
type noopPacer struct{}
|
|
|
|
func (p *noopPacer) maybeThrottle(_ uint64) error {
|
|
return nil
|
|
}
|
|
|
|
func (p *noopPacer) reportDeletion(_ uint64) {}
|
|
|
|
// history is a helper used to keep track of the recent history of a set of
|
|
// data points (in our case deleted bytes), at limited granularity.
|
|
// Specifically, we split the desired timeframe into 100 "epochs" and all times
|
|
// are effectively rounded down to the nearest epoch boundary.
|
|
type history struct {
|
|
epochDuration time.Duration
|
|
startTime time.Time
|
|
// currEpoch is the epoch of the most recent operation.
|
|
currEpoch int64
|
|
// val contains the recent epoch values.
|
|
// val[currEpoch % historyEpochs] is the current epoch.
|
|
// val[(currEpoch + 1) % historyEpochs] is the oldest epoch.
|
|
val [historyEpochs]int64
|
|
// sum is always equal to the sum of values in val.
|
|
sum int64
|
|
}
|
|
|
|
const historyEpochs = 100
|
|
|
|
// Init the history helper to keep track of data over the given number of
|
|
// seconds.
|
|
func (h *history) Init(now time.Time, timeframe time.Duration) {
|
|
*h = history{
|
|
epochDuration: timeframe / time.Duration(historyEpochs),
|
|
startTime: now,
|
|
currEpoch: 0,
|
|
sum: 0,
|
|
}
|
|
}
|
|
|
|
// Add adds a value for the current time.
|
|
func (h *history) Add(now time.Time, val int64) {
|
|
h.advance(now)
|
|
h.val[h.currEpoch%historyEpochs] += val
|
|
h.sum += val
|
|
}
|
|
|
|
// Sum returns the sum of recent values. The result is approximate in that the
|
|
// cut-off time is within 1% of the exact one.
|
|
func (h *history) Sum(now time.Time) int64 {
|
|
h.advance(now)
|
|
return h.sum
|
|
}
|
|
|
|
func (h *history) epoch(t time.Time) int64 {
|
|
return int64(t.Sub(h.startTime) / h.epochDuration)
|
|
}
|
|
|
|
// advance advances the time to the given time.
|
|
func (h *history) advance(now time.Time) {
|
|
epoch := h.epoch(now)
|
|
for h.currEpoch < epoch {
|
|
h.currEpoch++
|
|
// Forget the data for the oldest epoch.
|
|
h.sum -= h.val[h.currEpoch%historyEpochs]
|
|
h.val[h.currEpoch%historyEpochs] = 0
|
|
}
|
|
}
|