Skip to content

Commit 0c4fa6e

Browse files
author
Andrew Baptist
committed
db: Smooth out IO from flushing L0 and compaction
This PR adds a smoother which monitors the average time for flushing and compaction and paces future flush / compaction loops to attempt to have a consistent IO rate at all times rather than being spikey. Spikey IO can result in saturating the underlying device which then slows down writes to the WAL. By having a consistent rate of flushing and compaction the P99 latency is greatly reduced.
1 parent 5fdb3ea commit 0c4fa6e

6 files changed

Lines changed: 202 additions & 1 deletion

File tree

compaction.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2623,6 +2623,9 @@ func (d *DB) runCompaction(
26232623
for key, val := iter.First(); key != nil || !c.rangeDelFrag.Empty() || !c.rangeKeyFrag.Empty(); {
26242624
splitterSuggestion := splitter.onNewOutput(key)
26252625

2626+
startTime := time.Now()
2627+
d.smoother.startWork()
2628+
26262629
// Each inner loop iteration processes one key from the input iterator.
26272630
for ; key != nil; key, val = iter.Next() {
26282631
if split := splitter.shouldSplitBefore(key, tw); split == splitNow {
@@ -2686,10 +2689,12 @@ func (d *DB) runCompaction(
26862689
}
26872690
if tw == nil {
26882691
if err := newOutput(); err != nil {
2692+
d.smoother.finishWork(time.Since(startTime), false)
26892693
return nil, pendingOutputs, err
26902694
}
26912695
}
26922696
if err := tw.Add(*key, val); err != nil {
2697+
d.smoother.finishWork(time.Since(startTime), false)
26932698
return nil, pendingOutputs, err
26942699
}
26952700
}
@@ -2718,8 +2723,10 @@ func (d *DB) runCompaction(
27182723
splitKey = key.UserKey
27192724
}
27202725
if err := finishOutput(splitKey); err != nil {
2726+
d.smoother.finishWork(time.Since(startTime), false)
27212727
return nil, pendingOutputs, err
27222728
}
2729+
d.smoother.finishWork(time.Since(startTime), true)
27232730
}
27242731

27252732
for _, cl := range c.inputs {

db.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,8 @@ type DB struct {
254254

255255
commit *commitPipeline
256256

257+
smoother Smoother
258+
257259
// readState provides access to the state needed for reading without needing
258260
// to acquire DB.mu.
259261
readState struct {

open.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,12 @@ func Open(dirname string, opts *Options) (db *DB, _ error) {
7878
logRecycler: logRecycler{limit: opts.MemTableStopWritesThreshold + 1},
7979
closed: new(atomic.Value),
8080
closedCh: make(chan struct{}),
81+
smoother: Smoother{enabled: opts.Experimental.SmoothWriteIO},
8182
}
8283
d.mu.versions = &versionSet{}
8384
d.atomic.diskAvailBytes = math.MaxUint64
8485
d.mu.versions.diskAvailBytes = d.getDiskAvailableBytesCached
86+
d.smoother.start()
8587

8688
defer func() {
8789
// If an error or panic occurs during open, attempt to release the manually

options.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ func (o *IterOptions) getLogger() Logger {
223223
// Specifically, when configured with a RangeKeyMasking.Suffix _s_, and there
224224
// exists a range key with suffix _r_ covering a point key with suffix _p_, and
225225
//
226-
// _s_ ≤ _r_ < _p_
226+
// _s_ ≤ _r_ < _p_
227227
//
228228
// then the point key is elided.
229229
//
@@ -571,6 +571,10 @@ type Options struct {
571571
// ability to optionally schedule additional CPU. See the documentation
572572
// for CPUWorkPermissionGranter for more details.
573573
CPUWorkPermissionGranter CPUWorkPermissionGranter
574+
575+
// SmoothWriteIO will attempt to write to disk at a constant rate from
576+
// compaction and flushing rather than as fast as it can.
577+
SmoothWriteIO bool
574578
}
575579

576580
// Filters is a map from filter policy name to filter policy. It is used for
@@ -1146,6 +1150,9 @@ func (o *Options) Parse(s string, hooks *ParseHooks) error {
11461150
// a backwards incompatible change. Instead, leave in support for parsing the
11471151
// key but simply don't parse the value.
11481152

1153+
// TODO: Remove this line
1154+
o.Experimental.SmoothWriteIO = true
1155+
11491156
switch {
11501157
case section == "Version":
11511158
switch key {
@@ -1279,6 +1286,8 @@ func (o *Options) Parse(s string, hooks *ParseHooks) error {
12791286
o.Experimental.ReadSamplingMultiplier, err = strconv.ParseInt(value, 10, 64)
12801287
case "table_cache_shards":
12811288
o.Experimental.TableCacheShards, err = strconv.Atoi(value)
1289+
case "enable_flush_smoothing":
1290+
o.Experimental.SmoothWriteIO = true
12821291
case "table_format":
12831292
switch value {
12841293
case "leveldb":

smoother.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package pebble
2+
3+
import (
4+
"fmt"
5+
"math"
6+
"sync"
7+
"sync/atomic"
8+
"time"
9+
)
10+
11+
const decayRate = 0.9
12+
const sampleRate = 10 * time.Millisecond
13+
const numSamples = 100
14+
const minUtilization = 0.1
15+
16+
// Smoother will attempt to smooth out a process that runs multiple iterations.
17+
// The goal is not to have it run faster or slower, but simply with pace itself
18+
// to run evenly over time rather than being bunchy.
19+
type Smoother struct {
20+
enabled bool
21+
countRunning int32
22+
sleepingCount int32
23+
stopper chan struct{}
24+
25+
mu struct {
26+
sync.Mutex
27+
estimatedIterDurationNs float64
28+
estimatedUtilization float64
29+
}
30+
}
31+
32+
func (s *Smoother) start() {
33+
s.mu.estimatedUtilization = 1.0
34+
s.stopper = make(chan struct{})
35+
36+
go func() {
37+
ticker := time.NewTicker(sampleRate)
38+
defer ticker.Stop()
39+
40+
var sampleRunning, sampleSleeping int32
41+
var totalSamples int32
42+
43+
for {
44+
select {
45+
case <-s.stopper:
46+
return
47+
48+
case <-ticker.C:
49+
totalSamples++
50+
sampleRunning += atomic.LoadInt32(&s.countRunning)
51+
sampleSleeping += atomic.LoadInt32(&s.sleepingCount)
52+
53+
// Every 100 iterations, update the estimated utilization under lock
54+
if totalSamples == numSamples {
55+
utilRunning := float64(sampleRunning) / float64(totalSamples)
56+
utilSleeping := float64(sampleSleeping) / float64(totalSamples)
57+
// Add all the running time and half the sleeping time.
58+
util := utilRunning + utilSleeping/2
59+
sampleRunning = 0
60+
sampleSleeping = 0
61+
totalSamples = 0
62+
63+
s.mu.Lock()
64+
updatedEstimate := float64(s.mu.estimatedUtilization)*decayRate + float64(util)*(1-decayRate)
65+
s.mu.estimatedUtilization = math.Max(updatedEstimate, minUtilization)
66+
s.mu.Unlock()
67+
}
68+
time.Sleep(sampleRate)
69+
}
70+
}
71+
}()
72+
}
73+
74+
func (s *Smoother) stop() {
75+
close(s.stopper)
76+
}
77+
78+
func (s *Smoother) startWork() {
79+
atomic.AddInt32(&s.countRunning, 1)
80+
}
81+
82+
// finishWork records that work has completed and returns the amount of time the
83+
// process should sleep after this work.
84+
func (s *Smoother) finishWork(workDuration time.Duration, shouldSleep bool) time.Duration {
85+
atomic.AddInt32(&s.countRunning, -1)
86+
s.mu.Lock()
87+
s.mu.estimatedIterDurationNs = float64(s.mu.estimatedIterDurationNs)*decayRate + float64(workDuration)*(1-decayRate)
88+
sleepTime := 0.0
89+
if s.mu.estimatedUtilization < 1 {
90+
sleepTime = s.mu.estimatedIterDurationNs * ((1 / s.mu.estimatedUtilization) - 1)
91+
}
92+
s.mu.Unlock()
93+
fmt.Println(sleepTime, s.mu.estimatedIterDurationNs, s.mu.estimatedUtilization)
94+
95+
// Duration to sleep if the caller wants to sleep after a call.
96+
// FIXME
97+
// if !s.enabled {
98+
// return time.Duration(0)
99+
// }
100+
if shouldSleep && sleepTime > 0 {
101+
atomic.AddInt32(&s.sleepingCount, 1)
102+
time.Sleep(time.Duration(sleepTime))
103+
atomic.AddInt32(&s.sleepingCount, -1)
104+
}
105+
return time.Duration(sleepTime)
106+
}

smoother_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package pebble
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
"time"
7+
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func TestSmootherOff(t *testing.T) {
12+
smoother := Smoother{enabled: false}
13+
smoother.mu.estimatedUtilization = 0.5
14+
smoother.startWork()
15+
sleepTime := smoother.finishWork(time.Duration(100), false)
16+
require.Equal(t, time.Duration(0), sleepTime)
17+
}
18+
19+
func TestSleepConverge(t *testing.T) {
20+
smoother := Smoother{enabled: true}
21+
for x := 0; x < 100; x++ {
22+
smoother.startWork()
23+
smoother.finishWork(time.Duration(x)*time.Millisecond, false)
24+
}
25+
sleepTime := smoother.finishWork(100*time.Millisecond, false)
26+
// This is ~91 because it converges to recent measurements
27+
require.InDelta(t, 91*time.Millisecond, sleepTime, float64(10*time.Microsecond))
28+
}
29+
30+
func TestMultipleRunners(t *testing.T) {
31+
smoother := Smoother{enabled: true}
32+
smoother.start()
33+
smoother.mu.estimatedUtilization = 1
34+
for i := 0; i < 11; i++ {
35+
for j := 0; j < 10; j++ {
36+
smoother.startWork()
37+
}
38+
time.Sleep(102 * time.Millisecond)
39+
for j := 0; j < 10; j++ {
40+
smoother.finishWork(0, true)
41+
}
42+
}
43+
smoother.stop()
44+
fmt.Println(smoother.mu.estimatedUtilization)
45+
// The estimate should be about 10% closer to 10 starting at 1.0.
46+
require.InDelta(t, 1.9, smoother.mu.estimatedUtilization, 0.001)
47+
}
48+
49+
func TestSleep(t *testing.T) {
50+
smoother := Smoother{enabled: true}
51+
smoother.mu.estimatedIterDurationNs = float64(10 * time.Millisecond)
52+
smoother.mu.estimatedUtilization = 0.5
53+
54+
smoother.startWork()
55+
smoother.finishWork(1*time.Millisecond, true)
56+
57+
require.Equal(t, smoother.mu.estimatedUtilization, 0.5)
58+
require.InDelta(t, smoother.mu.estimatedIterDurationNs, float64(9*time.Millisecond), float64(time.Millisecond))
59+
}
60+
61+
func TestAvgUtil(t *testing.T) {
62+
smoother := Smoother{enabled: true}
63+
smoother.start()
64+
smoother.mu.estimatedUtilization = 0.5
65+
for i := 0; i < 11; i++ {
66+
smoother.startWork()
67+
time.Sleep(102 * time.Millisecond)
68+
smoother.finishWork(0, false)
69+
time.Sleep(102 * time.Millisecond)
70+
}
71+
smoother.stop()
72+
// It should never be exactly 0.5, but should be close. This test has a
73+
// potential to randomly fail, so think about ways to fix it.
74+
require.InDelta(t, 0.5, smoother.mu.estimatedUtilization, 0.01)
75+
}

0 commit comments

Comments
 (0)