github.com/cockroachdb/pebble@v0.0.0-20231214172447-ab4952c5f87b/internal/pacertoy/pebble/main.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 main
     6  
     7  import (
     8  	"fmt"
     9  	"math"
    10  	"os"
    11  	"sync"
    12  	"sync/atomic"
    13  	"time"
    14  
    15  	"github.com/cockroachdb/pebble/internal/rate"
    16  	"golang.org/x/exp/rand"
    17  )
    18  
    19  const (
    20  	// If measureLatency is true, the simulator outputs p50, p95, and p99
    21  	// latencies after all writes are completed. In this mode, all writes
    22  	// are immediately queued. If this is disabled, writes come in continually
    23  	// at different rates.
    24  	measureLatency = false
    25  
    26  	writeAmount = 2000 << 20 // 2 GB
    27  
    28  	// Max rate for all compactions. This is intentionally set low enough that
    29  	// user writes will have to be delayed.
    30  	maxCompactionRate = 100 << 20 // 100 MB/s
    31  	minCompactionRate = 20 << 20  // 20 MB/s
    32  
    33  	memtableSize        = 64 << 20 // 64 MB
    34  	maxMemtableCount    = 5
    35  	drainDelayThreshold = 1.05 * memtableSize
    36  	maxFlushRate        = 30 << 20 // 30 MB/s
    37  	minFlushRate        = 4 << 20  // 4 MB/s
    38  
    39  	l0CompactionThreshold = 1
    40  
    41  	levelRatio = 10
    42  	numLevels  = 7
    43  
    44  	compactionDebtSlowdownThreshold = 2 * memtableSize
    45  )
    46  
    47  type compactionPacer struct {
    48  	level      atomic.Int64
    49  	maxDrainer *rate.Limiter
    50  	minDrainer *rate.Limiter
    51  }
    52  
    53  func newCompactionPacer() *compactionPacer {
    54  	p := &compactionPacer{
    55  		maxDrainer: rate.NewLimiter(maxCompactionRate, maxCompactionRate),
    56  		minDrainer: rate.NewLimiter(minCompactionRate, minCompactionRate),
    57  	}
    58  	return p
    59  }
    60  
    61  func (p *compactionPacer) fill(n int64) {
    62  	p.level.Add(n)
    63  }
    64  
    65  func (p *compactionPacer) drain(n int64, delay bool) bool {
    66  	p.maxDrainer.Wait(float64(n))
    67  
    68  	if delay {
    69  		p.minDrainer.Wait(float64(n))
    70  	}
    71  	level := p.level.Add(-n)
    72  	return level <= compactionDebtSlowdownThreshold
    73  }
    74  
    75  type flushPacer struct {
    76  	level           atomic.Int64
    77  	drainDelayLevel float64
    78  	fillCond        sync.Cond
    79  	// minDrainer is the drainer which sets the minimum speed of draining.
    80  	minDrainer *rate.Limiter
    81  	// maxDrainer is the drainer which sets the maximum speed of draining.
    82  	maxDrainer *rate.Limiter
    83  }
    84  
    85  func newFlushPacer(mu *sync.Mutex) *flushPacer {
    86  	p := &flushPacer{
    87  		drainDelayLevel: drainDelayThreshold,
    88  		minDrainer:      rate.NewLimiter(minFlushRate, minFlushRate),
    89  		maxDrainer:      rate.NewLimiter(maxFlushRate, maxFlushRate),
    90  	}
    91  	p.fillCond.L = mu
    92  	return p
    93  }
    94  
    95  func (p *flushPacer) fill(n int64) {
    96  	p.level.Add(n)
    97  	p.fillCond.Signal()
    98  }
    99  
   100  func (p *flushPacer) drain(n int64, delay bool) bool {
   101  	p.maxDrainer.Wait(float64(n))
   102  
   103  	if delay {
   104  		p.minDrainer.Wait(float64(n))
   105  	}
   106  	level := p.level.Add(-n)
   107  	p.fillCond.Signal()
   108  	return float64(level) <= p.drainDelayLevel
   109  }
   110  
   111  // DB models a Pebble DB.
   112  type DB struct {
   113  	mu         sync.Mutex
   114  	flushPacer *flushPacer
   115  	flushCond  sync.Cond
   116  	memtables  []*int64
   117  	fill       atomic.Int64
   118  	drain      atomic.Int64
   119  
   120  	compactionMu    sync.Mutex
   121  	compactionPacer *compactionPacer
   122  	// L0 is represented as an array of integers whereas every other level
   123  	// is represented as a single integer.
   124  	L0 []*int64
   125  	// Non-L0 sstables. sstables[0] == L1.
   126  	sstables            []atomic.Int64
   127  	maxSSTableSizes     []int64
   128  	compactionFlushCond sync.Cond
   129  	prevCompactionDebt  float64
   130  }
   131  
   132  func newDB() *DB {
   133  	db := &DB{}
   134  	db.flushPacer = newFlushPacer(&db.mu)
   135  	db.flushCond.L = &db.mu
   136  	db.memtables = append(db.memtables, new(int64))
   137  
   138  	db.compactionFlushCond.L = &db.compactionMu
   139  	db.L0 = append(db.L0, new(int64))
   140  	db.compactionPacer = newCompactionPacer()
   141  
   142  	db.maxSSTableSizes = make([]int64, numLevels-1)
   143  	db.sstables = make([]atomic.Int64, numLevels-1)
   144  	base := int64(levelRatio)
   145  	for i := uint64(0); i < numLevels-2; i++ {
   146  		// Each level is 10 times larger than the one above it.
   147  		db.maxSSTableSizes[i] = memtableSize * l0CompactionThreshold * base
   148  		base *= levelRatio
   149  
   150  		// Begin with each level full.
   151  		newLevel := db.maxSSTableSizes[i]
   152  
   153  		db.sstables[i].Store(newLevel)
   154  	}
   155  	db.sstables[numLevels-2].Store(0)
   156  	db.maxSSTableSizes[numLevels-2] = math.MaxInt64
   157  
   158  	go db.drainMemtable()
   159  	go db.drainCompaction()
   160  
   161  	return db
   162  }
   163  
   164  // drainCompaction simulates background compactions.
   165  func (db *DB) drainCompaction() {
   166  	rng := rand.New(rand.NewSource(1))
   167  
   168  	for {
   169  		db.compactionMu.Lock()
   170  
   171  		for len(db.L0) <= l0CompactionThreshold {
   172  			db.compactionFlushCond.Wait()
   173  		}
   174  		l0Table := db.L0[0]
   175  		db.compactionMu.Unlock()
   176  
   177  		var delay bool
   178  		for i, size := int64(0), int64(0); i < *l0Table; i += size {
   179  			size = 10000 + rng.Int63n(500)
   180  			if size > (*l0Table - i) {
   181  				size = *l0Table - i
   182  			}
   183  			delay = db.compactionPacer.drain(size, delay)
   184  		}
   185  
   186  		db.compactionMu.Lock()
   187  		db.L0 = db.L0[1:]
   188  		db.compactionMu.Unlock()
   189  
   190  		singleTableSize := int64(memtableSize)
   191  		tablesToCompact := 0
   192  		for i := range db.sstables {
   193  			newSSTableSize := db.sstables[i].Add(singleTableSize)
   194  			if newSSTableSize > db.maxSSTableSizes[i] {
   195  				db.sstables[i].Add(-singleTableSize)
   196  				tablesToCompact++
   197  			} else {
   198  				// Lower levels do not need compaction if level above it did not
   199  				// need compaction.
   200  				break
   201  			}
   202  		}
   203  
   204  		totalCompactionBytes := int64(tablesToCompact * memtableSize)
   205  
   206  		for t := 0; t < tablesToCompact; t++ {
   207  			db.compactionPacer.fill(memtableSize)
   208  			for i, size := int64(0), int64(0); i < memtableSize; i += size {
   209  				size = 10000 + rng.Int63n(500)
   210  				if size > (totalCompactionBytes - i) {
   211  					size = totalCompactionBytes - i
   212  				}
   213  				delay = db.compactionPacer.drain(size, delay)
   214  			}
   215  
   216  			db.delayMemtableDrain()
   217  		}
   218  	}
   219  }
   220  
   221  // fillCompaction fills L0 sstables.
   222  func (db *DB) fillCompaction(size int64) {
   223  	db.compactionMu.Lock()
   224  
   225  	db.compactionPacer.fill(size)
   226  
   227  	last := db.L0[len(db.L0)-1]
   228  	if *last+size > memtableSize {
   229  		last = new(int64)
   230  		db.L0 = append(db.L0, last)
   231  		db.compactionFlushCond.Signal()
   232  	}
   233  	*last += size
   234  
   235  	db.compactionMu.Unlock()
   236  }
   237  
   238  // drainMemtable simulates memtable flushing.
   239  func (db *DB) drainMemtable() {
   240  	rng := rand.New(rand.NewSource(2))
   241  
   242  	for {
   243  		db.mu.Lock()
   244  		for len(db.memtables) <= 1 {
   245  			db.flushCond.Wait()
   246  		}
   247  		memtable := db.memtables[0]
   248  		db.mu.Unlock()
   249  
   250  		var delay bool
   251  		for i, size := int64(0), int64(0); i < *memtable; i += size {
   252  			size = 1000 + rng.Int63n(50)
   253  			if size > (*memtable - i) {
   254  				size = *memtable - i
   255  			}
   256  			delay = db.flushPacer.drain(size, delay)
   257  			db.drain.Add(size)
   258  
   259  			db.fillCompaction(size)
   260  		}
   261  
   262  		db.delayMemtableDrain()
   263  
   264  		db.mu.Lock()
   265  		db.memtables = db.memtables[1:]
   266  		db.mu.Unlock()
   267  	}
   268  }
   269  
   270  // delayMemtableDrain applies memtable drain delays depending on compaction debt.
   271  func (db *DB) delayMemtableDrain() {
   272  	totalCompactionBytes := db.compactionPacer.level.Load()
   273  	compactionDebt := math.Max(float64(totalCompactionBytes)-l0CompactionThreshold*memtableSize, 0.0)
   274  
   275  	db.mu.Lock()
   276  	if compactionDebt > compactionDebtSlowdownThreshold {
   277  		// Compaction debt is above the threshold and the debt is growing. Throttle memtable flushing.
   278  		drainLimit := maxFlushRate * float64(compactionDebtSlowdownThreshold/compactionDebt)
   279  		if drainLimit > 0 && drainLimit <= maxFlushRate {
   280  			db.flushPacer.maxDrainer.SetRate(drainLimit)
   281  		}
   282  	} else {
   283  		// Continuously speed up memtable flushing to make sure that slowdown signal did not
   284  		// decrease the memtable flush rate by too much.
   285  		drainLimit := db.flushPacer.maxDrainer.Rate() * 1.05
   286  		if drainLimit > 0 && drainLimit <= maxFlushRate {
   287  			db.flushPacer.maxDrainer.SetRate(drainLimit)
   288  		}
   289  	}
   290  
   291  	db.prevCompactionDebt = compactionDebt
   292  	db.mu.Unlock()
   293  }
   294  
   295  // fillMemtable simulates memtable filling.
   296  func (db *DB) fillMemtable(size int64) {
   297  	db.mu.Lock()
   298  
   299  	for len(db.memtables) > maxMemtableCount {
   300  		db.flushPacer.fillCond.Wait()
   301  	}
   302  	db.flushPacer.fill(size)
   303  	db.fill.Add(size)
   304  
   305  	last := db.memtables[len(db.memtables)-1]
   306  	if *last+size > memtableSize {
   307  		last = new(int64)
   308  		db.memtables = append(db.memtables, last)
   309  		db.flushCond.Signal()
   310  	}
   311  	*last += size
   312  
   313  	db.mu.Unlock()
   314  }
   315  
   316  // simulateWrite simulates user writes.
   317  func simulateWrite(db *DB, measureLatencyMode bool) {
   318  	limiter := rate.NewLimiter(10<<20, 10<<20) // 10 MB/s
   319  	fmt.Printf("filling at 10 MB/sec\n")
   320  
   321  	setRate := func(mb int) {
   322  		fmt.Printf("filling at %d MB/sec\n", mb)
   323  		limiter.SetRate(float64(mb << 20))
   324  	}
   325  
   326  	if !measureLatencyMode {
   327  		go func() {
   328  			rng := rand.New(rand.NewSource(3))
   329  			for {
   330  				secs := 5 + rng.Intn(5)
   331  				time.Sleep(time.Duration(secs) * time.Second)
   332  				mb := 10 + rng.Intn(20)
   333  				setRate(mb)
   334  			}
   335  		}()
   336  	}
   337  
   338  	rng := rand.New(rand.NewSource(uint64(4)))
   339  
   340  	totalWrites := int64(0)
   341  	percentiles := []int64{50, 95, 99}
   342  	percentileIndex := 0
   343  	percentileTimes := make([]time.Time, 0)
   344  
   345  	startTime := time.Now()
   346  	for totalWrites <= writeAmount {
   347  		size := 1000 + rng.Int63n(50)
   348  		if !measureLatencyMode {
   349  			limiter.Wait(float64(size))
   350  		}
   351  		db.fillMemtable(size)
   352  
   353  		// Calculate latency percentiles
   354  		totalWrites += size
   355  		if percentileIndex < len(percentiles) && totalWrites > (percentiles[percentileIndex]*writeAmount/100) {
   356  			percentileTimes = append(percentileTimes, time.Now())
   357  			percentileIndex++
   358  		}
   359  	}
   360  
   361  	time.Sleep(time.Second * 10)
   362  	// Latency should only be measured when `limiter.WaitN` is removed.
   363  	if measureLatencyMode {
   364  		fmt.Printf("_____p50______p95______p99\n")
   365  		fmt.Printf("%8s %8s %8s\n",
   366  			time.Duration(percentileTimes[0].Sub(startTime).Seconds())*time.Second,
   367  			time.Duration(percentileTimes[1].Sub(startTime).Seconds())*time.Second,
   368  			time.Duration(percentileTimes[2].Sub(startTime).Seconds())*time.Second)
   369  	}
   370  
   371  	os.Exit(0)
   372  }
   373  
   374  func main() {
   375  	db := newDB()
   376  
   377  	go simulateWrite(db, measureLatency)
   378  
   379  	tick := time.NewTicker(time.Second)
   380  	start := time.Now()
   381  	lastNow := start
   382  	var lastFill, lastDrain int64
   383  
   384  	for i := 0; ; i++ {
   385  		<-tick.C
   386  		if (i % 20) == 0 {
   387  			fmt.Printf("_elapsed___memtbs____dirty_____fill____drain____cdebt__l0count___max-f-rate\n")
   388  		}
   389  
   390  		db.mu.Lock()
   391  		memtableCount := len(db.memtables)
   392  		db.mu.Unlock()
   393  		dirty := db.flushPacer.level.Load()
   394  		fill := db.fill.Load()
   395  		drain := db.drain.Load()
   396  
   397  		db.compactionMu.Lock()
   398  		compactionL0 := len(db.L0)
   399  		db.compactionMu.Unlock()
   400  		totalCompactionBytes := db.compactionPacer.level.Load()
   401  		compactionDebt := math.Max(float64(totalCompactionBytes)-l0CompactionThreshold*memtableSize, 0.0)
   402  		maxFlushRate := db.flushPacer.maxDrainer.Rate()
   403  
   404  		now := time.Now()
   405  		elapsed := now.Sub(lastNow).Seconds()
   406  		fmt.Printf("%8s %8d %8.1f %8.1f %8.1f %8.1f %8d %12.1f\n",
   407  			time.Duration(now.Sub(start).Seconds()+0.5)*time.Second,
   408  			memtableCount,
   409  			float64(dirty)/(1024.0*1024.0),
   410  			float64(fill-lastFill)/(1024.0*1024.0*elapsed),
   411  			float64(drain-lastDrain)/(1024.0*1024.0*elapsed),
   412  			compactionDebt/(1024.0*1024.0),
   413  			compactionL0,
   414  			maxFlushRate/(1024.0*1024.0))
   415  
   416  		lastNow = now
   417  		lastFill = fill
   418  		lastDrain = drain
   419  	}
   420  }