Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(gc): improve periodic GC logic #73

Merged
merged 1 commit into from
Oct 14, 2019
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
77 changes: 41 additions & 36 deletions datastore.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ type Datastore struct {
closing chan struct{}

gcDiscardRatio float64
maxGcDuration time.Duration
gcSleep time.Duration
gcInterval time.Duration
}

Expand All @@ -46,11 +46,13 @@ type txn struct {
type Options struct {
// Please refer to the Badger docs to see what this is for
GcDiscardRatio float64
// Maximum duration to perform a single GC cycle for
MaxGcDuration time.Duration

// Interval between GC cycles
GcInterval time.Duration

// Sleep time between rounds of a single GC cycle.
GcSleep time.Duration

badger.Options
}

Expand All @@ -60,8 +62,8 @@ var DefaultOptions Options
func init() {
DefaultOptions = Options{
GcDiscardRatio: 0.2,
MaxGcDuration: 1 * time.Minute,
GcInterval: 45 * time.Minute,
GcInterval: 15 * time.Minute,
GcSleep: 10 * time.Second,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reduced these as probabilistically sampling a log every 10 seconds shouldn't be that expensive.

Rational behind this algorithm:

  1. If we assume that deletes are randomly distributed, having one value log ready for garbage collection should corallite with other value logs being ready.
  2. After we do a full pass through all value logs, we shouldn't need to GC for a while.

That's why I have the short sleep/long sleep system.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Stebalien In your first point, do you mean to say that if a sample of a value log file "hits" the discard ratio, the probability that a sample in the next log file will do so too goes up ?

Please can you explain this in a bit more detail ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but, actually, I'm not so sure about my assumption.

  • Assumption: Deletes are randomly distributed between all the value logs.
  • Conclusion: At any given point in time, all value logs should have approximately the same number of discarded items. Therefore, if any one value log is ready to be garbage collected, others are also likely to be ready for garbage collection.

That's not quite correct. Values in a given value log are temporally correlated so deletes aren't likely to be completely random. However, the fact that enough time has passed for one value log to collect garbage is still a good indication that another value log may also have collected enough garbage for compaction.

Options: badger.DefaultOptions(""),
}
DefaultOptions.Options.CompactL0OnClose = false
Expand All @@ -80,17 +82,17 @@ func NewDatastore(path string, options *Options) (*Datastore, error) {
// Copy the options because we modify them.
var opt badger.Options
var gcDiscardRatio float64
var maxGcDuration time.Duration
var gcSleep time.Duration
var gcInterval time.Duration
if options == nil {
opt = badger.DefaultOptions("")
gcDiscardRatio = DefaultOptions.GcDiscardRatio
maxGcDuration = DefaultOptions.MaxGcDuration
gcSleep = DefaultOptions.GcSleep
gcInterval = DefaultOptions.GcInterval
} else {
opt = options.Options
gcDiscardRatio = options.GcDiscardRatio
maxGcDuration = options.MaxGcDuration
gcSleep = options.GcSleep
gcInterval = options.GcInterval
}

Expand All @@ -110,7 +112,7 @@ func NewDatastore(path string, options *Options) (*Datastore, error) {
DB: kv,
closing: make(chan struct{}),
gcDiscardRatio: gcDiscardRatio,
maxGcDuration: maxGcDuration,
gcSleep: gcSleep,
gcInterval: gcInterval,
}

Expand All @@ -122,11 +124,26 @@ func NewDatastore(path string, options *Options) (*Datastore, error) {

// Keep scheduling GC's AFTER `gcInterval` has passed since the previous GC
func (d *Datastore) periodicGC() {
gcTimeout := time.NewTimer(d.gcInterval)
defer gcTimeout.Stop()

for {
select {
case <-time.After(d.gcInterval):
if err := d.CollectGarbage(); err != nil {
log.Warningf("error during a GC cycle: %s", err)
case <-gcTimeout.C:
switch err := d.gcOnce(); err {
case badger.ErrNoRewrite, badger.ErrRejected:
// No rewrite means we've fully garbage collected.
// Rejected means someone else is running a GC
// or we're closing.
gcTimeout.Reset(d.gcInterval)
case nil:
gcTimeout.Reset(d.gcSleep)
case ErrClosed:
return
default:
log.Errorf("error during a GC cycle: %s", err)
// Not much we can do on a random error but log it and continue.
gcTimeout.Reset(d.gcInterval)
}
case <-d.closing:
return
Expand Down Expand Up @@ -313,31 +330,10 @@ func (d *Datastore) Batch() (ds.Batch, error) {
}

func (d *Datastore) CollectGarbage() (err error) {
d.closeLk.RLock()
defer d.closeLk.RUnlock()
if d.closed {
return ErrClosed
}

gcTimeout := time.NewTimer(d.maxGcDuration)
defer gcTimeout.Stop()

// The idea is to keep calling DB.RunValueLogGC() till Badger no longer has any log files
// to GC(which would be indicated by an error, please refer to Badger GC docs). The timeout is to
// ensure we do not keep calling GC in case Badger has accumulated
// excessive garbage. However, we will finish earlier if Badger has nothing left to GC.
LOOP:
for {
select {
case <-gcTimeout.C:
break LOOP
default:
if err == nil {
err = d.DB.RunValueLogGC(d.gcDiscardRatio)
} else {
break LOOP
}
}
// to GC(which would be indicated by an error, please refer to Badger GC docs).
for err == nil {
err = d.gcOnce()
}

if err == badger.ErrNoRewrite {
Expand All @@ -347,6 +343,15 @@ LOOP:
return err
}

func (d *Datastore) gcOnce() error {
d.closeLk.RLock()
defer d.closeLk.RUnlock()
if d.closed {
return ErrClosed
}
return d.DB.RunValueLogGC(d.gcDiscardRatio)
}

var _ ds.Datastore = (*txn)(nil)
var _ ds.TTLDatastore = (*txn)(nil)

Expand Down